Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export * from "./lib/mimeType";
export * from "./lib/number";
export * from "./lib/object";
export * from "./lib/string";
export * from "./lib/swissStandards";
39 changes: 1 addition & 38 deletions src/lib/string.spec.ts
Original file line number Diff line number Diff line change
@@ -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([
Expand Down Expand Up @@ -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);
});
});
108 changes: 0 additions & 108 deletions src/lib/string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
32 changes: 32 additions & 0 deletions src/lib/swissStandards.spec.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
109 changes: 109 additions & 0 deletions src/lib/swissStandards.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Loading