kiban
0.3.0indexedOffers IBAN validation, formatting, and retrieval of country-specific details, with immutable objects, non-empty valid IBANs, and SEPA/SWIFT registry checks. Supports multi-platform environments.
Offers IBAN validation, formatting, and retrieval of country-specific details, with immutable objects, non-empty valid IBANs, and SEPA/SWIFT registry checks. Supports multi-platform environments.
This Kotlin Multiplatform library is a continuation and re-implementation of the original java-iban library by Barend Garvelink. It delivers IBAN validation, formatting, and country-specific IBAN details. The library is aimed to fulfill the same features as the original but in a Kotlin Multiplatform environment.
⚠ Important Note: The API of this library is still evolving and not yet stable. Expect breaking changes until the API stabilizes in a future release.
The original java-iban library laid a solid foundation for IBAN validation and utility functions in Java environments. This library reimagines those capabilities with Kotlin's cross-platform features, making it ready for use on multiple platforms such as JVM, Android, iOS, and more.
Artifacts are published to Maven Central.
dependencies {
implementation("nl.bijdorpstudio.kiban:kiban:0.5.0")
}
In a multiplatform project, add it to commonMain:
kotlin {
sourceSets {
commonMain.dependencies {
implementation("nl.bijdorpstudio.kiban:kiban:0.5.0")
}
}
}
Supported targets: JVM, Android, js (Node.js and browser), wasmJs (Node.js and browser), iOS, macOS, watchOS, tvOS, linuxX64, linuxArm64, and mingwX64.
Parsing is strict: invalid input throws a typed IbanParseException, so you don't need to unwrap a
Result for the common case. The exception-free toIbanOrNull() and isValidIban() are there for
when you want to check input without paying for a stack trace.
Input must be ASCII. ISO 13616 defines the IBAN character set as A-Z0-9, so upper case ASCII
letters, ASCII digits and (ASCII 0x20) spaces are all that parse; non-ASCII look-alikes such as
fullwidth 9 or Arabic-Indic ٩ digits are rejected rather than normalized, because silently
rewriting a bank account identifier hides upstream data corruption. NFKC-normalize user input before
parsing if your input layer can produce them.
The whitespace leniency is just as narrow: the (ASCII 0x20) space is ignored between the first and
last character, so both "NL91ABNA0417164300" and "NL91 ABNA 0417 1643 00" parse, but a leading
or trailing space is rejected and so is any other whitespace anywhere — a tab or a non-breaking
space mid-IBAN is a paste artifact, not grouping, and is reported as an invalid character. Trim
before parsing if your input layer can produce them.
Modulo97 is the one part of the library that still throws unconditionally: its inputs are
programmer-supplied, so a bad one is a contract violation rather than user input to be validated.
Iban's throwing entry points (Iban(...), Iban.compose(...), String.toIban()) are annotated
, so Kotlin/Native's Objective-C exporter emits an
out-parameter and Swift sees a normal function instead of the process aborting on an
unannotated exception.
Migrating from java-iban, from kiban 0.3.0 and earlier, or from the Result-returning 0.4.0 API?
See MIGRATION.md.
Every example above is walked through by samples/jvm-cli, a runnable demo
of the API; see samples/ for that and a Swift consumer exercising the library
through Kotlin/Native's Objective-C interop — see
docs/9-swift-interop-review.md for the review that found the
0.4.0 Result-returning API didn't survive the trip to Swift, which is what this strict,
-annotated API is meant to fix.
I (Barend) like the Joda-Time library, and I try to follow the same design principles. I'm explicitly targetting Android, which at the time this library started was still on Java 1.6. I'm trying to keep the library as simple as I can.
Adopted design choices from the Java library, plus:
IbanParseException on invalid input, rather than returning a Result. The exception type extends IllegalArgumentException, and callers who want typed errors can catch it and inspect the failure instead of matching on messages. Every throwing entry point carries , which is load-bearing for Kotlin/Native's Objective-C interop: an exception escaping an unannotated function aborts the process there, rather than surfacing as a catchable Swift error.The embedded country data (CountryCodesData.kt) and the country test data table are generated from the SWIFT IBAN Registry TXT. The registry TXT is not redistributable and is never committed: it lives only in the gitignored scripts/input/.
# Download "IBAN Registry (TXT)" in a browser from https://www.swift.com/standards/data-standards/iban,
# or try the scripted download. Headless Chromium is blocked by Swift's bot detection from at least
# some networks (which is why CI runs headed under Xvfb), so reach for --headed when this times out:
kotlin scripts/fetch_registry.main.kts --out scripts/input/iban-registry.txt
kotlin scripts/generate_country_data.main.kts --registry scripts/input/iban-registry.txt --rev <revision>
./gradlew :library:ktfmtFormatKmpCommonMain :library:ktfmtFormatKmpCommonTest
The registry's release number has to be supplied by hand: the download endpoint sends no filename and the registry page states no release, so neither script can detect it. Read it off the registry PDF and pass it as --rev.
The generator validates every entry before writing (mod-97 checksum, declared length and country prefix, and bank/branch identifier positions cross-checked against the registry's own identifier examples), so a malformed or truncated download fails loudly instead of landing in the library.
As this is still an evolving library with an unstable API, contributions are welcome! Join the development journey and help shape a modern, multiplatform IBAN utility library.
This project follows the same licensing model as the original library and is licensed under the Apache License 2.0.
// The primary entry point. Throws IbanParseException on invalid input.
val iban: Iban = Iban( "NL91ABNA0417164300" )
// Or use the String extension; same throwing behaviour.
val parsed: Iban = "NL91ABNA0417164300".toIban()
// Exception-free fast paths.
val orNull: Iban? = "NL91ABNA0417164301".toIbanOrNull() // null, check digits are wrong
val isValid: Boolean = "NL91ABNA0417164300".isValidIban() // true
// Failures carry a typed reason, so you never have to match on messages.
try {
Iban( input )
} catch ( failure: IbanParseException ) {
when ( failure ) {
is IbanParseException.UnknownCountryCode -> reportUnknown( failure.countryCode )
is IbanParseException.WrongLength -> reportLength( failure.expectedLength, failure.actualLength )
is IbanParseException.WrongChecksum -> reportChecksum()
is IbanParseException.Malformed -> reportMalformed( failure.kind )
}
}
// toString() emits standard formatting, plain is compact.
val formatted = iban.toString() // "NL91 ABNA 0417 1643 00"
val plain = iban.plain // "NL91ABNA0417164300"
// Input may be formatted.
val anotherIban = Iban( "BE68 5390 0754 7034" )
// Iban implements Comparable<T>.
val ibans = getListOfIBANs()
ibans.sorted() // sorts in lexical order
// The equals() and hashCode() methods are implemented.
val ibansAsKeys = mutableMapOf<Iban, String>()
ibansAsKeys.put( iban, "this is fine" )
// You can use the Modulo97 class directly to compute or verify the check digits on an input.
val candidate = "GB29 NWBK 6016 1331 9268 19"
val valid = Modulo97.verifyCheckDigits( candidate ) // true
// Compose the IBAN for a country and BBAN; also throws on invalid input.
Iban.compose( "BI", "10000100010000332045181" ) // BI4210000100010000332045181
// You can query whether an IBAN is of a SEPA-participating country
val isSepa = Iban( candidate ).isSEPA // true
// You can query whether an IBAN is in the SWIFT Registry
val isRegistered = Iban( candidate ).isInSwiftRegistry // true
// Modulo97 API methods take CharSequence, not just String.
val builder = StringBuilder( "LU000019400644750000" )
val checkDigits = Modulo97.calculateCheckDigits( builder ) // 28
// Modulo97 API can calculate check digits, also for non-iban inputs.
// It does assume/require that the check digits are on indices 2 and 3.
Modulo97.calculateCheckDigits( "GB", "NWBK60161331926819" ) // 29
Modulo97.calculateCheckDigits( "XX", "X" ) // 72
// Get the expected IBAN length for a country code:
val expectedLength: Int? = CountryCodes.getLength( "DK" ) // 18
// Get the Bank Identifier and Branch Identifier:
val bankId: String? = iban.bankIdentifier
val branchId: String? = iban.branchIdentifier
@Throws(IbanParseException::class)NSError**throws@ThrowsIban objects are immutable, and the Iban therein is non-empty and valid. There is no support for partial or invalid IBANs. Note that "valid" isn't as strict as it could be:
QA2!n4!a21!c) is not enforced. This seems to me like more work than necessary. The modulo-97 checksum catches most input errors anyway, and I don't want to force a memory-hungry regex check onto Android users. Speaking of Android, this mask could be used for keyboard switching on an Iban EditText, but that's for a different open-source project.Iban.parse() method. This, to me, would look too much like Joda-Time's pluggable Chronology system, which leads to PoLS violations (background: Why JSR-310 isn't Joda-Time).Iban class. Currently, that's the support for extracting Bank and Branch identifiers, which lives in the CountryCode class.@Throws(IbanParseException::class)Modulo97 keeps throwing: it is a low-level utility whose errors indicate a contract violation, not invalid user input.Surfaced from shared tags and platforms — no rankings paid for.