diff --git a/CHANGELOG.md b/CHANGELOG.md index ec8feb5..ca7607a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- `isValidSwissIbanNumber` string utility function - `isValidSwissSocialSecurityNumber` string utility function ## [2.0.0] - 2025-07-29 diff --git a/src/lib/string.spec.ts b/src/lib/string.spec.ts index f9049f8..2c0d975 100644 --- a/src/lib/string.spec.ts +++ b/src/lib/string.spec.ts @@ -1,4 +1,12 @@ -import { isNullOrEmpty, isNullOrWhitespace, capitalize, uncapitalize, truncate, isValidSwissSocialSecurityNumber } from "./string"; +import { + isNullOrEmpty, + isNullOrWhitespace, + capitalize, + uncapitalize, + truncate, + isValidSwissIbanNumber, + isValidSwissSocialSecurityNumber, +} from "./string"; describe("string tests", () => { test.each([ @@ -121,6 +129,20 @@ describe("string tests", () => { expect(truncate(value, maxLength)).toBe(expected); }); + test.each([ + [null as unknown as string, false], + [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], + ["DE93 0076 2011 6238 5295 7", false], + ])("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], diff --git a/src/lib/string.ts b/src/lib/string.ts index c14768c..c8fbdcb 100644 --- a/src/lib/string.ts +++ b/src/lib/string.ts @@ -65,6 +65,51 @@ 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 + * 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 { + // 1. Reject null, undefined or whitespace-only strings + if (isNullOrWhitespace(ibanNumber)) { + return false; + } + + // 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}$/); + + // 3. Check if input matches one of the allowed formats + if (!compactIbanNumberWithWhiteSpaces.test(ibanNumber) && !compactIbanNumberWithoutWhiteSpaces.test(ibanNumber)) { + return false; + } + + // 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; +} + /** * Validation of social insurance number with checking the checksum * Validation according to https://www.sozialversicherungsnummer.ch/aufbau-neu.htm