From a4f86a91f03571a9bcdb12d1177cf19b68fe9d2f Mon Sep 17 00:00:00 2001 From: Dominic Baur Date: Mon, 8 Sep 2025 08:14:50 +0200 Subject: [PATCH 1/2] Moved isValidSwissSocialInsuranceNumber and isValidSwissIbanNumber to swiss standards --- CHANGELOG.md | 8 +++ src/lib/string.spec.ts | 39 +----------- src/lib/string.ts | 108 -------------------------------- src/lib/swissStandards.spec.ts | 32 ++++++++++ src/lib/swissStandards.ts | 109 +++++++++++++++++++++++++++++++++ 5 files changed, 150 insertions(+), 146 deletions(-) create mode 100644 src/lib/swissStandards.spec.ts create mode 100644 src/lib/swissStandards.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a710d1..724c300 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Moved `isValidSwissIbanNumber` and `isValidSwissSocialInsuranceNumber` to swissStandards + +### Fixed + +- `isValidSwissSocialInsuranceNumber` is now named properly + ## [2.1.0] - 2025-09-03 ### Added diff --git a/src/lib/string.spec.ts b/src/lib/string.spec.ts index 2c0d975..94fc71e 100644 --- a/src/lib/string.spec.ts +++ b/src/lib/string.spec.ts @@ -1,12 +1,4 @@ -import { - isNullOrEmpty, - isNullOrWhitespace, - capitalize, - uncapitalize, - truncate, - isValidSwissIbanNumber, - isValidSwissSocialSecurityNumber, -} from "./string"; +import { isNullOrEmpty, isNullOrWhitespace, capitalize, uncapitalize, truncate } from "./string"; describe("string tests", () => { test.each([ @@ -128,33 +120,4 @@ 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 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], - ["7561234567891", false], - ["7569217076985", true], - ["756.92170769.85", false], - ["756.9217.0769.85", true], - ["756..9217.0769.85", false], - ["756.1234.5678.91", false], - ["test756.9217.0769.85", false], - ["7.56..9217...0769.85", false], - ])("check if the social insurance number is valid or not", (ahvNumber, expected) => { - expect(isValidSwissSocialSecurityNumber(ahvNumber)).toBe(expected); - }); }); diff --git a/src/lib/string.ts b/src/lib/string.ts index c8fbdcb..c0666b3 100644 --- a/src/lib/string.ts +++ b/src/lib/string.ts @@ -64,111 +64,3 @@ 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 - * @param socialInsuranceNumber The social insurance number to check - * Must be in one of the following formats: - * - "756.XXXX.XXXX.XX" with dots as separators - * - "756XXXXXXXXXX" with digits only - * @returns The result if the social insurance number is valid or not - */ -export function isValidSwissSocialSecurityNumber(socialInsuranceNumber: string): boolean { - // 1. Check if input is empty or only whitespace - if (isNullOrWhitespace(socialInsuranceNumber)) { - return false; - } - - /** - * 2. Check if input matches accepted formats: - * - With dots: 756.XXXX.XXXX.XX - * - Without dots: 756XXXXXXXXXX - */ - const socialInsuranceNumberWithDots = new RegExp(/^756\.\d{4}\.\d{4}\.\d{2}$/); - const socialInsuranceNumberWithoutDots = new RegExp(/^756\d{10}$/); - - if (!socialInsuranceNumberWithDots.test(socialInsuranceNumber) && !socialInsuranceNumberWithoutDots.test(socialInsuranceNumber)) { - return false; - } - - // 3. Remove all dots → get a string of 13 digits - const compactNumber = socialInsuranceNumber.replaceAll(".", ""); - - /** - * 4. Separate digits for checksum calculation - * - first 12 digits: used to calculate checksum - * - last digit: actual check digit - */ - const digits = compactNumber.slice(0, -1); - const reversedDigits = [...digits].reverse().join(""); - const reversedDigitsArray = [...reversedDigits]; - - /* - * 5. Calculate weighted sum for checksum - * - Even positions (after reversing) ×3 - * - Odd positions ×1 - */ - let sum = 0; - for (const [i, element] of reversedDigitsArray.entries()) { - sum += i % 2 === 0 ? Number(element) * 3 : Number(element) * 1; - } - - /* - * 6. Calculate expected check digit - * - Check digit = value to reach next multiple of 10 - */ - const checksum = (10 - (sum % 10)) % 10; - const checknumber = Number.parseInt(compactNumber.slice(-1)); - - /* - * 7. Compare calculated check digit with actual last digit - * - If equal → valid AHV number - */ - return checksum === checknumber; -} diff --git a/src/lib/swissStandards.spec.ts b/src/lib/swissStandards.spec.ts new file mode 100644 index 0000000..30f3df6 --- /dev/null +++ b/src/lib/swissStandards.spec.ts @@ -0,0 +1,32 @@ +import { isValidSwissIbanNumber, isValidSwissSocialInsuranceNumber } from "./swissStandards"; + +describe("Swiss standards test", () => { + 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], + ["7561234567891", false], + ["7569217076985", true], + ["756.92170769.85", false], + ["756.9217.0769.85", true], + ["756..9217.0769.85", false], + ["756.1234.5678.91", false], + ["test756.9217.0769.85", false], + ["7.56..9217...0769.85", false], + ])("check if the social insurance number is valid or not", (ahvNumber, expected) => { + expect(isValidSwissSocialInsuranceNumber(ahvNumber)).toBe(expected); + }); +}); diff --git a/src/lib/swissStandards.ts b/src/lib/swissStandards.ts new file mode 100644 index 0000000..cfcaa68 --- /dev/null +++ b/src/lib/swissStandards.ts @@ -0,0 +1,109 @@ +import { isNullOrWhitespace } from "./string"; + +/** + * 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 + * @param socialInsuranceNumber The social insurance number to check + * Must be in one of the following formats: + * - "756.XXXX.XXXX.XX" with dots as separators + * - "756XXXXXXXXXX" with digits only + * @returns The result if the social insurance number is valid or not + */ +export function isValidSwissSocialInsuranceNumber(socialInsuranceNumber: string): boolean { + // 1. Check if input is empty or only whitespace + if (isNullOrWhitespace(socialInsuranceNumber)) { + return false; + } + + /** + * 2. Check if input matches accepted formats: + * - With dots: 756.XXXX.XXXX.XX + * - Without dots: 756XXXXXXXXXX + */ + const socialInsuranceNumberWithDots = new RegExp(/^756\.\d{4}\.\d{4}\.\d{2}$/); + const socialInsuranceNumberWithoutDots = new RegExp(/^756\d{10}$/); + + if (!socialInsuranceNumberWithDots.test(socialInsuranceNumber) && !socialInsuranceNumberWithoutDots.test(socialInsuranceNumber)) { + return false; + } + + // 3. Remove all dots → get a string of 13 digits + const compactNumber = socialInsuranceNumber.replaceAll(".", ""); + + /** + * 4. Separate digits for checksum calculation + * - first 12 digits: used to calculate checksum + * - last digit: actual check digit + */ + const digits = compactNumber.slice(0, -1); + const reversedDigits = [...digits].reverse().join(""); + const reversedDigitsArray = [...reversedDigits]; + + /* + * 5. Calculate weighted sum for checksum + * - Even positions (after reversing) ×3 + * - Odd positions ×1 + */ + let sum = 0; + for (const [i, element] of reversedDigitsArray.entries()) { + sum += i % 2 === 0 ? Number(element) * 3 : Number(element) * 1; + } + + /* + * 6. Calculate expected check digit + * - Check digit = value to reach next multiple of 10 + */ + const checksum = (10 - (sum % 10)) % 10; + const checknumber = Number.parseInt(compactNumber.slice(-1)); + + /* + * 7. Compare calculated check digit with actual last digit + * - If equal → valid AHV number + */ + return checksum === checknumber; +} From f5b4750001e1ba6b10333d39c6f57435fe58f7c5 Mon Sep 17 00:00:00 2001 From: Dominic Baur Date: Mon, 8 Sep 2025 10:24:25 +0200 Subject: [PATCH 2/2] Add export --- src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.ts b/src/index.ts index b2fc510..a833712 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,3 +8,4 @@ export * from "./lib/mimeType"; export * from "./lib/number"; export * from "./lib/object"; export * from "./lib/string"; +export * from "./lib/swissStandards";