From 738d1acd381b2cdba923df49c0296619610dbc02 Mon Sep 17 00:00:00 2001 From: Dominic Baur Date: Wed, 27 Aug 2025 10:49:19 +0200 Subject: [PATCH 01/18] Add string utility function isValidSwissSocialSecurityNumber --- CHANGELOG.md | 4 ++++ src/lib/string.spec.ts | 12 +++++++++++- src/lib/string.ts | 31 +++++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c15e1c6..ec8feb5 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 + +- `isValidSwissSocialSecurityNumber` 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..7162223 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, isValidSwissSocialSecurityNumber } from "./string"; describe("string tests", () => { test.each([ @@ -120,4 +120,14 @@ 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], + ["7569217076985", false], + ["756.9217.0769.85", true], + ["756.1234.5678.99", false], + ])("check if ahv 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 c0666b3..fadbc98 100644 --- a/src/lib/string.ts +++ b/src/lib/string.ts @@ -64,3 +64,34 @@ export function truncate(value: string | undefined, maxLength: number, suffix = return `${value.slice(0, maxLength)}${suffix}`; } + +/** + * Validation of Social insurance number with regex and checking of checksum + * Validation according to https://www.sozialversicherungsnummer.ch/aufbau-neu.htm + * @param socialInsuranceNumber The social insurance number to check + * @returns the result if the social insurance number is valid or not + */ +export function isValidSwissSocialSecurityNumber(socialInsuranceNumber: string): boolean { + const regex = new RegExp(/[7][5][6][.][\d]{4}[.][\d]{4}[.][\d]{2}$/); + if (regex.test(socialInsuranceNumber) && !isNullOrEmpty(socialInsuranceNumber)) { + //todo check checksum + const number = socialInsuranceNumber.slice(0, -1); + + const reversedNumber = [...number.split(".").join("")].reverse().join(""); + const reversedNumberArray = [...reversedNumber]; + let sum = 0; + for (const [i, element] of reversedNumberArray.entries()) { + sum += i % 2 === 0 ? Number(element) * 3 : Number(element) * 1; + } + + const checksum = Math.ceil(sum / 10) * 10 - sum; + const checknumber = Number.parseInt(socialInsuranceNumber.slice(-1)); + + if (checksum !== checknumber) { + return false; + } + return true; + } else { + return false; + } +} From 4aac36e5fc853fdf09836496f869d987e9c23abb Mon Sep 17 00:00:00 2001 From: Dominic Baur Date: Wed, 27 Aug 2025 15:41:17 +0200 Subject: [PATCH 02/18] Add:ed that AHV number doesn't have to be formatted already to get checked --- src/lib/string.spec.ts | 4 +++- src/lib/string.ts | 9 ++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/lib/string.spec.ts b/src/lib/string.spec.ts index 7162223..d6d296c 100644 --- a/src/lib/string.spec.ts +++ b/src/lib/string.spec.ts @@ -124,9 +124,11 @@ describe("string tests", () => { test.each([ [null as unknown as string, false], [undefined as unknown as string, false], - ["7569217076985", false], + ["7569217076985", true], ["756.9217.0769.85", true], ["756.1234.5678.99", false], + ["7561234567899", false], + ["7000000000000", false], ])("check if ahv 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 fadbc98..3c0d129 100644 --- a/src/lib/string.ts +++ b/src/lib/string.ts @@ -72,9 +72,9 @@ export function truncate(value: string | undefined, maxLength: number, suffix = * @returns the result if the social insurance number is valid or not */ export function isValidSwissSocialSecurityNumber(socialInsuranceNumber: string): boolean { - const regex = new RegExp(/[7][5][6][.][\d]{4}[.][\d]{4}[.][\d]{2}$/); - if (regex.test(socialInsuranceNumber) && !isNullOrEmpty(socialInsuranceNumber)) { - //todo check checksum + if (!isNullOrEmpty(socialInsuranceNumber)) { + const compactInsuranceNumber = socialInsuranceNumber.replaceAll(/[\s.]+/g, ""); + if (!/^756\d{10}$/.test(compactInsuranceNumber)) return false; const number = socialInsuranceNumber.slice(0, -1); const reversedNumber = [...number.split(".").join("")].reverse().join(""); @@ -91,7 +91,6 @@ export function isValidSwissSocialSecurityNumber(socialInsuranceNumber: string): return false; } return true; - } else { - return false; } + return false; } From 32b3b2cb6943263dd0bcdabaac9996a9f346fd5a Mon Sep 17 00:00:00 2001 From: Dominic Baur Date: Wed, 27 Aug 2025 15:42:53 +0200 Subject: [PATCH 03/18] Changed comments --- src/lib/string.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/string.ts b/src/lib/string.ts index 3c0d129..8bb2163 100644 --- a/src/lib/string.ts +++ b/src/lib/string.ts @@ -66,10 +66,10 @@ export function truncate(value: string | undefined, maxLength: number, suffix = } /** - * Validation of Social insurance number with regex and checking of checksum + * 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 - * @returns the result if the social insurance number is valid or not + * @returns The result if the social insurance number is valid or not */ export function isValidSwissSocialSecurityNumber(socialInsuranceNumber: string): boolean { if (!isNullOrEmpty(socialInsuranceNumber)) { From 36175859a418ccb4705faf4ab9ba815220bb34bb Mon Sep 17 00:00:00 2001 From: "Sandro C." Date: Wed, 27 Aug 2025 15:59:50 +0200 Subject: [PATCH 04/18] Update src/lib/string.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/lib/string.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/lib/string.ts b/src/lib/string.ts index 8bb2163..c2c1844 100644 --- a/src/lib/string.ts +++ b/src/lib/string.ts @@ -87,10 +87,7 @@ export function isValidSwissSocialSecurityNumber(socialInsuranceNumber: string): const checksum = Math.ceil(sum / 10) * 10 - sum; const checknumber = Number.parseInt(socialInsuranceNumber.slice(-1)); - if (checksum !== checknumber) { - return false; - } - return true; + return checksum === checknumber; } return false; } From b6323b4766effb0e84e7514d5a7dd2dddd3a143b Mon Sep 17 00:00:00 2001 From: Dominic Baur Date: Wed, 27 Aug 2025 16:04:52 +0200 Subject: [PATCH 05/18] Fix --- src/lib/string.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/lib/string.ts b/src/lib/string.ts index 8bb2163..fea9204 100644 --- a/src/lib/string.ts +++ b/src/lib/string.ts @@ -75,7 +75,7 @@ export function isValidSwissSocialSecurityNumber(socialInsuranceNumber: string): if (!isNullOrEmpty(socialInsuranceNumber)) { const compactInsuranceNumber = socialInsuranceNumber.replaceAll(/[\s.]+/g, ""); if (!/^756\d{10}$/.test(compactInsuranceNumber)) return false; - const number = socialInsuranceNumber.slice(0, -1); + const number = compactInsuranceNumber.slice(0, -1); const reversedNumber = [...number.split(".").join("")].reverse().join(""); const reversedNumberArray = [...reversedNumber]; @@ -84,13 +84,10 @@ export function isValidSwissSocialSecurityNumber(socialInsuranceNumber: string): sum += i % 2 === 0 ? Number(element) * 3 : Number(element) * 1; } - const checksum = Math.ceil(sum / 10) * 10 - sum; - const checknumber = Number.parseInt(socialInsuranceNumber.slice(-1)); + const checksum = (10 - (sum % 10)) % 10; + const checknumber = Number.parseInt(compactInsuranceNumber.slice(-1)); - if (checksum !== checknumber) { - return false; - } - return true; + return checksum === checknumber; } return false; } From f7a4a547d4deb3f3609f0e3ce7093d3a7216a611 Mon Sep 17 00:00:00 2001 From: Dominic Baur Date: Wed, 27 Aug 2025 16:16:03 +0200 Subject: [PATCH 06/18] Fix --- src/lib/string.ts | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/src/lib/string.ts b/src/lib/string.ts index fea9204..f09a296 100644 --- a/src/lib/string.ts +++ b/src/lib/string.ts @@ -72,22 +72,23 @@ export function truncate(value: string | undefined, maxLength: number, suffix = * @returns The result if the social insurance number is valid or not */ export function isValidSwissSocialSecurityNumber(socialInsuranceNumber: string): boolean { - if (!isNullOrEmpty(socialInsuranceNumber)) { - const compactInsuranceNumber = socialInsuranceNumber.replaceAll(/[\s.]+/g, ""); - if (!/^756\d{10}$/.test(compactInsuranceNumber)) return false; - const number = compactInsuranceNumber.slice(0, -1); - - const reversedNumber = [...number.split(".").join("")].reverse().join(""); - const reversedNumberArray = [...reversedNumber]; - let sum = 0; - for (const [i, element] of reversedNumberArray.entries()) { - sum += i % 2 === 0 ? Number(element) * 3 : Number(element) * 1; - } + if (isNullOrEmpty(socialInsuranceNumber)) { + return false; + } + const compactInsuranceNumber = socialInsuranceNumber.replaceAll(/[\s.]+/g, ""); + if (!/^756\d{10}$/.test(compactInsuranceNumber)) { + return false; + } + const number = compactInsuranceNumber.slice(0, -1); + const reversedNumber = [...number.split(".").join("")].reverse().join(""); + const reversedNumberArray = [...reversedNumber]; + let sum = 0; + for (const [i, element] of reversedNumberArray.entries()) { + sum += i % 2 === 0 ? Number(element) * 3 : Number(element) * 1; + } - const checksum = (10 - (sum % 10)) % 10; - const checknumber = Number.parseInt(compactInsuranceNumber.slice(-1)); + const checksum = (10 - (sum % 10)) % 10; + const checknumber = Number.parseInt(compactInsuranceNumber.slice(-1)); - return checksum === checknumber; - } - return false; + return checksum === checknumber; } From 761cb3d22186889381591b36d1918c8e19405a7e Mon Sep 17 00:00:00 2001 From: Dominic Baur Date: Wed, 27 Aug 2025 16:47:46 +0200 Subject: [PATCH 07/18] Changed AHV number in test --- src/lib/string.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/string.spec.ts b/src/lib/string.spec.ts index d6d296c..6551f63 100644 --- a/src/lib/string.spec.ts +++ b/src/lib/string.spec.ts @@ -126,8 +126,8 @@ describe("string tests", () => { [undefined as unknown as string, false], ["7569217076985", true], ["756.9217.0769.85", true], - ["756.1234.5678.99", false], - ["7561234567899", false], + ["756.1234.5678.91", false], + ["7561234567891", false], ["7000000000000", false], ])("check if ahv number is valid or not", (ahvNumber, expected) => { expect(isValidSwissSocialSecurityNumber(ahvNumber)).toBe(expected); From 7762b6c3453bb29c1416cd4837a58ab88495a9d3 Mon Sep 17 00:00:00 2001 From: Dominic Baur Date: Wed, 27 Aug 2025 17:04:26 +0200 Subject: [PATCH 08/18] Fix --- 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 f09a296..3d6dd4f 100644 --- a/src/lib/string.ts +++ b/src/lib/string.ts @@ -80,7 +80,7 @@ export function isValidSwissSocialSecurityNumber(socialInsuranceNumber: string): return false; } const number = compactInsuranceNumber.slice(0, -1); - const reversedNumber = [...number.split(".").join("")].reverse().join(""); + const reversedNumber = [...number].reverse().join(""); const reversedNumberArray = [...reversedNumber]; let sum = 0; for (const [i, element] of reversedNumberArray.entries()) { From 0df21661a6c3435d6aa462d8c9444da59f9eff72 Mon Sep 17 00:00:00 2001 From: Dominic Baur Date: Thu, 28 Aug 2025 07:40:21 +0200 Subject: [PATCH 09/18] Changed to is null or whitespace check --- src/lib/string.spec.ts | 2 ++ src/lib/string.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lib/string.spec.ts b/src/lib/string.spec.ts index 6551f63..6495401 100644 --- a/src/lib/string.spec.ts +++ b/src/lib/string.spec.ts @@ -127,6 +127,8 @@ describe("string tests", () => { ["7569217076985", true], ["756.9217.0769.85", true], ["756.1234.5678.91", false], + ["7 5......69 2...1707......69.85", true], + ["7 5......61 2...3456......7......89 1", false], ["7561234567891", false], ["7000000000000", false], ])("check if ahv number is valid or not", (ahvNumber, expected) => { diff --git a/src/lib/string.ts b/src/lib/string.ts index 3d6dd4f..415cb44 100644 --- a/src/lib/string.ts +++ b/src/lib/string.ts @@ -72,7 +72,7 @@ export function truncate(value: string | undefined, maxLength: number, suffix = * @returns The result if the social insurance number is valid or not */ export function isValidSwissSocialSecurityNumber(socialInsuranceNumber: string): boolean { - if (isNullOrEmpty(socialInsuranceNumber)) { + if (isNullOrWhitespace(socialInsuranceNumber)) { return false; } const compactInsuranceNumber = socialInsuranceNumber.replaceAll(/[\s.]+/g, ""); From 84ccdff11dfd58661bd032325f58667e51fc2955 Mon Sep 17 00:00:00 2001 From: Dominic Baur Date: Thu, 28 Aug 2025 08:38:38 +0200 Subject: [PATCH 10/18] Add IBAN to check now must be in one of two valid formats --- src/lib/string.spec.ts | 8 ++++---- src/lib/string.ts | 25 +++++++++++++++++-------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/lib/string.spec.ts b/src/lib/string.spec.ts index 6495401..b96dee7 100644 --- a/src/lib/string.spec.ts +++ b/src/lib/string.spec.ts @@ -125,13 +125,13 @@ describe("string tests", () => { [null as unknown as string, false], [undefined as unknown as string, false], ["7569217076985", true], + ["7561234567891", false], ["756.9217.0769.85", true], ["756.1234.5678.91", false], - ["7 5......69 2...1707......69.85", true], + ["756.1234.5678.91", false], + ["7 5......69 2...1707......69.85", false], ["7 5......61 2...3456......7......89 1", false], - ["7561234567891", false], - ["7000000000000", false], - ])("check if ahv number is valid or not", (ahvNumber, expected) => { + ])("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 415cb44..8bd96c5 100644 --- a/src/lib/string.ts +++ b/src/lib/string.ts @@ -66,29 +66,38 @@ export function truncate(value: string | undefined, maxLength: number, suffix = } /** - * Validation of Social insurance number with checking the checksum + * 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 seperators + * - "756XXXXXXXXXX" with digits only * @returns The result if the social insurance number is valid or not */ export function isValidSwissSocialSecurityNumber(socialInsuranceNumber: string): boolean { if (isNullOrWhitespace(socialInsuranceNumber)) { return false; } - const compactInsuranceNumber = socialInsuranceNumber.replaceAll(/[\s.]+/g, ""); - if (!/^756\d{10}$/.test(compactInsuranceNumber)) { + + const socialInsuranceNumberWithDots = new RegExp(/[7][5][6][.][\d]{4}[.][\d]{4}[.][\d]{2}$/); + const socialInsuranceNumberWithoutDots = new RegExp(/^756\d{10}$/); + + if (!socialInsuranceNumberWithDots.test(socialInsuranceNumber) && !socialInsuranceNumberWithoutDots.test(socialInsuranceNumber)) { return false; } - const number = compactInsuranceNumber.slice(0, -1); - const reversedNumber = [...number].reverse().join(""); - const reversedNumberArray = [...reversedNumber]; + + const compactNumber = socialInsuranceNumber.replaceAll(/[\s.]+/g, ""); + const digits = compactNumber.slice(0, -1); + const reversedDigits = [...digits].reverse().join(""); + const reversedDigitsArray = [...reversedDigits]; + let sum = 0; - for (const [i, element] of reversedNumberArray.entries()) { + for (const [i, element] of reversedDigitsArray.entries()) { sum += i % 2 === 0 ? Number(element) * 3 : Number(element) * 1; } const checksum = (10 - (sum % 10)) % 10; - const checknumber = Number.parseInt(compactInsuranceNumber.slice(-1)); + const checknumber = Number.parseInt(compactNumber.slice(-1)); return checksum === checknumber; } From 211261805b8d779d1d8b20062eed579bcc16e826 Mon Sep 17 00:00:00 2001 From: Dominic Baur Date: Thu, 28 Aug 2025 10:32:46 +0200 Subject: [PATCH 11/18] Add new regexp schema --- src/lib/string.spec.ts | 1 + src/lib/string.ts | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/string.spec.ts b/src/lib/string.spec.ts index b96dee7..0cbeb8f 100644 --- a/src/lib/string.spec.ts +++ b/src/lib/string.spec.ts @@ -127,6 +127,7 @@ describe("string tests", () => { ["7569217076985", true], ["7561234567891", false], ["756.9217.0769.85", true], + ["test756.9217.0769.85", false], ["756.1234.5678.91", false], ["756.1234.5678.91", false], ["7 5......69 2...1707......69.85", false], diff --git a/src/lib/string.ts b/src/lib/string.ts index 8bd96c5..fe57615 100644 --- a/src/lib/string.ts +++ b/src/lib/string.ts @@ -79,13 +79,12 @@ export function isValidSwissSocialSecurityNumber(socialInsuranceNumber: string): return false; } - const socialInsuranceNumberWithDots = new RegExp(/[7][5][6][.][\d]{4}[.][\d]{4}[.][\d]{2}$/); + 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; } - const compactNumber = socialInsuranceNumber.replaceAll(/[\s.]+/g, ""); const digits = compactNumber.slice(0, -1); const reversedDigits = [...digits].reverse().join(""); From c775711e6e495b7eba228f2fd25b88fa66aecd80 Mon Sep 17 00:00:00 2001 From: Dominic Baur Date: Thu, 28 Aug 2025 11:11:56 +0200 Subject: [PATCH 12/18] Add replace only the dot out of the number --- src/lib/string.spec.ts | 9 ++++----- src/lib/string.ts | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/lib/string.spec.ts b/src/lib/string.spec.ts index 0cbeb8f..4a97de1 100644 --- a/src/lib/string.spec.ts +++ b/src/lib/string.spec.ts @@ -124,14 +124,13 @@ describe("string tests", () => { test.each([ [null as unknown as string, false], [undefined as unknown as string, false], - ["7569217076985", true], ["7561234567891", false], + ["7569217076985", true], ["756.9217.0769.85", true], - ["test756.9217.0769.85", false], + ["756..9217.0769.85", false], ["756.1234.5678.91", false], - ["756.1234.5678.91", false], - ["7 5......69 2...1707......69.85", false], - ["7 5......61 2...3456......7......89 1", 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 fe57615..ef02c3a 100644 --- a/src/lib/string.ts +++ b/src/lib/string.ts @@ -85,7 +85,7 @@ export function isValidSwissSocialSecurityNumber(socialInsuranceNumber: string): if (!socialInsuranceNumberWithDots.test(socialInsuranceNumber) && !socialInsuranceNumberWithoutDots.test(socialInsuranceNumber)) { return false; } - const compactNumber = socialInsuranceNumber.replaceAll(/[\s.]+/g, ""); + const compactNumber = socialInsuranceNumber.replaceAll(".", ""); const digits = compactNumber.slice(0, -1); const reversedDigits = [...digits].reverse().join(""); const reversedDigitsArray = [...reversedDigits]; From 90b40970491ea728f88463b511b90f0a0c8893dd Mon Sep 17 00:00:00 2001 From: Dominic Baur Date: Thu, 28 Aug 2025 15:09:07 +0200 Subject: [PATCH 13/18] Add comment how to check the social insurance number --- src/lib/string.ts | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/lib/string.ts b/src/lib/string.ts index ef02c3a..fb72f12 100644 --- a/src/lib/string.ts +++ b/src/lib/string.ts @@ -79,12 +79,29 @@ export function isValidSwissSocialSecurityNumber(socialInsuranceNumber: string): return false; } - const socialInsuranceNumberWithDots = new RegExp(/^756.\d{4}.\d{4}.\d{2}$/); - const socialInsuranceNumberWithoutDots = new RegExp(/^756\d{10}$/); + const socialInsuranceNumberWithDots = new RegExp(/^756.?\d{4}.?\d{4}.?\d{2}$/); - if (!socialInsuranceNumberWithDots.test(socialInsuranceNumber) && !socialInsuranceNumberWithoutDots.test(socialInsuranceNumber)) { + if (!socialInsuranceNumberWithDots.test(socialInsuranceNumber)) { return false; } + + /** + * Validates a Swiss social security number (AHV number). + * + * Validation steps: + * - The number must start with `756`, be 13 digits long, and follow one of the accepted formats: + * - `756.XXXX.XXXX.XX` or `756XXXXXXXXXX`. + * - Remove dots → 13 digits remain. + * - The last digit is the check digit. + * - To calculate the check digit: + * - Take the first 12 digits and reverse them. + * - Multiply digits at even positions by 3, and digits at odd positions by 1. + * - Sum all results. + * - Look at the last digit of the sum (sum % 10). + * - The check digit is the value needed to reach the next multiple of 10. + * - The number is valid if this check digit matches the last digit. + */ + const compactNumber = socialInsuranceNumber.replaceAll(".", ""); const digits = compactNumber.slice(0, -1); const reversedDigits = [...digits].reverse().join(""); From 3f2ee4bd34b1b80fb316edc1c161a7d20ccc2df9 Mon Sep 17 00:00:00 2001 From: Dominic Baur Date: Thu, 28 Aug 2025 15:22:33 +0200 Subject: [PATCH 14/18] Fix spelling --- 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 fb72f12..1ea2863 100644 --- a/src/lib/string.ts +++ b/src/lib/string.ts @@ -89,7 +89,7 @@ export function isValidSwissSocialSecurityNumber(socialInsuranceNumber: string): * Validates a Swiss social security number (AHV number). * * Validation steps: - * - The number must start with `756`, be 13 digits long, and follow one of the accepted formats: + * - The number must start with `756`, be 13 digits long and follow one of the accepted formats: * - `756.XXXX.XXXX.XX` or `756XXXXXXXXXX`. * - Remove dots → 13 digits remain. * - The last digit is the check digit. From 4fda01eebdf3dd45d97840cf0eb3ff980ca435b1 Mon Sep 17 00:00:00 2001 From: Dominic Baur Date: Thu, 28 Aug 2025 15:37:15 +0200 Subject: [PATCH 15/18] Spelling --- src/lib/string.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/string.ts b/src/lib/string.ts index 1ea2863..8dbf1f6 100644 --- a/src/lib/string.ts +++ b/src/lib/string.ts @@ -89,8 +89,8 @@ export function isValidSwissSocialSecurityNumber(socialInsuranceNumber: string): * Validates a Swiss social security number (AHV number). * * Validation steps: - * - The number must start with `756`, be 13 digits long and follow one of the accepted formats: - * - `756.XXXX.XXXX.XX` or `756XXXXXXXXXX`. + * - The number must start with 756, be 13 digits long and follow one of the accepted formats: + * - "756.XXXX.XXXX.XX" or "756XXXXXXXXXX". * - Remove dots → 13 digits remain. * - The last digit is the check digit. * - To calculate the check digit: From 22406ce6437732f69f565c120fda350e80bc25c7 Mon Sep 17 00:00:00 2001 From: "Sandro C." Date: Fri, 29 Aug 2025 14:40:33 +0200 Subject: [PATCH 16/18] 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 8dbf1f6..bfe17ac 100644 --- a/src/lib/string.ts +++ b/src/lib/string.ts @@ -70,7 +70,7 @@ export function truncate(value: string | undefined, maxLength: number, suffix = * 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 seperators + * - "756.XXXX.XXXX.XX" with dots as separators * - "756XXXXXXXXXX" with digits only * @returns The result if the social insurance number is valid or not */ From 46b68b84440f5e7bb6978cf384df78b191303abf Mon Sep 17 00:00:00 2001 From: Daniele Debernardi Date: Mon, 1 Sep 2025 11:04:01 +0200 Subject: [PATCH 17/18] Apply suggestion from @Copilot 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 bfe17ac..61fc9c0 100644 --- a/src/lib/string.ts +++ b/src/lib/string.ts @@ -79,7 +79,7 @@ export function isValidSwissSocialSecurityNumber(socialInsuranceNumber: string): return false; } - const socialInsuranceNumberWithDots = new RegExp(/^756.?\d{4}.?\d{4}.?\d{2}$/); + const socialInsuranceNumberWithDots = new RegExp(/^756\.?\d{4}\.?\d{4}\.?\d{2}$/); if (!socialInsuranceNumberWithDots.test(socialInsuranceNumber)) { return false; From 933ef62400552c343eea0e1e2e76168fed501d2e Mon Sep 17 00:00:00 2001 From: Dominic Baur Date: Mon, 1 Sep 2025 13:03:01 +0200 Subject: [PATCH 18/18] Add two schemas for social insurance number --- src/lib/string.spec.ts | 1 + src/lib/string.ts | 46 +++++++++++++++++++++++++----------------- 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/src/lib/string.spec.ts b/src/lib/string.spec.ts index 4a97de1..f9049f8 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], ["7561234567891", false], ["7569217076985", true], + ["756.92170769.85", false], ["756.9217.0769.85", true], ["756..9217.0769.85", false], ["756.1234.5678.91", false], diff --git a/src/lib/string.ts b/src/lib/string.ts index 61fc9c0..c14768c 100644 --- a/src/lib/string.ts +++ b/src/lib/string.ts @@ -75,45 +75,55 @@ export function truncate(value: string | undefined, maxLength: number, suffix = * @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; } - const socialInsuranceNumberWithDots = new RegExp(/^756\.?\d{4}\.?\d{4}\.?\d{2}$/); + /** + * 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)) { + if (!socialInsuranceNumberWithDots.test(socialInsuranceNumber) && !socialInsuranceNumberWithoutDots.test(socialInsuranceNumber)) { return false; } + // 3. Remove all dots → get a string of 13 digits + const compactNumber = socialInsuranceNumber.replaceAll(".", ""); + /** - * Validates a Swiss social security number (AHV number). - * - * Validation steps: - * - The number must start with 756, be 13 digits long and follow one of the accepted formats: - * - "756.XXXX.XXXX.XX" or "756XXXXXXXXXX". - * - Remove dots → 13 digits remain. - * - The last digit is the check digit. - * - To calculate the check digit: - * - Take the first 12 digits and reverse them. - * - Multiply digits at even positions by 3, and digits at odd positions by 1. - * - Sum all results. - * - Look at the last digit of the sum (sum % 10). - * - The check digit is the value needed to reach the next multiple of 10. - * - The number is valid if this check digit matches the last digit. + * 4. Separate digits for checksum calculation + * - first 12 digits: used to calculate checksum + * - last digit: actual check digit */ - - const compactNumber = socialInsuranceNumber.replaceAll(".", ""); 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; }