Skip to content

IBAN Validator — Kotlin 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 Kotlin 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: Kotlin (Kotlin 1.9, 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 BigInteger or
// regex machinery is required.

package org.cosmolabs.cosmodev.snippets

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

/** Result of validation — nullable fields mirror the TS nullable fields. */
data class IbanInfo(
    val input: String,
    val cleaned: String,
    val countryCode: String?,   // null when the head is not 2 uppercase letters
    val valid: Boolean = false,
    val checksumOk: Boolean = false,
    val lengthOk: Boolean = false,
    val expectedLength: Int? = null, // null when the country is unknown
    val formatted: String = "",
    val error: String? = null,  // null when valid
)

/** ISO 13616 mod-97 checksum over a CLEANED iban (uppercase, no spaces). */
fun mod97Check(cleaned: String): Boolean {
    if (cleaned.length < 4) return false
    // Move the first 4 chars (country + check) to the end.
    val rearranged = cleaned.substring(4) + cleaned.substring(0, 4)
    var rem = 0
    for (ch in rearranged) {
        val value: Int
        val digits: Int
        when {
            ch in '0'..'9' -> {
                value = ch - '0'
                digits = 1
            }
            ch in 'A'..'Z' -> {
                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.
 */
private fun formatGroups(cleaned: String): String =
    buildString(cleaned.length + cleaned.length / 4) {
        cleaned.forEachIndexed { i, ch ->
            if (i > 0 && i % 4 == 0) append(' ')
            append(ch)
        }
    }

/** Validate an IBAN. Never throws — all failures are reported via `error`. */
fun validateIban(input: String?): IbanInfo {
    val src = input.orEmpty()

    // Uppercase and strip whitespace/dashes.
    val cleaned = buildString {
        for (ch in src.uppercase()) {
            when (ch) {
                ' ', '-', '\t', '\n', '\r' -> {} // strip separators
                else -> append(ch)
            }
        }
    }
    val n = cleaned.length

    val cc = if (n >= 2 && cleaned[0] in 'A'..'Z' && cleaned[1] in 'A'..'Z') {
        cleaned.substring(0, 2)
    } else {
        null
    }
    val expected = cc?.let { IBAN_LENGTHS[it] }
    val formatted = formatGroups(cleaned)

    // Structure: 2 letters, 2 digits, then 1..30 alnum, total len 5..34.
    val structOk = n in 5..34
        && cleaned[0] in 'A'..'Z'
        && cleaned[1] in 'A'..'Z'
        && cleaned[2] in '0'..'9'
        && cleaned[3] in '0'..'9'
        && cleaned.substring(4).all { it in 'A'..'Z' || it in '0'..'9' }
    if (!structOk) {
        return IbanInfo(src, cleaned, cc, expectedLength = expected,
            formatted = formatted, error = "Invalid IBAN format.")
    }

    val checksumOk = mod97Check(cleaned)
    val lengthOk = expected == null || n == expected
    if (!lengthOk) {
        return IbanInfo(src, cleaned, cc, checksumOk = checksumOk,
            expectedLength = expected, formatted = formatted,
            error = "Length should be $expected for $cc.")
    }
    if (!checksumOk) {
        return IbanInfo(src, cleaned, cc, checksumOk = false, lengthOk = lengthOk,
            expectedLength = expected, formatted = formatted,
            error = "Checksum failed.")
    }
    return IbanInfo(src, cleaned, cc, valid = true, checksumOk = true,
        lengthOk = lengthOk, expectedLength = expected, formatted = formatted)
}

fun main() {
    // GB82 WEST 1234 5698 7654 32 — a known-good reference IBAN.
    val r = validateIban("GB82WEST12345698765432")
    println("valid=${r.valid} error=${r.error ?: "(none)"}")
}

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 →