Skip to content

Commit eed7908

Browse files
authored
Merge branch 'main' into feature/splitline
2 parents fc0e2b3 + 1eb6e73 commit eed7908

File tree

6 files changed

+151
-147
lines changed

6 files changed

+151
-147
lines changed

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
- `splitLine` string utility function
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: 1 addition & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,4 @@
1-
import {
2-
isNullOrEmpty,
3-
isNullOrWhitespace,
4-
capitalize,
5-
uncapitalize,
6-
truncate,
7-
splitLine,
8-
isValidSwissIbanNumber,
9-
isValidSwissSocialSecurityNumber,
10-
} from "./string";
1+
import { isNullOrEmpty, isNullOrWhitespace, capitalize, uncapitalize, truncate, splitLine } from "./string";
112

123
describe("string tests", () => {
134
test.each([
@@ -158,33 +149,4 @@ describe("string tests", () => {
158149
])("splitLine with the parameter to remove empty entries", (str, expected) => {
159150
expect(splitLine(str, true)).toEqual(expected);
160151
});
161-
162-
test.each([
163-
[null as unknown as string, false],
164-
[undefined as unknown as string, false],
165-
["CH9300762011623852957", true],
166-
["CH93 0076 2011 6238 5295 7", true],
167-
["CH930076 20116238 5295 7", false],
168-
["CH93-0076-2011-6238-5295-7", false],
169-
["CH93 0000 0000 0000 0000 1", false],
170-
["ch93 0076 2011 6238 5295 7", false],
171-
["DE93 0076 2011 6238 5295 7", false],
172-
])("check if this swiss IBAN is valid or not", (unformattedIbanNumber, expected) => {
173-
expect(isValidSwissIbanNumber(unformattedIbanNumber)).toBe(expected);
174-
});
175-
176-
test.each([
177-
[null as unknown as string, false],
178-
[undefined as unknown as string, false],
179-
["7561234567891", false],
180-
["7569217076985", true],
181-
["756.92170769.85", false],
182-
["756.9217.0769.85", true],
183-
["756..9217.0769.85", false],
184-
["756.1234.5678.91", false],
185-
["test756.9217.0769.85", false],
186-
["7.56..9217...0769.85", false],
187-
])("check if the social insurance number is valid or not", (ahvNumber, expected) => {
188-
expect(isValidSwissSocialSecurityNumber(ahvNumber)).toBe(expected);
189-
});
190152
});

src/lib/string.ts

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

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)