Skip to content

Commit 4976301

Browse files
authored
Merge branch 'main' into feature/trim(29)
2 parents d9b461d + 1eb6e73 commit 4976301

6 files changed

Lines changed: 150 additions & 139 deletions

File tree

CHANGELOG.md

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

1212
- `ltrim`, `rtrim` and `trim` string type utility functions
1313

14+
### Changed
15+
16+
- Moved `isValidSwissIbanNumber` and `isValidSwissSocialInsuranceNumber` to swissStandards
17+
18+
### Fixed
19+
20+
- `isValidSwissSocialInsuranceNumber` is now named properly
21+
1422
## [2.1.0] - 2025-09-03
1523

1624
### 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: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ import {
77
ltrim,
88
rtrim,
99
trim,
10-
isValidSwissIbanNumber,
11-
isValidSwissSocialSecurityNumber,
1210
} from "./string";
1311

1412
describe("string tests", () => {
@@ -160,33 +158,4 @@ describe("string tests", () => {
160158
])("trim", (haystack, needle, expected) => {
161159
expect(trim(haystack, needle)).toBe(expected);
162160
});
163-
164-
test.each([
165-
[null as unknown as string, false],
166-
[undefined as unknown as string, false],
167-
["CH9300762011623852957", true],
168-
["CH93 0076 2011 6238 5295 7", true],
169-
["CH930076 20116238 5295 7", false],
170-
["CH93-0076-2011-6238-5295-7", false],
171-
["CH93 0000 0000 0000 0000 1", false],
172-
["ch93 0076 2011 6238 5295 7", false],
173-
["DE93 0076 2011 6238 5295 7", false],
174-
])("check if this swiss IBAN is valid or not", (unformattedIbanNumber, expected) => {
175-
expect(isValidSwissIbanNumber(unformattedIbanNumber)).toBe(expected);
176-
});
177-
178-
test.each([
179-
[null as unknown as string, false],
180-
[undefined as unknown as string, false],
181-
["7561234567891", false],
182-
["7569217076985", true],
183-
["756.92170769.85", false],
184-
["756.9217.0769.85", true],
185-
["756..9217.0769.85", false],
186-
["756.1234.5678.91", false],
187-
["test756.9217.0769.85", false],
188-
["7.56..9217...0769.85", false],
189-
])("check if the social insurance number is valid or not", (ahvNumber, expected) => {
190-
expect(isValidSwissSocialSecurityNumber(ahvNumber)).toBe(expected);
191-
});
192161
});

src/lib/string.ts

Lines changed: 0 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -127,111 +127,3 @@ export function trim(haystack: string, needle: string): string {
127127
const trimmed = ltrim(haystack, needle);
128128
return rtrim(trimmed, needle);
129129
}
130-
131-
/**
132-
* Checks if the provided string is a valid swiss IBAN number
133-
* @param ibanNumber The provided IBAN number to check
134-
* Must be in one of the following formats:
135-
* - "CHXX XXXX XXXX XXXX XXXX X" with whitespaces
136-
* - "CHXXXXXXXXXXXXXXXXXXX" without whitespaces
137-
* @returns The result of the IBAN number check
138-
*/
139-
export function isValidSwissIbanNumber(ibanNumber: string): boolean {
140-
// 1. Reject null, undefined or whitespace-only strings
141-
if (isNullOrWhitespace(ibanNumber)) {
142-
return false;
143-
}
144-
145-
// 2. Define allowed strict formats
146-
// - with spaces: "CHXX XXXX XXXX XXXX XXXX X"
147-
const compactIbanNumberWithWhiteSpaces = new RegExp(/^CH\d{2}(?: \d{4}){4} \d{1}$/);
148-
// - without spaces: "CHXXXXXXXXXXXXXXXXXXX"
149-
const compactIbanNumberWithoutWhiteSpaces = new RegExp(/^CH\d{19}$/);
150-
151-
// 3. Check if input matches one of the allowed formats
152-
if (!compactIbanNumberWithWhiteSpaces.test(ibanNumber) && !compactIbanNumberWithoutWhiteSpaces.test(ibanNumber)) {
153-
return false;
154-
}
155-
156-
// 4. Remove all spaces to get a compact IBAN string
157-
const compactIbanNumber = ibanNumber.replaceAll(" ", "");
158-
159-
// 5. Rearrange IBAN for checksum calculation
160-
// - move first 4 characters (CH + 2 check digits) to the end
161-
const rearrangedIban = compactIbanNumber.slice(4) + compactIbanNumber.slice(0, 4);
162-
163-
// 6. Replace letters with numbers (A=10, B=11, ..., Z=35)
164-
const numericStr = rearrangedIban.replaceAll(/[A-Z]/g, (ch) => (ch.codePointAt(0)! - 55).toString());
165-
166-
// 7. Perform modulo 97 calculation to validate IBAN
167-
let restOfCalculation = 0;
168-
for (const digit of numericStr) {
169-
restOfCalculation = (restOfCalculation * 10 + Number(digit)) % 97;
170-
}
171-
172-
// 8. IBAN is valid only if the remainder equals 1
173-
return restOfCalculation === 1;
174-
}
175-
176-
/**
177-
* Validation of social insurance number with checking the checksum
178-
* Validation according to https://www.sozialversicherungsnummer.ch/aufbau-neu.htm
179-
* @param socialInsuranceNumber The social insurance number to check
180-
* Must be in one of the following formats:
181-
* - "756.XXXX.XXXX.XX" with dots as separators
182-
* - "756XXXXXXXXXX" with digits only
183-
* @returns The result if the social insurance number is valid or not
184-
*/
185-
export function isValidSwissSocialSecurityNumber(socialInsuranceNumber: string): boolean {
186-
// 1. Check if input is empty or only whitespace
187-
if (isNullOrWhitespace(socialInsuranceNumber)) {
188-
return false;
189-
}
190-
191-
/**
192-
* 2. Check if input matches accepted formats:
193-
* - With dots: 756.XXXX.XXXX.XX
194-
* - Without dots: 756XXXXXXXXXX
195-
*/
196-
const socialInsuranceNumberWithDots = new RegExp(/^756\.\d{4}\.\d{4}\.\d{2}$/);
197-
const socialInsuranceNumberWithoutDots = new RegExp(/^756\d{10}$/);
198-
199-
if (!socialInsuranceNumberWithDots.test(socialInsuranceNumber) && !socialInsuranceNumberWithoutDots.test(socialInsuranceNumber)) {
200-
return false;
201-
}
202-
203-
// 3. Remove all dots → get a string of 13 digits
204-
const compactNumber = socialInsuranceNumber.replaceAll(".", "");
205-
206-
/**
207-
* 4. Separate digits for checksum calculation
208-
* - first 12 digits: used to calculate checksum
209-
* - last digit: actual check digit
210-
*/
211-
const digits = compactNumber.slice(0, -1);
212-
const reversedDigits = [...digits].reverse().join("");
213-
const reversedDigitsArray = [...reversedDigits];
214-
215-
/*
216-
* 5. Calculate weighted sum for checksum
217-
* - Even positions (after reversing) ×3
218-
* - Odd positions ×1
219-
*/
220-
let sum = 0;
221-
for (const [i, element] of reversedDigitsArray.entries()) {
222-
sum += i % 2 === 0 ? Number(element) * 3 : Number(element) * 1;
223-
}
224-
225-
/*
226-
* 6. Calculate expected check digit
227-
* - Check digit = value to reach next multiple of 10
228-
*/
229-
const checksum = (10 - (sum % 10)) % 10;
230-
const checknumber = Number.parseInt(compactNumber.slice(-1));
231-
232-
/*
233-
* 7. Compare calculated check digit with actual last digit
234-
* - If equal → valid AHV number
235-
*/
236-
return checksum === checknumber;
237-
}

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)