Skip to content

IBAN Validator — Ruby 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 Ruby 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: Ruby (Ruby 3.2, 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
# structural regex (country code + 2 check digits + 1-30 BBAN chars),
# verifies the per-country length, and 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. Ruby ints are
# arbitrary precision, but the remainder is folded one digit at a time
# (it stays under 97) to mirror the reference algorithm exactly.
# validate_iban never raises — it reports failures through `error`.

# Per-country IBAN lengths (ISO 13616) — a representative subset.
IBAN_LENGTHS = {
  '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,
}.freeze

STRUCTURE = /\A[A-Z]{2}[0-9]{2}[A-Z0-9]{1,30}\z/.freeze
CC_HEAD   = /\A[A-Z]{2}/.freeze

# Result of validation. nil fields mirror the TS nullable fields.
IbanInfo = Struct.new(
  :input, :cleaned, :country_code, :valid, :checksum_ok, :length_ok,
  :expected_length, :formatted, :error,
  keyword_init: true
)

# ISO 13616 mod-97 checksum over a CLEANED iban (uppercase, no spaces).
def mod97_check(cleaned)
  return false if cleaned.length < 4

  # Move the first 4 chars (country + check) to the end.
  rearranged = cleaned[4..] + cleaned[...4]
  numeric = +''
  rearranged.each_char do |ch|
    case ch
    when '0'..'9' then numeric << ch
    when 'A'..'Z' then numeric << (ch.ord - 55).to_s # A=10 .. Z=35
    else return false # invalid character
    end
  end
  rem = 0
  numeric.each_char { |d| rem = (rem * 10 + d.ord - 48) % 97 }
  rem == 1
end

# Validate an IBAN. Always returns IbanInfo; never raises.
def validate_iban(input)
  src = input.to_s
  cleaned = src.upcase.gsub(/[\s-]/, '')
  cc = cleaned[CC_HEAD]
  expected = cc && IBAN_LENGTHS[cc]

  info = IbanInfo.new(
    input: src,
    cleaned: cleaned,
    country_code: cc,
    expected_length: expected,
    # Insert a space every 4 chars (last group may be short).
    formatted: cleaned.gsub(/(.{4})(?=.)/, '\1 ').strip
  )

  unless cleaned =~ STRUCTURE
    info.error = 'Invalid IBAN format.'
    return info
  end

  info.checksum_ok = mod97_check(cleaned)
  info.length_ok = expected.nil? || cleaned.length == expected
  if info.length_ok && info.checksum_ok
    info.valid = true
    return info
  end

  unless info.length_ok
    info.error = "Length should be #{expected} for #{cc}."
    return info
  end
  info.error = 'Checksum failed.'
  info
end

if __FILE__ == $PROGRAM_NAME
  # GB82 WEST 1234 5698 7654 32 — a known-good reference IBAN.
  r = validate_iban('GB82WEST12345698765432')
  puts "valid=#{r.valid} error=#{r.error || '(none)'}"
end

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 →