forked from neolution-ch/javascript-utils
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathswissStandards.ts
More file actions
132 lines (113 loc) · 4.82 KB
/
swissStandards.ts
File metadata and controls
132 lines (113 loc) · 4.82 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
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[0-9]{2} [0-9]{4} [0-9][A-Z0-9]{3} [A-Z0-9]{4} [A-Z0-9]{4} [A-Z0-9]$/);
// - without spaces: "CHXXXXXXXXXXXXXXXXXXX"
const compactIbanNumberWithoutWhiteSpaces = new RegExp(/^CH[0-9]{7}[A-Z0-9]{12}$/);
// 3. Check if the 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 a 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 the input is empty or only a whitespace
if (isNullOrWhitespace(socialInsuranceNumber)) {
return false;
}
/**
* 2. Check if the input matches one of the 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;
}
/**
* Attempts to parse and validate a Swiss IBAN.
* @param unformattedIbanNumber - The unformatted IBAN as a string
* @returns The result object with the following properties:
* @property {boolean} isValid - Indicates whether the IBAN is valid or not
* @property {string} iban - The cleaned IBAN, only present if valid
* @property {string} ibanFormatted - The formatted IBAN, only present if valid
*/
export function tryParseSwissIbanNumber(unformattedIbanNumber?: string) {
if (isNullOrWhitespace(unformattedIbanNumber)) {
return { isValid: false };
}
const iban = unformattedIbanNumber!.replaceAll(/[^A-Z0-9]/gi, "").toUpperCase();
const isValid = isValidSwissIbanNumber(iban);
return {
isValid: isValid,
iban: isValid ? iban : undefined,
ibanFormatted: isValid ? iban.match(/.{1,4}/g)?.join(" ") : undefined,
};
}