Skip to content
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- `isValidSwissIbanNumber` string utility function
- `isValidSwissSocialSecurityNumber` string utility function

## [2.0.0] - 2025-07-29
Expand Down
24 changes: 23 additions & 1 deletion src/lib/string.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { isNullOrEmpty, isNullOrWhitespace, capitalize, uncapitalize, truncate, isValidSwissSocialSecurityNumber } from "./string";
import {
isNullOrEmpty,
isNullOrWhitespace,
capitalize,
uncapitalize,
truncate,
isValidSwissIbanNumber,
isValidSwissSocialSecurityNumber,
} from "./string";

describe("string tests", () => {
test.each([
Expand Down Expand Up @@ -121,6 +129,20 @@ describe("string tests", () => {
expect(truncate(value, maxLength)).toBe(expected);
});

test.each([
[null as unknown as string, false],
[undefined as unknown as string, false],
["CH9300762011623852957", true],
["CH93 0076 2011 6238 5295 7", true],
["CH930076 20116238 5295 7", false],
["CH93-0076-2011-6238-5295-7", false],
["CH93 0000 0000 0000 0000 1", false],
["ch93 0076 2011 6238 5295 7", false],
["DE93 0076 2011 6238 5295 7", false],
])("check if this swiss IBAN is valid or not", (unformattedIbanNumber, expected) => {
expect(isValidSwissIbanNumber(unformattedIbanNumber)).toBe(expected);
});

test.each([
[null as unknown as string, false],
[undefined as unknown as string, false],
Expand Down
45 changes: 45 additions & 0 deletions src/lib/string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,51 @@ export function truncate(value: string | undefined, maxLength: number, suffix =
return `${value.slice(0, maxLength)}${suffix}`;
}

/**
* 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\d{2}(?: \d{4}){4} \d{1}$/);
// - without spaces: "CHXXXXXXXXXXXXXXXXXXX"
const compactIbanNumberWithoutWhiteSpaces = new RegExp(/^CH\d{19}$/);

// 3. Check if 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 social insurance number with checking the checksum
* Validation according to https://www.sozialversicherungsnummer.ch/aufbau-neu.htm
Expand Down
Loading