IBAN Validator — Go 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 Go implementation — the same logic the interactive tool runs, in a shareable, citable form.
// Package ibanvalidator is the Go twin of CosmoDev's src/lib/iban.ts (dual
// source: the web lib is TypeScript, the CLI lib is Go — kept in lock-step).
// Pure + deterministic, never panics. The table-driven tests in
// iban-validator_test.go share vectors with src/lib/iban.test.ts so the two
// implementations are held to the same contract.
//
// The algorithm mirrors the TS lib exactly: normalise (uppercase, strip
// whitespace/dashes) → read the country code → look up the per-country length →
// verify the structure (2 letters, 2 check digits, 1–30 alphanumerics) → check
// the length → run the ISO 13616 mod-97 checksum (first 4 chars rotated to the
// end, letters → A=10…Z=35, running remainder mod 97 must end at 1).
package ibanvalidator
import (
"fmt"
"regexp"
"strings"
)
// IbanInfo is the structured verdict returned by ValidateIban. It is the Go twin
// of IbanInfo in src/lib/iban.ts. Nullable TS fields use the Go zero value as
// the "absent" sentinel, mirroring TS null (the same convention as
// emailvalidator.EmailResult):
// - CountryCode: "" means null (a real code is always 2 uppercase letters).
// - ExpectedLength: 0 means null (real ISO 13616 lengths are ≥ 15).
// - Error: "" means null.
type IbanInfo struct {
Input string
Cleaned string
CountryCode string
Valid bool
ChecksumOK bool
LengthOK bool
ExpectedLength int
Formatted string
Error string
}
// IbanLengths holds the per-country IBAN lengths (ISO 13616) — the same
// representative subset as IBAN_LENGTHS in src/lib/iban.ts.
var IbanLengths = map[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,
}
var (
// countryCodeRe matches a leading pair of uppercase letters — the country
// code. Mirrors /^[A-Z]{2}/ in the TS lib.
countryCodeRe = regexp.MustCompile(`^[A-Z]{2}`)
// formatRe is the structural check: 2 letters, 2 check digits, then 1–30
// alphanumerics. Mirrors /^[A-Z]{2}[0-9]{2}[A-Z0-9]{1,30}$/ in the TS lib.
formatRe = regexp.MustCompile(`^[A-Z]{2}[0-9]{2}[A-Z0-9]{1,30}$`)
// stripRe matches a single whitespace rune or dash, removed during
// normalisation. Mirrors /[\s-]/g in the TS lib.
stripRe = regexp.MustCompile(`[\s-]`)
)
// formatGroups inserts a space after every run of 4 characters, mirroring the
// TS lib's cleaned.replace(/(.{4})(?=.)/g, '$1 ').trim(). Go's RE2 has no
// lookahead, so the grouping is built directly; because a space is only ever
// inserted between groups, the result is already trimmed (TrimSpace is applied
// defensively to match the TS .trim() exactly).
func formatGroups(s string) string {
if s == "" {
return ""
}
runes := []rune(s)
var b strings.Builder
for i, r := range runes {
if i > 0 && i%4 == 0 {
b.WriteByte(' ')
}
b.WriteRune(r)
}
return strings.TrimSpace(b.String())
}
// Mod97Check runs the ISO 13616 mod-97 checksum over a CLEANED iban (uppercase,
// no spaces). It is the Go twin of mod97Check() in src/lib/iban.ts. The first
// 4 characters (country + check digits) are rotated to the end; letters become
// A=10…Z=35; the running remainder mod 97 must end at 1. Any character outside
// [0-9A-Z] (including lowercase or punctuation) makes it return false. It never
// panics: the per-digit modular reduction keeps the accumulator well below any
// integer limit, so no big.Int is needed (the TS lib's BigInt is cautionary).
func Mod97Check(cleaned string) bool {
var rearranged string
if len(cleaned) >= 4 {
rearranged = cleaned[4:] + cleaned[:4]
} else {
// JS slice(4) === "" and slice(0,4) === cleaned when fewer than 4 chars,
// so rearranged is just the original string.
rearranged = cleaned
}
numeric := make([]byte, 0, len(rearranged)*2)
for _, r := range rearranged {
switch {
case r >= '0' && r <= '9':
numeric = append(numeric, byte(r))
case r >= 'A' && r <= 'Z':
v := int(r - 55) // A=10 .. Z=35 (always two digits)
numeric = append(numeric, byte('0'+v/10), byte('0'+v%10))
default:
return false // invalid character (mirrors the TS else branch)
}
}
rem := 0
for _, ch := range numeric {
rem = (rem*10 + int(ch-'0')) % 97
}
return rem == 1
}
// ValidateIban validates an IBAN and returns the full verdict. It is the Go twin
// of validateIban() in src/lib/iban.ts and must agree with it on every shared
// vector. It never panics: the TS lib's nullish-input handling (input ?? "")
// maps to the empty-string path here, since Go has no null strings — ValidateIban
// ("") produces the same IbanInfo the TS lib yields for null/undefined.
func ValidateIban(input string) IbanInfo {
cleaned := stripRe.ReplaceAllString(strings.ToUpper(input), "")
cc := ""
if countryCodeRe.MatchString(cleaned) {
cc = cleaned[:2] // ^[A-Z]{2} guarantees len(cleaned) >= 2
}
var expected int
if cc != "" {
if v, ok := IbanLengths[cc]; ok {
expected = v
}
}
// base mirrors the TS `base` object: all verdict flags false, no error.
base := IbanInfo{
Input: input,
Cleaned: cleaned,
CountryCode: cc,
ExpectedLength: expected,
Formatted: formatGroups(cleaned),
}
if !formatRe.MatchString(cleaned) {
base.Error = "Invalid IBAN format."
return base
}
lengthOk := expected == 0 || len(cleaned) == expected // expected===null ? true : …
checksumOk := Mod97Check(cleaned)
if !lengthOk {
base.ChecksumOK = checksumOk
base.Error = fmt.Sprintf("Length should be %d for %s.", expected, cc)
return base
}
if !checksumOk {
base.LengthOK = true
base.Error = "Checksum failed."
return base
}
base.LengthOK = true
base.ChecksumOK = true
base.Valid = true
return base
}
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 →