IBAN Validator — Java 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 Java 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: Java (Java 17, 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;
import java.util.Locale;
import java.util.Map;
/** Pure IBAN validation logic — ISO 13616 mod-97 checksum. */
public final class IbanValidator {
/** Per-country IBAN lengths (ISO 13616) — a representative subset. */
private static final Map<String, Integer> IBAN_LENGTHS = Map.ofEntries(
Map.entry("AL", 28), Map.entry("AD", 24), Map.entry("AT", 20),
Map.entry("AZ", 28), Map.entry("BH", 22), Map.entry("BY", 28),
Map.entry("BE", 16), Map.entry("BA", 20), Map.entry("BR", 29),
Map.entry("BG", 22), Map.entry("CR", 22), Map.entry("HR", 21),
Map.entry("CY", 28), Map.entry("CZ", 24), Map.entry("DK", 18),
Map.entry("DO", 28), Map.entry("EE", 20), Map.entry("FO", 18),
Map.entry("FI", 18), Map.entry("FR", 27), Map.entry("GE", 22),
Map.entry("DE", 22), Map.entry("GI", 23), Map.entry("GR", 27),
Map.entry("GL", 18), Map.entry("GT", 28), Map.entry("HU", 28),
Map.entry("IS", 26), Map.entry("IE", 22), Map.entry("IL", 23),
Map.entry("IT", 27), Map.entry("JO", 30), Map.entry("KZ", 20),
Map.entry("XK", 20), Map.entry("KW", 30), Map.entry("LV", 21),
Map.entry("LB", 28), Map.entry("LI", 21), Map.entry("LT", 20),
Map.entry("LU", 20), Map.entry("MK", 19), Map.entry("MT", 31),
Map.entry("MR", 27), Map.entry("MU", 30), Map.entry("MC", 27),
Map.entry("MD", 24), Map.entry("ME", 22), Map.entry("NL", 18),
Map.entry("NO", 15), Map.entry("PK", 24), Map.entry("PS", 29),
Map.entry("PL", 28), Map.entry("PT", 25), Map.entry("QA", 29),
Map.entry("RO", 24), Map.entry("LC", 32), Map.entry("SM", 27),
Map.entry("ST", 25), Map.entry("SA", 24), Map.entry("RS", 22),
Map.entry("SC", 31), Map.entry("SK", 24), Map.entry("SI", 19),
Map.entry("SG", 19), Map.entry("ES", 24), Map.entry("SE", 24),
Map.entry("CH", 21), Map.entry("TL", 23), Map.entry("TN", 24),
Map.entry("TR", 26), Map.entry("UA", 29), Map.entry("AE", 23),
Map.entry("GB", 22), Map.entry("VG", 24));
/** Result of validation — nullable fields mirror the TS nullable fields. */
public record IbanInfo(
String input,
String cleaned,
String countryCode, // null when the head is not 2 uppercase letters
boolean valid,
boolean checksumOk,
boolean lengthOk,
Integer expectedLength, // null when the country is unknown
String formatted,
String error) { // null when valid
}
private IbanValidator() {
}
/** ISO 13616 mod-97 checksum over a CLEANED iban (uppercase, no spaces). */
public static boolean mod97Check(String cleaned) {
int n = cleaned.length();
if (n < 4) {
return false;
}
// Move the first 4 chars (country + check) to the end.
String rearranged = cleaned.substring(4) + cleaned.substring(0, 4);
int rem = 0;
for (int i = 0; i < rearranged.length(); i++) {
char ch = rearranged.charAt(i);
int value;
int digits;
if (ch >= '0' && ch <= '9') {
value = ch - '0';
digits = 1;
} else if (ch >= 'A' && ch <= '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 static String formatGroups(String cleaned) {
StringBuilder out = new StringBuilder(cleaned.length() + cleaned.length() / 4);
for (int i = 0; i < cleaned.length(); i++) {
if (i > 0 && i % 4 == 0) {
out.append(' ');
}
out.append(cleaned.charAt(i));
}
return out.toString();
}
private static boolean isUpperAlpha(char c) {
return c >= 'A' && c <= 'Z';
}
private static boolean isDigitChar(char c) {
return c >= '0' && c <= '9';
}
/** Validate an IBAN. Never throws — all failures are reported via {@code error}. */
public static IbanInfo validateIban(String input) {
String src = (input == null) ? "" : input;
// Uppercase and strip whitespace/dashes.
StringBuilder cleanedSb = new StringBuilder();
for (char ch : src.toUpperCase(Locale.ROOT).toCharArray()) {
if (ch == ' ' || ch == '-' || ch == '\t' || ch == '\n' || ch == '\r') {
continue;
}
cleanedSb.append(ch);
}
String cleaned = cleanedSb.toString();
int n = cleaned.length();
String cc = n >= 2 && isUpperAlpha(cleaned.charAt(0)) && isUpperAlpha(cleaned.charAt(1))
? cleaned.substring(0, 2)
: null;
Integer expected = (cc == null) ? null : IBAN_LENGTHS.get(cc);
String formatted = formatGroups(cleaned);
// Structure: 2 letters, 2 digits, then 1..30 alnum, total len 5..34.
boolean structOk = n >= 5 && n <= 34
&& isUpperAlpha(cleaned.charAt(0))
&& isUpperAlpha(cleaned.charAt(1))
&& isDigitChar(cleaned.charAt(2))
&& isDigitChar(cleaned.charAt(3));
if (structOk) {
for (int i = 4; i < n; i++) {
char ch = cleaned.charAt(i);
if (!isUpperAlpha(ch) && !isDigitChar(ch)) {
structOk = false;
break;
}
}
}
if (!structOk) {
return new IbanInfo(src, cleaned, cc, false, false, false, expected,
formatted, "Invalid IBAN format.");
}
boolean checksumOk = mod97Check(cleaned);
boolean lengthOk = expected == null || n == expected;
if (!lengthOk) {
return new IbanInfo(src, cleaned, cc, false, checksumOk, false, expected,
formatted, "Length should be " + expected + " for " + cc + ".");
}
if (!checksumOk) {
return new IbanInfo(src, cleaned, cc, false, false, lengthOk, expected,
formatted, "Checksum failed.");
}
return new IbanInfo(src, cleaned, cc, true, true, lengthOk, expected,
formatted, null);
}
public static void main(String[] args) {
// GB82 WEST 1234 5698 7654 32 — a known-good reference IBAN.
IbanInfo r = validateIban("GB82WEST12345698765432");
System.out.println("valid=" + r.valid()
+ " error=" + (r.error() == null ? "(none)" : r.error()));
}
}
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 →