Skip to content

IBAN Validator — C source

Validate International Bank Account Numbers (IBAN) with the mod-97 checksum, verify the country-specific length, and format the result. 100% client-side, no network.

This is the C implementation — the same logic the interactive tool runs, in a shareable, citable form.

/*
 * iban-validator — pure IBAN validation logic (ISO 13616 mod-97 checksum).
 *
 * Language: C (C11, standard library only)
 * Source:   CosmoDev polyglot showcase port of the iban-validator tool,
 *           ported from src/lib/iban.ts (the canonical TypeScript
 *           implementation).
 * License:  display source — part of CosmoDev's polyglot tool pages
 *
 * Normalizes the input (uppercase, strip whitespace/dashes), checks the
 * structure (country code + 2 check digits + 1-30 BBAN chars), verifies
 * the per-country length, then runs the ISO 13616 mod-97 checksum: move
 * the first 4 chars to the end, map A=10..Z=35, and confirm the
 * resulting integer is congruent to 1 mod 97. The remainder is folded
 * one decimal digit at a time (it stays < 97), so no big-integer or
 * regex machinery is required.
 */

#include <ctype.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
#include <string.h>

/* Per-country IBAN lengths (ISO 13616) — a representative subset. */
static const struct {
    const char *cc;
    int len;
} IBAN_LENGTHS[] = {
    {"AL", 28}, {"AD", 24}, {"AT", 20}, {"AZ", 28}, {"BH", 22}, {"BY", 28},
    {"BE", 16}, {"BA", 20}, {"BR", 29}, {"BG", 22}, {"CR", 22}, {"HR", 21},
    {"CY", 28}, {"CZ", 24}, {"DK", 18}, {"DO", 28}, {"EE", 20}, {"FO", 18},
    {"FI", 18}, {"FR", 27}, {"GE", 22}, {"DE", 22}, {"GI", 23}, {"GR", 27},
    {"GL", 18}, {"GT", 28}, {"HU", 28}, {"IS", 26}, {"IE", 22}, {"IL", 23},
    {"IT", 27}, {"JO", 30}, {"KZ", 20}, {"XK", 20}, {"KW", 30}, {"LV", 21},
    {"LB", 28}, {"LI", 21}, {"LT", 20}, {"LU", 20}, {"MK", 19}, {"MT", 31},
    {"MR", 27}, {"MU", 30}, {"MC", 27}, {"MD", 24}, {"ME", 22}, {"NL", 18},
    {"NO", 15}, {"PK", 24}, {"PS", 29}, {"PL", 28}, {"PT", 25}, {"QA", 29},
    {"RO", 24}, {"LC", 32}, {"SM", 27}, {"ST", 25}, {"SA", 24}, {"RS", 22},
    {"SC", 31}, {"SK", 24}, {"SI", 19}, {"SG", 19}, {"ES", 24}, {"SE", 24},
    {"CH", 21}, {"TL", 23}, {"TN", 24}, {"TR", 26}, {"UA", 29}, {"AE", 23},
    {"GB", 22}, {"VG", 24},
};

#define IBAN_LENGTHS_COUNT (sizeof IBAN_LENGTHS / sizeof IBAN_LENGTHS[0])

/* Result of validation — nullable fields are empty string / NULL / -1. */
typedef struct {
    char input[256];
    char cleaned[64];
    char country_code[3]; /* "" when the head is not 2 uppercase letters */
    bool valid;
    bool checksum_ok;
    bool length_ok;
    int expected_length; /* -1 when the country is unknown */
    char formatted[80];
    char error_buf[80];
    const char *error; /* NULL when valid; may point into error_buf */
} IbanInfo;

static int iban_length_for(const char *cc)
{
    for (size_t i = 0; i < IBAN_LENGTHS_COUNT; i++)
        if (strcmp(IBAN_LENGTHS[i].cc, cc) == 0)
            return IBAN_LENGTHS[i].len;
    return -1;
}

static bool is_upper_alpha(unsigned char c) { return c >= 'A' && c <= 'Z'; }
static bool is_digit_char(unsigned char c) { return c >= '0' && c <= '9'; }

/* ISO 13616 mod-97 checksum over a CLEANED iban (uppercase, no spaces). */
static bool mod97_check(const char *cleaned)
{
    size_t n = strlen(cleaned);
    if (n < 4)
        return false;
    /* Rearranged = cleaned[4..] ++ cleaned[..4] (country + check moved to
     * the end); index (i + 4) % n walks that rotation in place. */
    unsigned rem = 0;
    for (size_t i = 0; i < n; i++) {
        unsigned char ch = (unsigned char)cleaned[(i + 4) % n];
        unsigned value;
        int digits;
        if (is_digit_char(ch)) {
            value = ch - '0';
            digits = 1;
        } else if (is_upper_alpha(ch)) {
            value = ch - 'A' + 10; /* A=10 .. Z=35 */
            digits = 2;
        } else {
            return false; /* invalid character */
        }
        /* Fold one decimal digit at a time; rem stays < 97. */
        if (digits == 2)
            rem = (rem * 10 + value / 10) % 97;
        rem = (rem * 10 + value % 10) % 97;
    }
    return rem == 1;
}

/* Insert a space every 4 characters (last group may be short) — the
 * hand-rolled equivalent of the TS regex /(.{4})(?=.)/g + trim. */
static void format_groups(const char *cleaned, char *out, size_t cap)
{
    size_t n = strlen(cleaned), o = 0;
    for (size_t i = 0; i < n; i++) {
        if (i > 0 && i % 4 == 0 && o + 1 < cap)
            out[o++] = ' ';
        if (o + 1 < cap)
            out[o++] = cleaned[i];
    }
    out[o] = '\0';
}

/* Validate an IBAN. Never fails — all failures are reported via `error`. */
static IbanInfo validate_iban(const char *input)
{
    IbanInfo info = {0};
    info.expected_length = -1;
    snprintf(info.input, sizeof info.input, "%s", input ? input : "");

    /* Uppercase and strip whitespace/dashes. */
    size_t o = 0;
    for (const char *p = info.input; *p; p++) {
        unsigned char ch = (unsigned char)*p;
        if (ch == ' ' || ch == '-' || ch == '\t' || ch == '\n' || ch == '\r')
            continue;
        if (o + 1 < sizeof info.cleaned)
            info.cleaned[o++] = (char)toupper(ch);
    }
    info.cleaned[o] = '\0';

    size_t n = strlen(info.cleaned);
    if (n >= 2 && is_upper_alpha((unsigned char)info.cleaned[0]) &&
                 is_upper_alpha((unsigned char)info.cleaned[1])) {
        info.country_code[0] = info.cleaned[0];
        info.country_code[1] = info.cleaned[1];
        info.expected_length = iban_length_for(info.country_code);
    }
    format_groups(info.cleaned, info.formatted, sizeof info.formatted);

    /* Structure: 2 letters, 2 digits, then 1..30 alnum, total len 5..34. */
    bool struct_ok = n >= 5 && n <= 34
        && is_upper_alpha((unsigned char)info.cleaned[0])
        && is_upper_alpha((unsigned char)info.cleaned[1])
        && is_digit_char((unsigned char)info.cleaned[2])
        && is_digit_char((unsigned char)info.cleaned[3]);
    if (struct_ok) {
        for (size_t i = 4; i < n; i++) {
            unsigned char ch = (unsigned char)info.cleaned[i];
            if (!is_upper_alpha(ch) && !is_digit_char(ch)) {
                struct_ok = false;
                break;
            }
        }
    }
    if (!struct_ok) {
        info.error = "Invalid IBAN format.";
        return info;
    }

    info.checksum_ok = mod97_check(info.cleaned);
    info.length_ok = info.expected_length < 0
        || (int)n == info.expected_length;
    if (!info.length_ok) {
        snprintf(info.error_buf, sizeof info.error_buf,
                 "Length should be %d for %s.", info.expected_length,
                 info.country_code);
        info.error = info.error_buf;
        return info;
    }
    if (!info.checksum_ok) {
        info.error = "Checksum failed.";
        return info;
    }
    info.valid = true;
    return info;
}

int main(void)
{
    /* GB82 WEST 1234 5698 7654 32 — a known-good reference IBAN. */
    IbanInfo r = validate_iban("GB82WEST12345698765432");
    printf("valid=%d error=%s\n", r.valid, r.error ? r.error : "(none)");
    return 0;
}

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →