Skip to content

Commit 1eb6e73

Browse files
authored
Moved isValidSwissSocialInsuranceNumber and isValidSwissIbanNumber to swiss standards and fixed function name for insurance number (#82)
Moved isValidSwissSocialInsuranceNumber and isValidSwissIbanNumber to swiss standards and fixed function name for insurance number. Issue #77
1 parent e7ff99c commit 1eb6e73

6 files changed

Lines changed: 151 additions & 146 deletions

File tree

CHANGELOG.md

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

88
## [Unreleased]
99

10+
### Changed
11+
12+
- Moved `isValidSwissIbanNumber` and `isValidSwissSocialInsuranceNumber` to swissStandards
13+
14+
### Fixed
15+
16+
- `isValidSwissSocialInsuranceNumber` is now named properly
17+
1018
## [2.1.0] - 2025-09-03
1119

1220
### Added

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ export * from "./lib/mimeType";
88
export * from "./lib/number";
99
export * from "./lib/object";
1010
export * from "./lib/string";
11+
export * from "./lib/swissStandards";

src/lib/string.spec.ts

Lines changed: 1 addition & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,4 @@
1-
import {
2-
isNullOrEmpty,
3-
isNullOrWhitespace,
4-
capitalize,
5-
uncapitalize,
6-
truncate,
7-
isValidSwissIbanNumber,
8-
isValidSwissSocialSecurityNumber,
9-
} from "./string";
1+
import { isNullOrEmpty, isNullOrWhitespace, capitalize, uncapitalize, truncate } from "./string";
102

113
describe("string tests", () => {
124
test.each([
@@ -128,33 +120,4 @@ describe("string tests", () => {
128120
])("truncate without suffix parameter", (value, maxLength, expected) => {
129121
expect(truncate(value, maxLength)).toBe(expected);
130122
});
131-
132-
test.each([
133-
[null as unknown as string, false],
134-
[undefined as unknown as string, false],
135-
["CH9300762011623852957", true],
136-
["CH93 0076 2011 6238 5295 7", true],
137-
["CH930076 20116238 5295 7", false],
138-
["CH93-0076-2011-6238-5295-7", false],
139-
["CH93 0000 0000 0000 0000 1", false],
140-
["ch93 0076 2011 6238 5295 7", false],
141-
["DE93 0076 2011 6238 5295 7", false],
142-
])("check if this swiss IBAN is valid or not", (unformattedIbanNumber, expected) => {
143-
expect(isValidSwissIbanNumber(unformattedIbanNumber)).toBe(expected);
144-
});
145-
146-
test.each([
147-
[null as unknown as string, false],
148-
[undefined as unknown as string, false],
149-
["7561234567891", false],
150-
["7569217076985", true],
151-
["756.92170769.85", false],
152-
["756.9217.0769.85", true],
153-
["756..9217.0769.85", false],
154-
["756.1234.5678.91", false],
155-
["test756.9217.0769.85", false],
156-
["7.56..9217...0769.85", false],
157-
])("check if the social insurance number is valid or not", (ahvNumber, expected) => {
158-
expect(isValidSwissSocialSecurityNumber(ahvNumber)).toBe(expected);
159-
});
160123
});

src/lib/string.ts

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

6565
return `${value.slice(0, maxLength)}${suffix}`;
6666
}
67-
68-
/**
69-
* Checks if the provided string is a valid swiss IBAN number
70-
* @param ibanNumber The provided IBAN number to check
71-
* Must be in one of the following formats:
72-
* - "CHXX XXXX XXXX XXXX XXXX X" with whitespaces
73-
* - "CHXXXXXXXXXXXXXXXXXXX" without whitespaces
74-
* @returns The result of the IBAN number check
75-
*/
76-
export function isValidSwissIbanNumber(ibanNumber: string): boolean {
77-
// 1. Reject null, undefined or whitespace-only strings
78-
if (isNullOrWhitespace(ibanNumber)) {
79-
return false;
80-
}
81-
82-
// 2. Define allowed strict formats
83-
// - with spaces: "CHXX XXXX XXXX XXXX XXXX X"
84-
const compactIbanNumberWithWhiteSpaces = new RegExp(/^CH\d{2}(?: \d{4}){4} \d{1}$/);
85-
// - without spaces: "CHXXXXXXXXXXXXXXXXXXX"
86-
const compactIbanNumberWithoutWhiteSpaces = new RegExp(/^CH\d{19}$/);
87-
88-
// 3. Check if input matches one of the allowed formats
89-
if (!compactIbanNumberWithWhiteSpaces.test(ibanNumber) && !compactIbanNumberWithoutWhiteSpaces.test(ibanNumber)) {
90-
return false;
91-
}
92-
93-
// 4. Remove all spaces to get a compact IBAN string
94-
const compactIbanNumber = ibanNumber.replaceAll(" ", "");
95-
96-
// 5. Rearrange IBAN for checksum calculation
97-
// - move first 4 characters (CH + 2 check digits) to the end
98-
const rearrangedIban = compactIbanNumber.slice(4) + compactIbanNumber.slice(0, 4);
99-
100-
// 6. Replace letters with numbers (A=10, B=11, ..., Z=35)
101-
const numericStr = rearrangedIban.replaceAll(/[A-Z]/g, (ch) => (ch.codePointAt(0)! - 55).toString());
102-
103-
// 7. Perform modulo 97 calculation to validate IBAN
104-
let restOfCalculation = 0;
105-
for (const digit of numericStr) {
106-
restOfCalculation = (restOfCalculation * 10 + Number(digit)) % 97;
107-
}
108-
109-
// 8. IBAN is valid only if the remainder equals 1
110-
return restOfCalculation === 1;
111-
}
112-
113-
/**
114-
* Validation of social insurance number with checking the checksum
115-
* Validation according to https://www.sozialversicherungsnummer.ch/aufbau-neu.htm
116-
* @param socialInsuranceNumber The social insurance number to check
117-
* Must be in one of the following formats:
118-
* - "756.XXXX.XXXX.XX" with dots as separators
119-
* - "756XXXXXXXXXX" with digits only
120-
* @returns The result if the social insurance number is valid or not
121-
*/
122-
export function isValidSwissSocialSecurityNumber(socialInsuranceNumber: string): boolean {
123-
// 1. Check if input is empty or only whitespace
124-
if (isNullOrWhitespace(socialInsuranceNumber)) {
125-
return false;
126-
}
127-
128-
/**
129-
* 2. Check if input matches accepted formats:
130-
* - With dots: 756.XXXX.XXXX.XX
131-
* - Without dots: 756XXXXXXXXXX
132-
*/
133-
const socialInsuranceNumberWithDots = new RegExp(/^756\.\d{4}\.\d{4}\.\d{2}$/);
134-
const socialInsuranceNumberWithoutDots = new RegExp(/^756\d{10}$/);
135-
136-
if (!socialInsuranceNumberWithDots.test(socialInsuranceNumber) && !socialInsuranceNumberWithoutDots.test(socialInsuranceNumber)) {
137-
return false;
138-
}
139-
140-
// 3. Remove all dots → get a string of 13 digits
141-
const compactNumber = socialInsuranceNumber.replaceAll(".", "");
142-
143-
/**
144-
* 4. Separate digits for checksum calculation
145-
* - first 12 digits: used to calculate checksum
146-
* - last digit: actual check digit
147-
*/
148-
const digits = compactNumber.slice(0, -1);
149-
const reversedDigits = [...digits].reverse().join("");
150-
const reversedDigitsArray = [...reversedDigits];
151-
152-
/*
153-
* 5. Calculate weighted sum for checksum
154-
* - Even positions (after reversing) ×3
155-
* - Odd positions ×1
156-
*/
157-
let sum = 0;
158-
for (const [i, element] of reversedDigitsArray.entries()) {
159-
sum += i % 2 === 0 ? Number(element) * 3 : Number(element) * 1;
160-
}
161-
162-
/*
163-
* 6. Calculate expected check digit
164-
* - Check digit = value to reach next multiple of 10
165-
*/
166-
const checksum = (10 - (sum % 10)) % 10;
167-
const checknumber = Number.parseInt(compactNumber.slice(-1));
168-
169-
/*
170-
* 7. Compare calculated check digit with actual last digit
171-
* - If equal → valid AHV number
172-
*/
173-
return checksum === checknumber;
174-
}

src/lib/swissStandards.spec.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { isValidSwissIbanNumber, isValidSwissSocialInsuranceNumber } from "./swissStandards";
2+
3+
describe("Swiss standards test", () => {
4+
test.each([
5+
[null as unknown as string, false],
6+
[undefined as unknown as string, false],
7+
["CH9300762011623852957", true],
8+
["CH93 0076 2011 6238 5295 7", true],
9+
["CH930076 20116238 5295 7", false],
10+
["CH93-0076-2011-6238-5295-7", false],
11+
["CH93 0000 0000 0000 0000 1", false],
12+
["ch93 0076 2011 6238 5295 7", false],
13+
["DE93 0076 2011 6238 5295 7", false],
14+
])("check if this swiss IBAN is valid or not", (unformattedIbanNumber, expected) => {
15+
expect(isValidSwissIbanNumber(unformattedIbanNumber)).toBe(expected);
16+
});
17+
18+
test.each([
19+
[null as unknown as string, false],
20+
[undefined as unknown as string, false],
21+
["7561234567891", false],
22+
["7569217076985", true],
23+
["756.92170769.85", false],
24+
["756.9217.0769.85", true],
25+
["756..9217.0769.85", false],
26+
["756.1234.5678.91", false],
27+
["test756.9217.0769.85", false],
28+
["7.56..9217...0769.85", false],
29+
])("check if the social insurance number is valid or not", (ahvNumber, expected) => {
30+
expect(isValidSwissSocialInsuranceNumber(ahvNumber)).toBe(expected);
31+
});
32+
});

src/lib/swissStandards.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { isNullOrWhitespace } from "./string";
2+
3+
/**
4+
* Checks if the provided string is a valid swiss IBAN number
5+
* @param ibanNumber The provided IBAN number to check
6+
* Must be in one of the following formats:
7+
* - "CHXX XXXX XXXX XXXX XXXX X" with whitespaces
8+
* - "CHXXXXXXXXXXXXXXXXXXX" without whitespaces
9+
* @returns The result of the IBAN number check
10+
*/
11+
export function isValidSwissIbanNumber(ibanNumber: string): boolean {
12+
// 1. Reject null, undefined or whitespace-only strings
13+
if (isNullOrWhitespace(ibanNumber)) {
14+
return false;
15+
}
16+
17+
// 2. Define allowed strict formats
18+
// - with spaces: "CHXX XXXX XXXX XXXX XXXX X"
19+
const compactIbanNumberWithWhiteSpaces = new RegExp(/^CH\d{2}(?: \d{4}){4} \d{1}$/);
20+
// - without spaces: "CHXXXXXXXXXXXXXXXXXXX"
21+
const compactIbanNumberWithoutWhiteSpaces = new RegExp(/^CH\d{19}$/);
22+
23+
// 3. Check if input matches one of the allowed formats
24+
if (!compactIbanNumberWithWhiteSpaces.test(ibanNumber) && !compactIbanNumberWithoutWhiteSpaces.test(ibanNumber)) {
25+
return false;
26+
}
27+
28+
// 4. Remove all spaces to get a compact IBAN string
29+
const compactIbanNumber = ibanNumber.replaceAll(" ", "");
30+
31+
// 5. Rearrange IBAN for checksum calculation
32+
// - move first 4 characters (CH + 2 check digits) to the end
33+
const rearrangedIban = compactIbanNumber.slice(4) + compactIbanNumber.slice(0, 4);
34+
35+
// 6. Replace letters with numbers (A=10, B=11, ..., Z=35)
36+
const numericStr = rearrangedIban.replaceAll(/[A-Z]/g, (ch) => (ch.codePointAt(0)! - 55).toString());
37+
38+
// 7. Perform modulo 97 calculation to validate IBAN
39+
let restOfCalculation = 0;
40+
for (const digit of numericStr) {
41+
restOfCalculation = (restOfCalculation * 10 + Number(digit)) % 97;
42+
}
43+
44+
// 8. IBAN is valid only if the remainder equals 1
45+
return restOfCalculation === 1;
46+
}
47+
48+
/**
49+
* Validation of social insurance number with checking the checksum
50+
* Validation according to https://www.sozialversicherungsnummer.ch/aufbau-neu.htm
51+
* @param socialInsuranceNumber The social insurance number to check
52+
* Must be in one of the following formats:
53+
* - "756.XXXX.XXXX.XX" with dots as separators
54+
* - "756XXXXXXXXXX" with digits only
55+
* @returns The result if the social insurance number is valid or not
56+
*/
57+
export function isValidSwissSocialInsuranceNumber(socialInsuranceNumber: string): boolean {
58+
// 1. Check if input is empty or only whitespace
59+
if (isNullOrWhitespace(socialInsuranceNumber)) {
60+
return false;
61+
}
62+
63+
/**
64+
* 2. Check if input matches accepted formats:
65+
* - With dots: 756.XXXX.XXXX.XX
66+
* - Without dots: 756XXXXXXXXXX
67+
*/
68+
const socialInsuranceNumberWithDots = new RegExp(/^756\.\d{4}\.\d{4}\.\d{2}$/);
69+
const socialInsuranceNumberWithoutDots = new RegExp(/^756\d{10}$/);
70+
71+
if (!socialInsuranceNumberWithDots.test(socialInsuranceNumber) && !socialInsuranceNumberWithoutDots.test(socialInsuranceNumber)) {
72+
return false;
73+
}
74+
75+
// 3. Remove all dots → get a string of 13 digits
76+
const compactNumber = socialInsuranceNumber.replaceAll(".", "");
77+
78+
/**
79+
* 4. Separate digits for checksum calculation
80+
* - first 12 digits: used to calculate checksum
81+
* - last digit: actual check digit
82+
*/
83+
const digits = compactNumber.slice(0, -1);
84+
const reversedDigits = [...digits].reverse().join("");
85+
const reversedDigitsArray = [...reversedDigits];
86+
87+
/*
88+
* 5. Calculate weighted sum for checksum
89+
* - Even positions (after reversing) ×3
90+
* - Odd positions ×1
91+
*/
92+
let sum = 0;
93+
for (const [i, element] of reversedDigitsArray.entries()) {
94+
sum += i % 2 === 0 ? Number(element) * 3 : Number(element) * 1;
95+
}
96+
97+
/*
98+
* 6. Calculate expected check digit
99+
* - Check digit = value to reach next multiple of 10
100+
*/
101+
const checksum = (10 - (sum % 10)) % 10;
102+
const checknumber = Number.parseInt(compactNumber.slice(-1));
103+
104+
/*
105+
* 7. Compare calculated check digit with actual last digit
106+
* - If equal → valid AHV number
107+
*/
108+
return checksum === checknumber;
109+
}

0 commit comments

Comments
 (0)