commit fd6b16355c3c14111a0c23688ee0c6806e51c3a7
parent e5135f1d692db6556e7a7223e84d0ac981f8eb2c
Author: Antoine A <>
Date: Fri, 31 Jan 2025 16:26:39 +0100
common: optimize IBAN checksum
Diffstat:
2 files changed, 55 insertions(+), 39 deletions(-)
diff --git a/common/src/main/kotlin/TalerCommon.kt b/common/src/main/kotlin/TalerCommon.kt
@@ -1,6 +1,6 @@
/*
* This file is part of LibEuFin.
- * Copyright (C) 2024 Taler Systems S.A.
+ * Copyright (C) 2024-2025 Taler Systems S.A.
*
* LibEuFin is free software; you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@@ -282,43 +282,6 @@ class TalerAmount {
}
}
-@JvmInline
-value class IBAN private constructor(val value: String) {
- override fun toString(): String = value
-
- companion object {
- private val SEPARATOR = Regex("[\\ \\-]")
-
- fun parse(raw: String): IBAN {
- val iban: String = raw.uppercase().replace(SEPARATOR, "")
- val builder = StringBuilder(iban.length + iban.asSequence().map { if (it.isDigit()) 1 else 2 }.sum())
- (iban.subSequence(4, iban.length).asSequence() + iban.subSequence(0, 4).asSequence()).forEach {
- if (it.isDigit()) {
- builder.append(it)
- } else {
- builder.append((it.code - 'A'.code) + 10)
- }
- }
- val str = builder.toString()
- val mod = str.toBigInteger().mod(97.toBigInteger()).toInt()
- if (mod != 1) throw CommonError.Payto("Iban malformed, modulo is $mod expected 1")
- return IBAN(iban)
- }
-
- fun rand(): IBAN {
- val ccNoCheck = "131400" // DE00
- val bban = (0..10).map {
- (0..9).random()
- }.joinToString("") // 10 digits account number
- var checkDigits: String = "98".toBigInteger().minus("$bban$ccNoCheck".toBigInteger().mod("97".toBigInteger())).toString()
- if (checkDigits.length == 1) {
- checkDigits = "0${checkDigits}"
- }
- return IBAN("DE$checkDigits$bban")
- }
- }
-}
-
@Serializable(with = Payto.Serializer::class)
sealed class Payto {
abstract val parsed: URI
diff --git a/common/src/main/kotlin/iban.kt b/common/src/main/kotlin/iban.kt
@@ -1,6 +1,6 @@
/*
* This file is part of LibEuFin.
- * Copyright (C) 2024 Taler Systems S.A.
+ * Copyright (C) 2024-2025 Taler Systems S.A.
* LibEuFin is free software; you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@@ -31,4 +31,57 @@ private val ibanRegex = Regex("^[A-Z]{2}[0-9]{2}[a-zA-Z0-9]{1,30}$")
fun validateIban(iban: String): Boolean {
return ibanRegex.matches(iban)
+}
+
+
+@JvmInline
+value class IBAN private constructor(val value: String) {
+ override fun toString(): String = value
+
+ companion object {
+ private val SEPARATOR = Regex("[\\ \\-]")
+
+ fun checksum(iban: String): Int =
+ (iban.substring(4 until iban.length).asSequence() + iban.substring(0 until 4).asSequence())
+ .fold(0) { acc, char ->
+ var checksum = if (char.isDigit()) {
+ acc * 10 + char.code - '0'.code
+ } else {
+ acc * 100 + char.code - 'A'.code + 10
+ }
+ if (checksum > 9_999_999) {
+ checksum %= 97
+ }
+ checksum
+ } % 97
+
+
+ fun parse(raw: String): IBAN {
+ val iban: String = raw.uppercase().replace(SEPARATOR, "")
+ if (iban.length < 5) {
+ throw CommonError.Payto("malformed IBAN, string is too small only ${iban.length} char")
+ }
+ for (c in iban.substring(0 until 2)) {
+ if (!c.isLetter()) throw CommonError.Payto("malformed IBAN, malformed country code")
+ }
+ for (c in iban.substring(2 until 4)) {
+ if (!c.isDigit()) throw CommonError.Payto("malformed IBAN, malformed check digit")
+ }
+ val checksum = checksum(iban)
+ if (checksum != 1) throw CommonError.Payto("malformed IBAN, modulo is $checksum expected 1")
+ return IBAN(iban)
+ }
+
+ fun rand(): IBAN {
+ val bban = (0..10).map {
+ (0..9).random()
+ }.joinToString("") // 10 digits account number
+ val checkDigits = 98 - checksum("DE00$bban");
+ return if (checkDigits < 10) {
+ IBAN("DE0$checkDigits$bban")
+ } else {
+ IBAN("DE$checkDigits$bban")
+ }
+ }
+ }
}
\ No newline at end of file