Skip to content

Commit 7b07249

Browse files
committed
Merge branch 'main' of https://github.com/dom-baur/javascript-utils into feature/formatSwissSocialInsuranceNumber
2 parents 15b1c43 + bc3dd52 commit 7b07249

5 files changed

Lines changed: 184 additions & 12 deletions

File tree

CHANGELOG.md

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

1212
- `formatSwissSocialInsuranceNumber` swiss standard function
13+
- `splitLines` string utility function
14+
- `trimStart`, `trimEnd` and `trim` string type utility functions
1315

1416
### Changed
1517

@@ -18,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1820
### Fixed
1921

2022
- `isValidSwissSocialInsuranceNumber` is now named properly
23+
- `isValidSwissIbanNumber` now also allows IBAN numbers with letters
2124

2225
## [2.1.0] - 2025-09-03
2326

src/lib/string.spec.ts

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { isNullOrEmpty, isNullOrWhitespace, capitalize, uncapitalize, truncate } from "./string";
1+
import { isNullOrEmpty, isNullOrWhitespace, capitalize, uncapitalize, truncate, splitLines, trimStart, trimEnd, trim } from "./string";
22

33
describe("string tests", () => {
44
test.each([
@@ -120,4 +120,77 @@ describe("string tests", () => {
120120
])("truncate without suffix parameter", (value, maxLength, expected) => {
121121
expect(truncate(value, maxLength)).toBe(expected);
122122
});
123+
124+
test.each([
125+
[null as unknown as string, " ", null as unknown as string],
126+
[undefined as unknown as string, " ", undefined as unknown as string],
127+
["hello world", "hello world", ""],
128+
["hello world", " ", "hello world"],
129+
[" hello world", " ", "hello world"],
130+
["hello world hello world", "hello world", " hello world"],
131+
["hello worldhello world", "hello world", ""],
132+
])("left trim", (haystack, needle, expected) => {
133+
expect(trimStart(haystack, needle)).toBe(expected);
134+
});
135+
136+
test.each([
137+
[null as unknown as string, " ", null as unknown as string],
138+
[undefined as unknown as string, " ", undefined as unknown as string],
139+
["hello world", "hello world", ""],
140+
["hello world ", " ", "hello world"],
141+
["hello world", " ", "hello world"],
142+
["hello world hello world", "hello world", "hello world "],
143+
["hello worldhello world", "hello world", ""],
144+
])("right trim", (haystack, needle, expected) => {
145+
expect(trimEnd(haystack, needle)).toBe(expected);
146+
});
147+
148+
test.each([
149+
[null as unknown as string, " ", null as unknown as string],
150+
[undefined as unknown as string, " ", undefined as unknown as string],
151+
["hello world", "", "hello world"],
152+
[" hello world ", " ", "hello world"],
153+
["hello world ", " ", "hello world"],
154+
[" hello world", " ", "hello world"],
155+
["hello worldhello world", "hello world", ""],
156+
])("trim", (haystack, needle, expected) => {
157+
expect(trim(haystack, needle)).toBe(expected);
158+
});
159+
160+
test.each([
161+
["", false, false, []],
162+
[null as unknown as string, false, false, []],
163+
[undefined as unknown as string, false, false, []],
164+
[" aaaa \n\nbbbb \n \ncccc", false, false, [" aaaa ", "", "bbbb ", " ", "cccc"]],
165+
[" aaaa \r\rbbbb \r \rcccc", false, false, [" aaaa ", "", "bbbb ", " ", "cccc"]],
166+
[" aaaa \r\rbbbb \n \r\ncccc", false, false, [" aaaa ", "", "bbbb ", " ", "cccc"]],
167+
[" aaaa \r\n\r\nbbbb \r\n \r\ncccc", false, false, [" aaaa ", "", "bbbb ", " ", "cccc"]],
168+
169+
[" aaaa \n\nbbbb \n \ncccc", true, false, [" aaaa ", "bbbb ", " ", "cccc"]],
170+
[" aaaa \r\rbbbb \r \rcccc", true, false, [" aaaa ", "bbbb ", " ", "cccc"]],
171+
[" aaaa \r\rbbbb \n \r\ncccc", true, false, [" aaaa ", "bbbb ", " ", "cccc"]],
172+
[" aaaa \r\n\r\nbbbb \r\n \r\ncccc", true, false, [" aaaa ", "bbbb ", " ", "cccc"]],
173+
174+
[" aaaa \n\nbbbb \n \ncccc", false, true, ["aaaa", "", "bbbb", "", "cccc"]],
175+
[" aaaa \r\rbbbb \r \rcccc", false, true, ["aaaa", "", "bbbb", "", "cccc"]],
176+
[" aaaa \r\rbbbb \n \r\ncccc", false, true, ["aaaa", "", "bbbb", "", "cccc"]],
177+
[" aaaa \r\n\r\nbbbb \r\n \r\ncccc", false, true, ["aaaa", "", "bbbb", "", "cccc"]],
178+
179+
[" aaaa \n\nbbbb \n \ncccc", true, true, ["aaaa", "bbbb", "cccc"]],
180+
[" aaaa \r\rbbbb \r \rcccc", true, true, ["aaaa", "bbbb", "cccc"]],
181+
[" aaaa \r\rbbbb \n \r\ncccc", true, true, ["aaaa", "bbbb", "cccc"]],
182+
])("splitLines with parameter removeEmptyEntries and trimEntries", (str, removeEmptyEntries, trimEntries, expected) => {
183+
expect(splitLines(str, removeEmptyEntries, trimEntries)).toEqual(expected);
184+
});
185+
186+
test.each([
187+
["", []],
188+
[null as unknown as string, []],
189+
[undefined as unknown as string, []],
190+
[" aaaa \n\nbbbb \n \ncccc", [" aaaa ", "", "bbbb ", " ", "cccc"]],
191+
[" aaaa \r\rbbbb \r \rcccc", [" aaaa ", "", "bbbb ", " ", "cccc"]],
192+
[" aaaa \r\n\nbbbb \r\n \r\ncccc", [" aaaa ", "", "bbbb ", " ", "cccc"]],
193+
])("splitLines with just the string as a parameter", (str, expected) => {
194+
expect(splitLines(str)).toEqual(expected);
195+
});
123196
});

src/lib/string.ts

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

6565
return `${value.slice(0, maxLength)}${suffix}`;
6666
}
67+
68+
/**
69+
* Removes all occurrences of needle from the start of haystack
70+
* @param haystack string to trim
71+
* @param needle the thing to trim
72+
* @returns the string trimmed from the left side
73+
*/
74+
export function trimStart(haystack: string, needle: string): string {
75+
if (isNullOrEmpty(haystack) || isNullOrEmpty(needle)) {
76+
return haystack;
77+
}
78+
79+
let offset = 0;
80+
81+
while (haystack.indexOf(needle, offset) === offset) {
82+
offset = offset + needle.length;
83+
}
84+
return haystack.slice(offset);
85+
}
86+
87+
/**
88+
* Removes all occurrences of needle from the end of haystack
89+
* @param haystack string to trim
90+
* @param needle the thing to trim
91+
* @returns the string trimmed from the right side
92+
*/
93+
export function trimEnd(haystack: string, needle: string): string {
94+
if (isNullOrEmpty(haystack) || isNullOrEmpty(needle)) {
95+
return haystack;
96+
}
97+
98+
let offset = haystack.length,
99+
idx = -1;
100+
101+
while (true) {
102+
idx = haystack.lastIndexOf(needle, offset - 1);
103+
if (idx === -1 || idx + needle.length !== offset) {
104+
break;
105+
}
106+
if (idx === 0) {
107+
return "";
108+
}
109+
offset = idx;
110+
}
111+
112+
return haystack.slice(0, offset);
113+
}
114+
115+
/**
116+
* Removes all occurrences of needle from the start and the end of haystack
117+
* @param haystack string to trim
118+
* @param needle the thing to trim
119+
* @returns the string trimmed from the right and left side
120+
*/
121+
export function trim(haystack: string, needle: string): string {
122+
const trimmed = trimStart(haystack, needle);
123+
return trimEnd(trimmed, needle);
124+
}
125+
126+
/**
127+
* Splits the string at line breaks
128+
* @param str the string to split
129+
* @param removeEmptyEntries the option to remove empty entries
130+
* @param trimEntries the option to trim the entries
131+
* @returns the individual lines as an array
132+
*/
133+
export function splitLines(str: string, removeEmptyEntries: boolean = false, trimEntries: boolean = false): string[] {
134+
if (isNullOrEmpty(str)) {
135+
return [];
136+
}
137+
138+
let splitted = str.split(/\r\n|\r|\n/);
139+
140+
if (trimEntries) {
141+
splitted = splitted.map((x) => x.trim());
142+
}
143+
144+
if (removeEmptyEntries) {
145+
splitted = splitted.filter((line) => line.length > 0);
146+
}
147+
148+
return splitted;
149+
}

src/lib/swissStandards.spec.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,21 @@ describe("Swiss standards test", () => {
1111
["CH93 0000 0000 0000 0000 1", false],
1212
["ch93 0076 2011 6238 5295 7", false],
1313
["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);
14+
])("check if the swiss IBAN number is valid or not", (iBanNumberToCheck, expected) => {
15+
expect(isValidSwissIbanNumber(iBanNumberToCheck)).toBe(expected);
16+
});
17+
18+
test.each([
19+
[null as unknown as string, false],
20+
[undefined as unknown as string, false],
21+
["CH3400762ABC123DEF456", true],
22+
["CH34 0076 2ABC 123D EF45 6", true],
23+
["Some random string", false],
24+
["DE34 0076 2ABC 123D EF45 3", false],
25+
["CH34 0076 2ABC 123D EF45 \n6", false],
26+
["CH34 0076 2ABC 123D EF45 !", false],
27+
])("check if the siwss IBAN number with letters is valid or not", (iBanNumberToCheck, expected) => {
28+
expect(isValidSwissIbanNumber(iBanNumberToCheck)).toBe(expected);
1629
});
1730

1831
test.each([
@@ -26,8 +39,8 @@ describe("Swiss standards test", () => {
2639
["756.1234.5678.91", false],
2740
["test756.9217.0769.85", false],
2841
["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);
42+
])("check if the social insurance number is valid or not", (socialInsuranceNumberToCheck, expected) => {
43+
expect(isValidSwissSocialInsuranceNumber(socialInsuranceNumberToCheck)).toBe(expected);
3144
});
3245

3346
test.each([

src/lib/swissStandards.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@ export function isValidSwissIbanNumber(ibanNumber: string): boolean {
1616

1717
// 2. Define allowed strict formats
1818
// - with spaces: "CHXX XXXX XXXX XXXX XXXX X"
19-
const compactIbanNumberWithWhiteSpaces = new RegExp(/^CH\d{2}(?: \d{4}){4} \d{1}$/);
19+
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]$/);
2020
// - without spaces: "CHXXXXXXXXXXXXXXXXXXX"
21-
const compactIbanNumberWithoutWhiteSpaces = new RegExp(/^CH\d{19}$/);
21+
const compactIbanNumberWithoutWhiteSpaces = new RegExp(/^CH[0-9]{7}[A-Z0-9]{12}$/);
2222

23-
// 3. Check if input matches one of the allowed formats
24-
if (!compactIbanNumberWithWhiteSpaces.test(ibanNumber) && !compactIbanNumberWithoutWhiteSpaces.test(ibanNumber)) {
23+
// 3. Check if the input matches one of the allowed formats
24+
if (!(compactIbanNumberWithWhiteSpaces.test(ibanNumber) || compactIbanNumberWithoutWhiteSpaces.test(ibanNumber))) {
2525
return false;
2626
}
2727

@@ -46,7 +46,7 @@ export function isValidSwissIbanNumber(ibanNumber: string): boolean {
4646
}
4747

4848
/**
49-
* Validation of social insurance number with checking the checksum
49+
* Validation of a social insurance number with checking the checksum
5050
* Validation according to https://www.sozialversicherungsnummer.ch/aufbau-neu.htm
5151
* @param socialInsuranceNumber The social insurance number to check
5252
* Must be in one of the following formats:
@@ -55,13 +55,13 @@ export function isValidSwissIbanNumber(ibanNumber: string): boolean {
5555
* @returns The result if the social insurance number is valid or not
5656
*/
5757
export function isValidSwissSocialInsuranceNumber(socialInsuranceNumber: string): boolean {
58-
// 1. Check if input is empty or only whitespace
58+
// 1. Check if the input is empty or only a whitespace
5959
if (isNullOrWhitespace(socialInsuranceNumber)) {
6060
return false;
6161
}
6262

6363
/**
64-
* 2. Check if input matches accepted formats:
64+
* 2. Check if the input matches one of the accepted formats:
6565
* - With dots: 756.XXXX.XXXX.XX
6666
* - Without dots: 756XXXXXXXXXX
6767
*/

0 commit comments

Comments
 (0)