Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 15 additions & 1 deletion src/lib/string.spec.ts
Original file line number Diff line number Diff line change
@@ -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([
Expand Down Expand Up @@ -120,4 +120,18 @@ 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],
["7561234567891", false],
["7569217076985", true],
["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);
});
});
53 changes: 53 additions & 0 deletions src/lib/string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,56 @@ export function truncate(value: string | undefined, maxLength: number, suffix =

return `${value.slice(0, maxLength)}${suffix}`;
}

/**
* 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
Comment thread
neoscie marked this conversation as resolved.
Outdated
* @returns The result if the social insurance number is valid or not
*/
export function isValidSwissSocialSecurityNumber(socialInsuranceNumber: string): boolean {
if (isNullOrWhitespace(socialInsuranceNumber)) {
return false;
}

const socialInsuranceNumberWithDots = new RegExp(/^756.?\d{4}.?\d{4}.?\d{2}$/);
Comment thread
drebrez marked this conversation as resolved.
Outdated

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(".", "");
Comment thread
fwermelinger marked this conversation as resolved.
const digits = compactNumber.slice(0, -1);
const reversedDigits = [...digits].reverse().join("");
const reversedDigitsArray = [...reversedDigits];

let sum = 0;
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(compactNumber.slice(-1));

return checksum === checknumber;
}
Comment thread
dom-baur marked this conversation as resolved.
Loading