IBAN Validator — Swift 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 Swift 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: Swift (Swift 5.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 big-integer
// machinery is required.
/// Per-country IBAN lengths (ISO 13616) — a representative subset.
let ibanLengths: [String: Int] = [
"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,
]
/// Result of validation — optionals mirror the TS nullable fields.
struct IbanInfo {
var input: String
var cleaned: String
var countryCode: String? // nil when the head is not 2 uppercase letters
var valid = false
var checksumOk = false
var lengthOk = false
var expectedLength: Int? // nil when the country is unknown
var formatted = ""
var error: String? // nil when valid
}
private func isUpperAlpha(_ c: Unicode.Scalar) -> Bool {
c.value >= 65 && c.value <= 90 // "A"..."Z"
}
private func isDigitScalar(_ c: Unicode.Scalar) -> Bool {
c.value >= 48 && c.value <= 57 // "0"..."9"
}
/// ISO 13616 mod-97 checksum over a CLEANED iban (uppercase, no spaces).
func mod97Check(_ cleaned: String) -> Bool {
let scalars = Array(cleaned.unicodeScalars)
guard scalars.count >= 4 else { return false }
// Rearranged = scalars[4...] ++ scalars[..<4] (country + check moved to end);
// index (i + 4) % count walks that rotation in place.
var rem = 0
for i in 0..<scalars.count {
let scalar = scalars[(i + 4) % scalars.count]
let value: Int
let digits: Int
if isDigitScalar(scalar) {
value = Int(scalar.value) - 48
digits = 1
} else if isUpperAlpha(scalar) {
value = Int(scalar.value) - 55 // 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 func formatGroups(_ cleaned: String) -> String {
var out = ""
for (i, scalar) in cleaned.unicodeScalars.enumerated() {
if i > 0 && i % 4 == 0 {
out.unicodeScalars.append(" ")
}
out.unicodeScalars.append(scalar)
}
return out
}
/// Validate an IBAN. Never traps — all failures are reported via `error`.
func validateIban(_ input: String) -> IbanInfo {
// Uppercase and strip whitespace/dashes.
var cleanedView = String.UnicodeScalarView()
for scalar in input.uppercased().unicodeScalars {
let v = scalar.value
if v == 32 || v == 45 || v == 9 || v == 10 || v == 13 {
continue // strip " ", "-", "\t", "\n", "\r"
}
cleanedView.append(scalar)
}
let cleaned = String(cleanedView)
let scalars = Array(cleaned.unicodeScalars)
var info = IbanInfo(input: input, cleaned: cleaned)
if scalars.count >= 2 && isUpperAlpha(scalars[0]) && isUpperAlpha(scalars[1]) {
info.countryCode = String(String.UnicodeScalarView(scalars[0...1]))
}
if let cc = info.countryCode {
info.expectedLength = ibanLengths[cc]
}
info.formatted = formatGroups(cleaned)
// Structure: 2 letters, 2 digits, then 1..30 alnum, total len 5..34.
let structOk = (5...34).contains(scalars.count)
&& isUpperAlpha(scalars[0]) && isUpperAlpha(scalars[1])
&& isDigitScalar(scalars[2]) && isDigitScalar(scalars[3])
&& scalars[4...].allSatisfy { isUpperAlpha($0) || isDigitScalar($0) }
if !structOk {
info.error = "Invalid IBAN format."
return info
}
info.checksumOk = mod97Check(cleaned)
info.lengthOk = info.expectedLength.map { scalars.count == $0 } ?? true
if !info.lengthOk {
info.error = "Length should be \(info.expectedLength!) for \(info.countryCode!)."
return info
}
if !info.checksumOk {
info.error = "Checksum failed."
return info
}
info.valid = true
return info
}
// GB82 WEST 1234 5698 7654 32 — a known-good reference IBAN.
let r = validateIban("GB82WEST12345698765432")
print("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 →