Skip to content

Commit 6906e9a

Browse files
authored
Merge branch 'main' into feature/trim(29)
2 parents 874b0be + e7ff99c commit 6906e9a

File tree

4 files changed

+155
-3
lines changed

4 files changed

+155
-3
lines changed

CHANGELOG.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@ 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+
## [2.1.0] - 2025-09-03
15+
16+
### Added
17+
18+
- `isValidSwissIbanNumber` string utility function
19+
- `isValidSwissSocialSecurityNumber` string utility function
20+
1421
## [2.0.0] - 2025-07-29
1522

1623
### Added
@@ -169,7 +176,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
169176
- added `eslint-plugin-jsdoc` to lint jsdoc comments
170177
- `getEnumNameFromValue`,`getEnumValueFromName`, `getEnumNames`,`getEnumNameValues` functions to respectively get the name of an enum from its value, the value from its name, all the names and all the values
171178

172-
[unreleased]: https://github.com/neolution-ch/javascript-utils/compare/2.0.0...HEAD
179+
[unreleased]: https://github.com/neolution-ch/javascript-utils/compare/2.1.0...HEAD
180+
[2.1.0]: https://github.com/neolution-ch/javascript-utils/compare/2.0.0...2.1.0
173181
[2.0.0]: https://github.com/neolution-ch/javascript-utils/compare/1.5.0...2.0.0
174182
[1.5.0]: https://github.com/neolution-ch/javascript-utils/compare/1.4.0...1.5.0
175183
[1.4.0]: https://github.com/neolution-ch/javascript-utils/compare/1.3.1...1.4.0

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@neolution-ch/javascript-utils",
3-
"version": "2.0.0",
3+
"version": "2.1.0",
44
"description": "This is a collection of utilities that we have created to help with our development process.",
55
"homepage": "https://neolution-ch.github.io/javascript-utils",
66
"repository": {

src/lib/string.spec.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1-
import { isNullOrEmpty, isNullOrWhitespace, capitalize, uncapitalize, truncate, ltrim, rtrim, trim } from "./string";
1+
import {
2+
isNullOrEmpty,
3+
isNullOrWhitespace,
4+
capitalize,
5+
uncapitalize,
6+
truncate,
7+
isValidSwissIbanNumber,
8+
isValidSwissSocialSecurityNumber,
9+
} from "./string";
210

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

src/lib/string.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,111 @@ 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+
}

0 commit comments

Comments
 (0)