From ce8a94c3e82a221e938b96594c1e8858f0b7c012 Mon Sep 17 00:00:00 2001 From: Dominic Baur Date: Wed, 27 Aug 2025 15:27:01 +0200 Subject: [PATCH 01/11] Add utility function isValidSwissIbanNumber --- CHANGELOG.md | 4 ++++ src/lib/string.spec.ts | 14 +++++++++++++- src/lib/string.ts | 30 ++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c15e1c6..e0dfef1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- `isValidSwissIbanNumber` string utility function + ## [2.0.0] - 2025-07-29 ### Added diff --git a/src/lib/string.spec.ts b/src/lib/string.spec.ts index 94fc71e..8bab2ea 100644 --- a/src/lib/string.spec.ts +++ b/src/lib/string.spec.ts @@ -1,4 +1,4 @@ -import { isNullOrEmpty, isNullOrWhitespace, capitalize, uncapitalize, truncate } from "./string"; +import { isNullOrEmpty, isNullOrWhitespace, capitalize, uncapitalize, truncate, isValidSwissIbanNumber } from "./string"; describe("string tests", () => { test.each([ @@ -120,4 +120,16 @@ describe("string tests", () => { ])("truncate without suffix parameter", (value, maxLength, expected) => { expect(truncate(value, maxLength)).toBe(expected); }); + + test.each([ + [null as unknown as string, false], + [undefined as unknown as string, false], + ["CH9300762011623852957", true], + ["CH93 0000 0000 0000 0000 1", false], + ["ch93 0076 2011 6238 5295 7", false], + ["DE93 0076 2011 6238 5295 7", false], + ["CH93 0076 2011 6238 5295 7", true], + ])("Is IBAN valid", (unformattedIbanNumber, expected) => { + expect(isValidSwissIbanNumber(unformattedIbanNumber)).toBe(expected); + }); }); diff --git a/src/lib/string.ts b/src/lib/string.ts index c0666b3..28d9d74 100644 --- a/src/lib/string.ts +++ b/src/lib/string.ts @@ -64,3 +64,33 @@ export function truncate(value: string | undefined, maxLength: number, suffix = return `${value.slice(0, maxLength)}${suffix}`; } + +/** + * Checks if the provided string is a valid swiss IBAN number + * @param iBanNumber The provided IBAN number to check + * @returns The result of the IBAN number check + */ +export function isValidSwissIbanNumber(iBanNumber: string): boolean { + if (!isNullOrEmpty(iBanNumber)) { + const compactIban = iBanNumber.replaceAll(/\s+/g, ""); + if (!/^CH\d{19}$/.test(compactIban)) return false; + const rearrangedIban = compactIban.slice(4) + compactIban.slice(0, 4).toString(); + + const numericStr = Array.from(rearrangedIban, (ch) => { + if (/[A-Z]/.test(ch)) { + const code = ch.codePointAt(0); + // code is never undefined! + return (code! - 55).toString(); + } + return ch; + }).join(""); + + let restOfCalculation = 0; + for (const digit of numericStr) { + restOfCalculation = (restOfCalculation * 10 + Number(digit)) % 97; + } + + return restOfCalculation === 1; + } + return false; +} From 1114fda81b67438c35730b9fc5b4727666323aa0 Mon Sep 17 00:00:00 2001 From: "Sandro C." Date: Wed, 27 Aug 2025 16:05:16 +0200 Subject: [PATCH 02/11] Update src/lib/string.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/lib/string.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/string.ts b/src/lib/string.ts index 28d9d74..5538350 100644 --- a/src/lib/string.ts +++ b/src/lib/string.ts @@ -66,7 +66,7 @@ export function truncate(value: string | undefined, maxLength: number, suffix = } /** - * Checks if the provided string is a valid swiss IBAN number + * Checks if the provided string is a valid Swiss IBAN number * @param iBanNumber The provided IBAN number to check * @returns The result of the IBAN number check */ From bba833b0f68bbdf8eaebfab39910e991b06ce4c5 Mon Sep 17 00:00:00 2001 From: "Sandro C." Date: Wed, 27 Aug 2025 16:05:41 +0200 Subject: [PATCH 03/11] Update src/lib/string.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/lib/string.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/string.ts b/src/lib/string.ts index 5538350..9c1aa8f 100644 --- a/src/lib/string.ts +++ b/src/lib/string.ts @@ -74,7 +74,7 @@ export function isValidSwissIbanNumber(iBanNumber: string): boolean { if (!isNullOrEmpty(iBanNumber)) { const compactIban = iBanNumber.replaceAll(/\s+/g, ""); if (!/^CH\d{19}$/.test(compactIban)) return false; - const rearrangedIban = compactIban.slice(4) + compactIban.slice(0, 4).toString(); + const rearrangedIban = compactIban.slice(4) + compactIban.slice(0, 4); const numericStr = Array.from(rearrangedIban, (ch) => { if (/[A-Z]/.test(ch)) { From 2e788ea9d4b64da00c15e22c038acc43d44e3439 Mon Sep 17 00:00:00 2001 From: Dominic Baur Date: Wed, 27 Aug 2025 16:11:34 +0200 Subject: [PATCH 04/11] Changed test name --- src/lib/string.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/string.spec.ts b/src/lib/string.spec.ts index 8bab2ea..e4e4d67 100644 --- a/src/lib/string.spec.ts +++ b/src/lib/string.spec.ts @@ -129,7 +129,7 @@ describe("string tests", () => { ["ch93 0076 2011 6238 5295 7", false], ["DE93 0076 2011 6238 5295 7", false], ["CH93 0076 2011 6238 5295 7", true], - ])("Is IBAN valid", (unformattedIbanNumber, expected) => { + ])("Is Swiss IBAN valid", (unformattedIbanNumber, expected) => { expect(isValidSwissIbanNumber(unformattedIbanNumber)).toBe(expected); }); }); From 7afe6cf6e97cf1c913bd5cdbae3f3af5410874a1 Mon Sep 17 00:00:00 2001 From: Dominic Baur Date: Wed, 27 Aug 2025 16:27:13 +0200 Subject: [PATCH 05/11] Fix --- src/lib/string.ts | 39 ++++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/src/lib/string.ts b/src/lib/string.ts index 9c1aa8f..00eb0c4 100644 --- a/src/lib/string.ts +++ b/src/lib/string.ts @@ -71,26 +71,27 @@ export function truncate(value: string | undefined, maxLength: number, suffix = * @returns The result of the IBAN number check */ export function isValidSwissIbanNumber(iBanNumber: string): boolean { - if (!isNullOrEmpty(iBanNumber)) { - const compactIban = iBanNumber.replaceAll(/\s+/g, ""); - if (!/^CH\d{19}$/.test(compactIban)) return false; - const rearrangedIban = compactIban.slice(4) + compactIban.slice(0, 4); - - const numericStr = Array.from(rearrangedIban, (ch) => { - if (/[A-Z]/.test(ch)) { - const code = ch.codePointAt(0); - // code is never undefined! - return (code! - 55).toString(); - } - return ch; - }).join(""); - - let restOfCalculation = 0; - for (const digit of numericStr) { - restOfCalculation = (restOfCalculation * 10 + Number(digit)) % 97; + if (isNullOrEmpty(iBanNumber)) { + return false; + } + const compactIban = iBanNumber.replaceAll(/\s+/g, ""); + if (!/^CH\d{19}$/.test(compactIban)) { + return false; + } + const rearrangedIban = compactIban.slice(4) + compactIban.slice(0, 4); + const numericStr = Array.from(rearrangedIban, (ch) => { + if (/[A-Z]/.test(ch)) { + const code = ch.codePointAt(0); + // code is never undefined! + return (code! - 55).toString(); } + return ch; + }).join(""); - return restOfCalculation === 1; + let restOfCalculation = 0; + for (const digit of numericStr) { + restOfCalculation = (restOfCalculation * 10 + Number(digit)) % 97; } - return false; + + return restOfCalculation === 1; } From 86335efb1324176f5446e906d781e3e44ce1b86f Mon Sep 17 00:00:00 2001 From: Dominic Baur Date: Thu, 28 Aug 2025 07:48:53 +0200 Subject: [PATCH 06/11] Changed parameter name and added is null or whitespace check --- src/lib/string.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lib/string.ts b/src/lib/string.ts index 00eb0c4..3aa0315 100644 --- a/src/lib/string.ts +++ b/src/lib/string.ts @@ -67,14 +67,14 @@ export function truncate(value: string | undefined, maxLength: number, suffix = /** * Checks if the provided string is a valid Swiss IBAN number - * @param iBanNumber The provided IBAN number to check + * @param ibanNumber The provided IBAN number to check * @returns The result of the IBAN number check */ -export function isValidSwissIbanNumber(iBanNumber: string): boolean { - if (isNullOrEmpty(iBanNumber)) { +export function isValidSwissIbanNumber(ibanNumber: string): boolean { + if (isNullOrWhitespace(ibanNumber)) { return false; } - const compactIban = iBanNumber.replaceAll(/\s+/g, ""); + const compactIban = ibanNumber.replaceAll(/\s+/g, ""); if (!/^CH\d{19}$/.test(compactIban)) { return false; } From 2c78286fe47132a06b20ee9017c3b60526aa08db Mon Sep 17 00:00:00 2001 From: Dominic Baur Date: Thu, 28 Aug 2025 09:03:44 +0200 Subject: [PATCH 07/11] Add the swiss IBAN to check now must be in one of the two defined formats --- src/lib/string.spec.ts | 5 +++-- src/lib/string.ts | 25 +++++++++++++------------ 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/lib/string.spec.ts b/src/lib/string.spec.ts index e4e4d67..7fb2273 100644 --- a/src/lib/string.spec.ts +++ b/src/lib/string.spec.ts @@ -125,11 +125,12 @@ describe("string tests", () => { [null as unknown as string, false], [undefined as unknown as string, false], ["CH9300762011623852957", true], + ["CH93 0076 2011 6238 5295 7", true], + ["CH93-0076-2011-6238-5295-7", false], ["CH93 0000 0000 0000 0000 1", false], ["ch93 0076 2011 6238 5295 7", false], ["DE93 0076 2011 6238 5295 7", false], - ["CH93 0076 2011 6238 5295 7", true], - ])("Is Swiss IBAN valid", (unformattedIbanNumber, expected) => { + ])("check if this swiss IBAN is valid or not", (unformattedIbanNumber, expected) => { expect(isValidSwissIbanNumber(unformattedIbanNumber)).toBe(expected); }); }); diff --git a/src/lib/string.ts b/src/lib/string.ts index 3aa0315..0a3352f 100644 --- a/src/lib/string.ts +++ b/src/lib/string.ts @@ -66,27 +66,28 @@ export function truncate(value: string | undefined, maxLength: number, suffix = } /** - * Checks if the provided string is a valid Swiss IBAN number + * Checks if the provided string is a valid swiss IBAN number * @param ibanNumber The provided IBAN number to check + * Must be in one of the following formats: + * - "CHXX XXXX XXXX XXXX XXXX X" with whitespaces + * - "CHXXXXXXXXXXXXXXXXXXX" without whitespaces * @returns The result of the IBAN number check */ export function isValidSwissIbanNumber(ibanNumber: string): boolean { if (isNullOrWhitespace(ibanNumber)) { return false; } - const compactIban = ibanNumber.replaceAll(/\s+/g, ""); - if (!/^CH\d{19}$/.test(compactIban)) { + + const compactIbanNumberWithWhiteSpaces = new RegExp(/^CH\d{2}(?:\s?\d{4}){4}\s?\d{1}$/); + const compactIbanNumberWithoutWhiteSpaces = new RegExp(/^CH\d{19}$/); + + if (!compactIbanNumberWithWhiteSpaces.test(ibanNumber) && !compactIbanNumberWithoutWhiteSpaces.test(ibanNumber)) { return false; } - const rearrangedIban = compactIban.slice(4) + compactIban.slice(0, 4); - const numericStr = Array.from(rearrangedIban, (ch) => { - if (/[A-Z]/.test(ch)) { - const code = ch.codePointAt(0); - // code is never undefined! - return (code! - 55).toString(); - } - return ch; - }).join(""); + + const compactInsuranceNumber = ibanNumber.replaceAll(/[\s.]+/g, ""); + const rearrangedIban = compactInsuranceNumber.slice(4) + compactInsuranceNumber.slice(0, 4); + const numericStr = rearrangedIban.replaceAll(/[A-Z]/g, (ch) => (ch.codePointAt(0)! - 55).toString()); let restOfCalculation = 0; for (const digit of numericStr) { From 5054683531ec0f9df658195fa82cf4e0dde6da4f Mon Sep 17 00:00:00 2001 From: Dominic Baur Date: Thu, 28 Aug 2025 14:06:41 +0200 Subject: [PATCH 08/11] Removed double check --- src/lib/string.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/lib/string.ts b/src/lib/string.ts index 0a3352f..e8a8eb5 100644 --- a/src/lib/string.ts +++ b/src/lib/string.ts @@ -79,14 +79,13 @@ export function isValidSwissIbanNumber(ibanNumber: string): boolean { } const compactIbanNumberWithWhiteSpaces = new RegExp(/^CH\d{2}(?:\s?\d{4}){4}\s?\d{1}$/); - const compactIbanNumberWithoutWhiteSpaces = new RegExp(/^CH\d{19}$/); - if (!compactIbanNumberWithWhiteSpaces.test(ibanNumber) && !compactIbanNumberWithoutWhiteSpaces.test(ibanNumber)) { + if (!compactIbanNumberWithWhiteSpaces.test(ibanNumber)) { return false; } - const compactInsuranceNumber = ibanNumber.replaceAll(/[\s.]+/g, ""); - const rearrangedIban = compactInsuranceNumber.slice(4) + compactInsuranceNumber.slice(0, 4); + const compactIbanNumber = ibanNumber.replaceAll(" ", ""); + const rearrangedIban = compactIbanNumber.slice(4) + compactIbanNumber.slice(0, 4); const numericStr = rearrangedIban.replaceAll(/[A-Z]/g, (ch) => (ch.codePointAt(0)! - 55).toString()); let restOfCalculation = 0; From 7da0f6dbfb7ee315213d191298074051d2317c43 Mon Sep 17 00:00:00 2001 From: Dominic Baur Date: Thu, 28 Aug 2025 15:22:14 +0200 Subject: [PATCH 09/11] Add explanation --- src/lib/string.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/lib/string.ts b/src/lib/string.ts index e8a8eb5..6729831 100644 --- a/src/lib/string.ts +++ b/src/lib/string.ts @@ -84,6 +84,19 @@ export function isValidSwissIbanNumber(ibanNumber: string): boolean { return false; } + /** + * Validates a Swiss IBAN number. + * + * Steps: + * - The number must start with `CH`, be 21 digits long and follow one of the accepted formats: + * - `CHXX XXXX XXXX XXXX XXXX X`or `CHXXXXXXXXXXXXXXXXXXX`. + * - Remove all whitespaces. + * - Rearrange by moving the first 4 characters (CH + 2 check digits) to the end. + * - Replace letters with numbers: A=10, B=11, ..., Z=35. + * - Convert the resulting string to a large integer and calculate modulo 97. + * - The number is valid if the resto of the calculation equals 1. + */ + const compactIbanNumber = ibanNumber.replaceAll(" ", ""); const rearrangedIban = compactIbanNumber.slice(4) + compactIbanNumber.slice(0, 4); const numericStr = rearrangedIban.replaceAll(/[A-Z]/g, (ch) => (ch.codePointAt(0)! - 55).toString()); From 42ce69d46cf153f9592ccafdf85d55903015bcde Mon Sep 17 00:00:00 2001 From: Dominic Baur Date: Tue, 2 Sep 2025 16:56:43 +0200 Subject: [PATCH 10/11] Add double regex schema for IBAN number check --- src/lib/string.spec.ts | 1 + src/lib/string.ts | 31 ++++++++++++++++--------------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/lib/string.spec.ts b/src/lib/string.spec.ts index 7fb2273..f9c167e 100644 --- a/src/lib/string.spec.ts +++ b/src/lib/string.spec.ts @@ -126,6 +126,7 @@ describe("string tests", () => { [undefined as unknown as string, false], ["CH9300762011623852957", true], ["CH93 0076 2011 6238 5295 7", true], + ["CH930076 20116238 5295 7", false], ["CH93-0076-2011-6238-5295-7", false], ["CH93 0000 0000 0000 0000 1", false], ["ch93 0076 2011 6238 5295 7", false], diff --git a/src/lib/string.ts b/src/lib/string.ts index 6729831..5215667 100644 --- a/src/lib/string.ts +++ b/src/lib/string.ts @@ -74,37 +74,38 @@ export function truncate(value: string | undefined, maxLength: number, suffix = * @returns The result of the IBAN number check */ export function isValidSwissIbanNumber(ibanNumber: string): boolean { + // 1. Reject null, undefined or whitespace-only strings if (isNullOrWhitespace(ibanNumber)) { return false; } - const compactIbanNumberWithWhiteSpaces = new RegExp(/^CH\d{2}(?:\s?\d{4}){4}\s?\d{1}$/); + // 2. Define allowed strict formats + // - with spaces: "CHXX XXXX XXXX XXXX XXXX X" + const compactIbanNumberWithWhiteSpaces = new RegExp(/^CH\d{2}(?: \d{4}){4} \d{1}$/); + // - without spaces: "CHXXXXXXXXXXXXXXXXXXX" + const compactIbanNumberWithoutWhiteSpaces = new RegExp(/^CH\d{19}$/); - if (!compactIbanNumberWithWhiteSpaces.test(ibanNumber)) { + // 3. Check if input matches one of the allowed formats + if (!compactIbanNumberWithWhiteSpaces.test(ibanNumber) && !compactIbanNumberWithoutWhiteSpaces.test(ibanNumber)) { return false; } - /** - * Validates a Swiss IBAN number. - * - * Steps: - * - The number must start with `CH`, be 21 digits long and follow one of the accepted formats: - * - `CHXX XXXX XXXX XXXX XXXX X`or `CHXXXXXXXXXXXXXXXXXXX`. - * - Remove all whitespaces. - * - Rearrange by moving the first 4 characters (CH + 2 check digits) to the end. - * - Replace letters with numbers: A=10, B=11, ..., Z=35. - * - Convert the resulting string to a large integer and calculate modulo 97. - * - The number is valid if the resto of the calculation equals 1. - */ - + // 4. Remove all spaces to get a compact IBAN string const compactIbanNumber = ibanNumber.replaceAll(" ", ""); + + // 5. Rearrange IBAN for checksum calculation + // - move first 4 characters (CH + 2 check digits) to the end const rearrangedIban = compactIbanNumber.slice(4) + compactIbanNumber.slice(0, 4); + + // 6. Replace letters with numbers (A=10, B=11, ..., Z=35) const numericStr = rearrangedIban.replaceAll(/[A-Z]/g, (ch) => (ch.codePointAt(0)! - 55).toString()); + // 7. Perform modulo 97 calculation to validate IBAN let restOfCalculation = 0; for (const digit of numericStr) { restOfCalculation = (restOfCalculation * 10 + Number(digit)) % 97; } + // 8. IBAN is valid only if the remainder equals 1 return restOfCalculation === 1; } From 328ef6c84570ee959ce9ffa76dcbb2198f98aa76 Mon Sep 17 00:00:00 2001 From: Dominic Baur Date: Wed, 3 Sep 2025 15:16:13 +0200 Subject: [PATCH 11/11] Formatted --- src/lib/string.spec.ts | 14 +++++++++++--- src/lib/string.ts | 2 +- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/lib/string.spec.ts b/src/lib/string.spec.ts index f18d05a..2c0d975 100644 --- a/src/lib/string.spec.ts +++ b/src/lib/string.spec.ts @@ -1,4 +1,12 @@ -import { isNullOrEmpty, isNullOrWhitespace, capitalize, uncapitalize, truncate, isValidSwissIbanNumber ,isValidSwissSocialSecurityNumber } from "./string"; +import { + isNullOrEmpty, + isNullOrWhitespace, + capitalize, + uncapitalize, + truncate, + isValidSwissIbanNumber, + isValidSwissSocialSecurityNumber, +} from "./string"; describe("string tests", () => { test.each([ @@ -134,7 +142,7 @@ describe("string tests", () => { ])("check if this swiss IBAN is valid or not", (unformattedIbanNumber, expected) => { expect(isValidSwissIbanNumber(unformattedIbanNumber)).toBe(expected); }); - + test.each([ [null as unknown as string, false], [undefined as unknown as string, false], @@ -149,4 +157,4 @@ describe("string tests", () => { ])("check if the social insurance number is valid or not", (ahvNumber, expected) => { expect(isValidSwissSocialSecurityNumber(ahvNumber)).toBe(expected); }); -}); \ No newline at end of file +}); diff --git a/src/lib/string.ts b/src/lib/string.ts index c0aa095..c8fbdcb 100644 --- a/src/lib/string.ts +++ b/src/lib/string.ts @@ -171,4 +171,4 @@ export function isValidSwissSocialSecurityNumber(socialInsuranceNumber: string): * - If equal → valid AHV number */ return checksum === checknumber; -} \ No newline at end of file +}