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 →