From 95f072978bc671fb47931d16ce5e708c5df0df80 Mon Sep 17 00:00:00 2001 From: Dominic Baur Date: Thu, 4 Sep 2025 15:06:15 +0200 Subject: [PATCH 1/5] Add string utility function formatSwissSocialInsuranceNumber --- CHANGELOG.md | 4 + src/lib/string.spec.ts | 39 +-------- src/lib/string.ts | 108 ------------------------- src/lib/swissStandards.spec.ts | 48 +++++++++++ src/lib/swissStandards.ts | 141 +++++++++++++++++++++++++++++++++ 5 files changed, 194 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..7234a7d 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 + +- `formatSwissSocialInsuranceNumber` string utility function + ## [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..9ddc61d --- /dev/null +++ b/src/lib/swissStandards.spec.ts @@ -0,0 +1,48 @@ +import { formatSwissSocialInsuranceNumber, isValidSwissIbanNumber, isValidSwissSocialSecurityNumber } 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(isValidSwissSocialSecurityNumber(ahvNumber)).toBe(expected); + }); + + test.each([ + [null as unknown as string, null, false], + ["7569217076985", "756.9217.0769.85", true], + ["7561234567891", "756.1234.5678.91", false], + ["75 61 23 456789 1", "756.1234.5678.91", false], + ["75 69 21 707698 5", "756.9217.0769.85", true], + ])( + "Check if the social insurance number gets formatted correctly", + (unformattedSocialInsuranceNumber, expectedSocialInsuranceNumber, expectedIsValid) => { + const result = formatSwissSocialInsuranceNumber(unformattedSocialInsuranceNumber); + + expect(result.socialInsuranceNumber).toBe(expectedSocialInsuranceNumber); + expect(result.isValidSwissSocialInsuranceNumber).toBe(expectedIsValid); + }, + ); +}); diff --git a/src/lib/swissStandards.ts b/src/lib/swissStandards.ts new file mode 100644 index 0000000..3e22fd5 --- /dev/null +++ b/src/lib/swissStandards.ts @@ -0,0 +1,141 @@ +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 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; +} + +/** + * Formats a unformatted Swiss social insurance number to the standard format "756.XXXX.XXXX.XX" + * @param unformattedSocialInsuranceNumber the unformatted Swiss social insurance number to format + * @returns a object containing the formatted Swiss social insurance number and a boolean indicating if the number was valid or not + */ +export function formatSwissSocialInsuranceNumber(unformattedSocialInsuranceNumber: string): { + /** + * The formatted Swiss social insurance number or the original input if the Swiss social insurance number was invalid + */ + socialInsuranceNumber: string; + /** + * The result if the social insurance number is a valid Swiss social insurance number or not + */ + isValidSwissSocialInsuranceNumber: boolean; +} { + // 1. Check if the unformatted Swiss social insurance number is empty or only a whitespace + if (isNullOrWhitespace(unformattedSocialInsuranceNumber)) { + return { socialInsuranceNumber: unformattedSocialInsuranceNumber, isValidSwissSocialInsuranceNumber: false }; + } + + // 2. Remove all non-digit characters, then format as Swiss social insurance number (XXX.XXXX.XXXX.XX) + const cleaned = unformattedSocialInsuranceNumber.replaceAll(/\D+/g, ""); + const formattedSwissSocialInsuranceNumber = cleaned.replaceAll(/(\d{3})(\d{4})(\d{4})(\d{2})/g, "$1.$2.$3.$4"); + + // 3. If the Swiss social insurance number is valid return the formatted number with the true status + if (isValidSwissSocialSecurityNumber(formattedSwissSocialInsuranceNumber)) { + return { socialInsuranceNumber: formattedSwissSocialInsuranceNumber, isValidSwissSocialInsuranceNumber: true }; + } + // 4. If the Swiss social insurance number is not valid return the formatted number with the false status + return { socialInsuranceNumber: formattedSwissSocialInsuranceNumber, isValidSwissSocialInsuranceNumber: false }; +} From cf7a4ca4eaddd41a21f1cf68d20e9c5cb1677bea Mon Sep 17 00:00:00 2001 From: Dominic Baur Date: Fri, 5 Sep 2025 10:39:31 +0200 Subject: [PATCH 2/5] Fix function name --- CHANGELOG.md | 4 ++++ src/lib/swissStandards.spec.ts | 4 ++-- src/lib/swissStandards.ts | 4 ++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7234a7d..caf0c32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `formatSwissSocialInsuranceNumber` string utility function +### Fixed + +- `isValidSwissSocialInsuranceNumber` is now named properly + ## [2.1.0] - 2025-09-03 ### Added diff --git a/src/lib/swissStandards.spec.ts b/src/lib/swissStandards.spec.ts index 9ddc61d..b98e613 100644 --- a/src/lib/swissStandards.spec.ts +++ b/src/lib/swissStandards.spec.ts @@ -1,4 +1,4 @@ -import { formatSwissSocialInsuranceNumber, isValidSwissIbanNumber, isValidSwissSocialSecurityNumber } from "./swissStandards"; +import { formatSwissSocialInsuranceNumber, isValidSwissIbanNumber, isValidSwissSocialInsuranceNumber } from "./swissStandards"; describe("Swiss standards test", () => { test.each([ @@ -27,7 +27,7 @@ describe("Swiss standards test", () => { ["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); + expect(isValidSwissSocialInsuranceNumber(ahvNumber)).toBe(expected); }); test.each([ diff --git a/src/lib/swissStandards.ts b/src/lib/swissStandards.ts index 3e22fd5..fcd3491 100644 --- a/src/lib/swissStandards.ts +++ b/src/lib/swissStandards.ts @@ -54,7 +54,7 @@ export function isValidSwissIbanNumber(ibanNumber: string): boolean { * - "756XXXXXXXXXX" with digits only * @returns The result if the social insurance number is valid or not */ -export function isValidSwissSocialSecurityNumber(socialInsuranceNumber: string): boolean { +export function isValidSwissSocialInsuranceNumber(socialInsuranceNumber: string): boolean { // 1. Check if input is empty or only whitespace if (isNullOrWhitespace(socialInsuranceNumber)) { return false; @@ -133,7 +133,7 @@ export function formatSwissSocialInsuranceNumber(unformattedSocialInsuranceNumbe const formattedSwissSocialInsuranceNumber = cleaned.replaceAll(/(\d{3})(\d{4})(\d{4})(\d{2})/g, "$1.$2.$3.$4"); // 3. If the Swiss social insurance number is valid return the formatted number with the true status - if (isValidSwissSocialSecurityNumber(formattedSwissSocialInsuranceNumber)) { + if (isValidSwissSocialInsuranceNumber(formattedSwissSocialInsuranceNumber)) { return { socialInsuranceNumber: formattedSwissSocialInsuranceNumber, isValidSwissSocialInsuranceNumber: true }; } // 4. If the Swiss social insurance number is not valid return the formatted number with the false status From 15b1c43353027ae4c5cfb4e095e0b22b0620d6f2 Mon Sep 17 00:00:00 2001 From: Dominic Baur Date: Mon, 8 Sep 2025 16:41:27 +0200 Subject: [PATCH 3/5] Fixed changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29cc3a1..59db9f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- `formatSwissSocialInsuranceNumber` string utility function +- `formatSwissSocialInsuranceNumber` swiss standard function ### Changed From 579fd32ff6d64ca7170776f3e330fd9f4338bff7 Mon Sep 17 00:00:00 2001 From: Dominic Baur Date: Wed, 1 Oct 2025 16:17:54 +0200 Subject: [PATCH 4/5] Removed the old formatFunction --- src/lib/swissStandards.spec.ts | 18 +----------------- src/lib/swissStandards.ts | 32 -------------------------------- 2 files changed, 1 insertion(+), 49 deletions(-) diff --git a/src/lib/swissStandards.spec.ts b/src/lib/swissStandards.spec.ts index 43ed0e9..ec29423 100644 --- a/src/lib/swissStandards.spec.ts +++ b/src/lib/swissStandards.spec.ts @@ -1,4 +1,4 @@ -import { formatSwissSocialInsuranceNumber, isValidSwissIbanNumber, isValidSwissSocialInsuranceNumber } from "./swissStandards"; +import { isValidSwissIbanNumber, isValidSwissSocialInsuranceNumber } from "./swissStandards"; describe("Swiss standards test", () => { test.each([ @@ -42,20 +42,4 @@ describe("Swiss standards test", () => { ])("check if the social insurance number is valid or not", (socialInsuranceNumberToCheck, expected) => { expect(isValidSwissSocialInsuranceNumber(socialInsuranceNumberToCheck)).toBe(expected); }); - - test.each([ - [null as unknown as string, null, false], - ["7569217076985", "756.9217.0769.85", true], - ["7561234567891", "756.1234.5678.91", false], - ["75 61 23 456789 1", "756.1234.5678.91", false], - ["75 69 21 707698 5", "756.9217.0769.85", true], - ])( - "Check if the social insurance number gets formatted correctly", - (unformattedSocialInsuranceNumber, expectedSocialInsuranceNumber, expectedIsValid) => { - const result = formatSwissSocialInsuranceNumber(unformattedSocialInsuranceNumber); - - expect(result.socialInsuranceNumber).toBe(expectedSocialInsuranceNumber); - expect(result.isValidSwissSocialInsuranceNumber).toBe(expectedIsValid); - }, - ); }); diff --git a/src/lib/swissStandards.ts b/src/lib/swissStandards.ts index b709390..3fd7d13 100644 --- a/src/lib/swissStandards.ts +++ b/src/lib/swissStandards.ts @@ -107,35 +107,3 @@ export function isValidSwissSocialInsuranceNumber(socialInsuranceNumber: string) */ return checksum === checknumber; } - -/** - * Formats a unformatted Swiss social insurance number to the standard format "756.XXXX.XXXX.XX" - * @param unformattedSocialInsuranceNumber the unformatted Swiss social insurance number to format - * @returns a object containing the formatted Swiss social insurance number and a boolean indicating if the number was valid or not - */ -export function formatSwissSocialInsuranceNumber(unformattedSocialInsuranceNumber: string): { - /** - * The formatted Swiss social insurance number or the original input if the Swiss social insurance number was invalid - */ - socialInsuranceNumber: string; - /** - * The result if the social insurance number is a valid Swiss social insurance number or not - */ - isValidSwissSocialInsuranceNumber: boolean; -} { - // 1. Check if the unformatted Swiss social insurance number is empty or only a whitespace - if (isNullOrWhitespace(unformattedSocialInsuranceNumber)) { - return { socialInsuranceNumber: unformattedSocialInsuranceNumber, isValidSwissSocialInsuranceNumber: false }; - } - - // 2. Remove all non-digit characters, then format as Swiss social insurance number (XXX.XXXX.XXXX.XX) - const cleaned = unformattedSocialInsuranceNumber.replaceAll(/\D+/g, ""); - const formattedSwissSocialInsuranceNumber = cleaned.replaceAll(/(\d{3})(\d{4})(\d{4})(\d{2})/g, "$1.$2.$3.$4"); - - // 3. If the Swiss social insurance number is valid return the formatted number with the true status - if (isValidSwissSocialInsuranceNumber(formattedSwissSocialInsuranceNumber)) { - return { socialInsuranceNumber: formattedSwissSocialInsuranceNumber, isValidSwissSocialInsuranceNumber: true }; - } - // 4. If the Swiss social insurance number is not valid return the formatted number with the false status - return { socialInsuranceNumber: formattedSwissSocialInsuranceNumber, isValidSwissSocialInsuranceNumber: false }; -} From 411bd7af6356aecb890cfc6ec277071e90582397 Mon Sep 17 00:00:00 2001 From: Dominic Baur Date: Thu, 2 Oct 2025 16:19:36 +0200 Subject: [PATCH 5/5] Add swiss standards function tryParseSwissSocialInsuranceNumber --- CHANGELOG.md | 4 +-- src/lib/swissStandards.spec.ts | 34 ++++++++++++++++++++++- src/lib/swissStandards.ts | 51 +++++++++++++++++++++++++++++++++- 3 files changed, 85 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fff3566..96ad242 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- `formatSwissSocialInsuranceNumber` swiss standard function -- `splitLines` string utility function - `tryParseSwissIbanNumber` swiss standard function +- `tryParseSwissSocialInsuranceNumber` swiss standard function +- `splitLines` string utility function - `trimStart`, `trimEnd` and `trim` string type utility functions ### Changed diff --git a/src/lib/swissStandards.spec.ts b/src/lib/swissStandards.spec.ts index 9708e93..8315a86 100644 --- a/src/lib/swissStandards.spec.ts +++ b/src/lib/swissStandards.spec.ts @@ -1,4 +1,9 @@ -import { isValidSwissIbanNumber, isValidSwissSocialInsuranceNumber, tryParseSwissIbanNumber } from "./swissStandards"; +import { + isValidSwissIbanNumber, + isValidSwissSocialInsuranceNumber, + tryParseSwissIbanNumber, + tryParseSwissSocialInsuranceNumber, +} from "./swissStandards"; describe("Swiss standards test", () => { test.each([ @@ -58,4 +63,31 @@ describe("Swiss standards test", () => { ])("check if the Swiss IBAN number gets parsed correctly", (unformattedSwissIbanNumber, expected) => { expect(tryParseSwissIbanNumber(unformattedSwissIbanNumber)).toEqual(expected); }); + + test.each([ + [ + undefined as unknown as string, + { isValid: false, swissSocialInsuranceNumber: undefined, swissSocialInsuranceNumberFormatted: undefined }, + ], + [null as unknown as string, { isValid: false, swissSocialInsuranceNumber: undefined, swissSocialInsuranceNumberFormatted: undefined }], + [ + "7569217076985", + { isValid: true, swissSocialInsuranceNumber: "7569217076985", swissSocialInsuranceNumberFormatted: "756.9217.0769.85" }, + ], + [ + "756.9217.0769.85", + { isValid: true, swissSocialInsuranceNumber: "7569217076985", swissSocialInsuranceNumberFormatted: "756.9217.0769.85" }, + ], + [ + "7 56-9217.076 + 9.8 >5", + { isValid: true, swissSocialInsuranceNumber: "7569217076985", swissSocialInsuranceNumberFormatted: "756.9217.0769.85" }, + ], + [ + "7 56-9217.076 + 9.8 \n5", + { isValid: true, swissSocialInsuranceNumber: "7569217076985", swissSocialInsuranceNumberFormatted: "756.9217.0769.85" }, + ], + ["7561234567891", { isValid: false, swissSocialInsuranceNumber: undefined, swissSocialInsuranceNumberFormatted: undefined }], + ])("check if the Swiss IBAN number gets parsed correctly", (unformattedSwissIbanNumber, expected) => { + expect(tryParseSwissSocialInsuranceNumber(unformattedSwissIbanNumber)).toEqual(expected); + }); }); diff --git a/src/lib/swissStandards.ts b/src/lib/swissStandards.ts index ac13650..c0089fe 100644 --- a/src/lib/swissStandards.ts +++ b/src/lib/swissStandards.ts @@ -116,7 +116,20 @@ export function isValidSwissSocialInsuranceNumber(socialInsuranceNumber: string) * @property {string} iban - The cleaned IBAN, only present if valid * @property {string} ibanFormatted - The formatted IBAN, only present if valid */ -export function tryParseSwissIbanNumber(unformattedIbanNumber?: string) { +export function tryParseSwissIbanNumber(unformattedIbanNumber?: string): { + /** + * Indicates whether the IBAN is valid or not + */ + isValid: boolean; + /** + * The cleaned IBAN, only present if valid + */ + iban?: string; + /** + * The formatted IBAN, only present if valid + */ + ibanFormatted?: string; +} { if (isNullOrWhitespace(unformattedIbanNumber)) { return { isValid: false }; } @@ -130,3 +143,39 @@ export function tryParseSwissIbanNumber(unformattedIbanNumber?: string) { ibanFormatted: isValid ? iban.match(/.{1,4}/g)?.join(" ") : undefined, }; } + +/** + * Attempts to parse and validate a Swiss social insurance number. + * @param unformattedInsuranceNumber - The unformatted Swiss social insurance number + * @returns The result object with the following properties: + * @property {boolean} isValid - Indicates whether the Swiss social insurance number is valid or not + * @property {string} swissSocialInsuranceNumber - The cleaned Swiss social insurance number, only present if valid + * @property {string} swissSocialInsuranceNumberFormatted - The formatted Swiss social insurance number, only present if valid + */ +export function tryParseSwissSocialInsuranceNumber(unformattedInsuranceNumber?: string): { + /** + * Indicates whether the Swiss social insurance number is valid or not + */ + isValid: boolean; + /** + * The cleaned Swiss social insurance number, only present if valid + */ + swissSocialInsuranceNumber?: string; + /** + * The formatted Swiss social insurance number, only present if valid + */ + swissSocialInsuranceNumberFormatted?: string; +} { + if (isNullOrWhitespace(unformattedInsuranceNumber)) { + return { isValid: false }; + } + + const insuranceNumber = unformattedInsuranceNumber!.replaceAll(/\D/g, ""); + const isValid = isValidSwissSocialInsuranceNumber(insuranceNumber); + + return { + isValid: isValid, + swissSocialInsuranceNumber: isValid ? insuranceNumber : undefined, + swissSocialInsuranceNumberFormatted: isValid ? insuranceNumber.replace(/^(\d{3})(\d{4})(\d{4})(\d{2})$/, "$1.$2.$3.$4") : undefined, + }; +}