Skip to content

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 →