Skip to content

Commit 18d0936

Browse files
dom-baurneoscieCopilotdrebrez
authored
Add string utility function: isValidSwissSocialSecurityNumber (#78)
Add string utility function isValidSwissSocialSecurityNumber for issue #77 --------- Co-authored-by: Sandro C. <sc@neolution.ch> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Daniele Debernardi <drebrez@gmail.com>
1 parent 1827861 commit 18d0936

3 files changed

Lines changed: 83 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- `isValidSwissSocialSecurityNumber` string utility function
13+
1014
## [2.0.0] - 2025-07-29
1115

1216
### Added

src/lib/string.spec.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { isNullOrEmpty, isNullOrWhitespace, capitalize, uncapitalize, truncate } from "./string";
1+
import { isNullOrEmpty, isNullOrWhitespace, capitalize, uncapitalize, truncate, isValidSwissSocialSecurityNumber } from "./string";
22

33
describe("string tests", () => {
44
test.each([
@@ -120,4 +120,19 @@ describe("string tests", () => {
120120
])("truncate without suffix parameter", (value, maxLength, expected) => {
121121
expect(truncate(value, maxLength)).toBe(expected);
122122
});
123+
124+
test.each([
125+
[null as unknown as string, false],
126+
[undefined as unknown as string, false],
127+
["7561234567891", false],
128+
["7569217076985", true],
129+
["756.92170769.85", false],
130+
["756.9217.0769.85", true],
131+
["756..9217.0769.85", false],
132+
["756.1234.5678.91", false],
133+
["test756.9217.0769.85", false],
134+
["7.56..9217...0769.85", false],
135+
])("check if the social insurance number is valid or not", (ahvNumber, expected) => {
136+
expect(isValidSwissSocialSecurityNumber(ahvNumber)).toBe(expected);
137+
});
123138
});

src/lib/string.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,66 @@ export function truncate(value: string | undefined, maxLength: number, suffix =
6464

6565
return `${value.slice(0, maxLength)}${suffix}`;
6666
}
67+
68+
/**
69+
* Validation of social insurance number with checking the checksum
70+
* Validation according to https://www.sozialversicherungsnummer.ch/aufbau-neu.htm
71+
* @param socialInsuranceNumber The social insurance number to check
72+
* Must be in one of the following formats:
73+
* - "756.XXXX.XXXX.XX" with dots as separators
74+
* - "756XXXXXXXXXX" with digits only
75+
* @returns The result if the social insurance number is valid or not
76+
*/
77+
export function isValidSwissSocialSecurityNumber(socialInsuranceNumber: string): boolean {
78+
// 1. Check if input is empty or only whitespace
79+
if (isNullOrWhitespace(socialInsuranceNumber)) {
80+
return false;
81+
}
82+
83+
/**
84+
* 2. Check if input matches accepted formats:
85+
* - With dots: 756.XXXX.XXXX.XX
86+
* - Without dots: 756XXXXXXXXXX
87+
*/
88+
const socialInsuranceNumberWithDots = new RegExp(/^756\.\d{4}\.\d{4}\.\d{2}$/);
89+
const socialInsuranceNumberWithoutDots = new RegExp(/^756\d{10}$/);
90+
91+
if (!socialInsuranceNumberWithDots.test(socialInsuranceNumber) && !socialInsuranceNumberWithoutDots.test(socialInsuranceNumber)) {
92+
return false;
93+
}
94+
95+
// 3. Remove all dots → get a string of 13 digits
96+
const compactNumber = socialInsuranceNumber.replaceAll(".", "");
97+
98+
/**
99+
* 4. Separate digits for checksum calculation
100+
* - first 12 digits: used to calculate checksum
101+
* - last digit: actual check digit
102+
*/
103+
const digits = compactNumber.slice(0, -1);
104+
const reversedDigits = [...digits].reverse().join("");
105+
const reversedDigitsArray = [...reversedDigits];
106+
107+
/*
108+
* 5. Calculate weighted sum for checksum
109+
* - Even positions (after reversing) ×3
110+
* - Odd positions ×1
111+
*/
112+
let sum = 0;
113+
for (const [i, element] of reversedDigitsArray.entries()) {
114+
sum += i % 2 === 0 ? Number(element) * 3 : Number(element) * 1;
115+
}
116+
117+
/*
118+
* 6. Calculate expected check digit
119+
* - Check digit = value to reach next multiple of 10
120+
*/
121+
const checksum = (10 - (sum % 10)) % 10;
122+
const checknumber = Number.parseInt(compactNumber.slice(-1));
123+
124+
/*
125+
* 7. Compare calculated check digit with actual last digit
126+
* - If equal → valid AHV number
127+
*/
128+
return checksum === checknumber;
129+
}

0 commit comments

Comments
 (0)