Skip to content

Commit cfdea24

Browse files
committed
better mod10
1 parent d038041 commit cfdea24

File tree

4 files changed

+51
-22
lines changed

4 files changed

+51
-22
lines changed

packages/validation/src/no.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ export function validateObosMembershipNumber(
104104
return /^\d{7}$/.test(value);
105105
}
106106

107-
type PersonalIdentityNumberOptions = ValidatorOptions;
107+
type NationalIdentityNumberOptions = ValidatorOptions;
108108

109109
/**
110110
* Validates that the input value is a Norwegian national identity number (fødselsnummer or d-nummer).
@@ -122,7 +122,7 @@ type PersonalIdentityNumberOptions = ValidatorOptions;
122122
*/
123123
export function validateNationalIdentityNumber(
124124
value: string,
125-
options: PersonalIdentityNumberOptions = {},
125+
options: NationalIdentityNumberOptions = {},
126126
): boolean {
127127
if (options.allowFormatting) {
128128
// biome-ignore lint/style/noParameterAssign:

packages/validation/src/se.ts

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,9 @@ export function validateOrganizationNumber(
8484
return /^\d{10}$/.test(value);
8585
}
8686

87-
type PersonalIdentityNumberOptions = ValidatorOptions;
87+
type NationalIdenityNumberOptions = ValidatorOptions & {
88+
format?: ['long', 'short'];
89+
};
8890

8991
/**
9092
* Validates that the input value is a Swedish national identity number (personnummer or samordningsnummer).
@@ -102,31 +104,32 @@ type PersonalIdentityNumberOptions = ValidatorOptions;
102104
*/
103105
export function validateNationalIdentityNumber(
104106
value: string,
105-
options: PersonalIdentityNumberOptions = {},
107+
options: NationalIdenityNumberOptions = {},
106108
): boolean {
107109
if (options.allowFormatting) {
108110
// biome-ignore lint/style/noParameterAssign:
109111
value = stripFormatting(value);
110112
}
111113

112-
const controlDigitCheck = mod10(value);
113-
if (!controlDigitCheck) {
114-
return false;
115-
}
114+
// this allows us to handle both YYYYMMDD and YYMMDD when extracting the date
115+
const offset = value.length === 12 ? 2 : 0;
116116

117-
let add = 0;
118-
if (value.length === 12) {
119-
add = 2;
117+
// when verifying the value, we must always use the short format.
118+
// because the long format would generate a different checksum
119+
const isValid = mod10(offset ? value.substring(2) : value);
120+
if (!isValid) {
121+
return false;
120122
}
121123

122124
// copy/inspiration from NAV https://github.com/navikt/fnrvalidator/blob/77e57f0bc8e3570ddc2f0a94558c58d0f7259fe0/src/validator.ts#L108
123-
let year = Number(value.substring(0, 2 + add));
124-
const month = Number(value.substring(2 + add, 4 + add));
125-
let day = Number(value.substring(4 + add, 6 + add));
125+
let year = Number(value.substring(0, 2 + offset));
126+
const month = Number(value.substring(2 + offset, 4 + offset));
127+
let day = Number(value.substring(4 + offset, 6 + offset));
126128

127129
// 1900 isn't a leap year, but 2000 is. Since JS two digits years to the Date constructor is an offset from the year 1900
128130
// we need to special handle that case. For other cases it doesn't really matter if the year is 1925 or 2025.
129-
if (year === 0) {
131+
// if (options.format === 'short' && year === 0) {
132+
if (value.length === 10 && year === 0) {
130133
year = 2000;
131134
}
132135

packages/validation/src/utils.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,17 @@ export function mod11(value: string, weights: number[]): boolean {
2929

3030
/**
3131
* Also known as Luhn's algorithm.
32-
* Used to validate Swedish national identity numbers.
33-
* See https://no.wikipedia.org/wiki/MOD10
32+
* Used to validate Swedish national identity numbers and Norwegian KID numbers
33+
*
34+
* See https://no.wikipedia.org/wiki/MOD10 and https://sv.wikipedia.org/wiki/Luhn-algoritmen#Kontroll_av_nummer
3435
*/
3536
export function mod10(value: string): boolean {
3637
let sum = 0;
3738

38-
for (let i = 0; i < value.length; ++i) {
39-
const weight = 2 - (i % 2);
40-
39+
let weight = 1;
40+
// loop in reverse, starting with 1 as the weight for the last digit
41+
// which is control digit
42+
for (let i = value.length - 1; i >= 0; --i) {
4143
let number = Number(value[i]);
4244

4345
number = weight * number;
@@ -51,6 +53,8 @@ export function mod10(value: string): boolean {
5153
}
5254

5355
sum += number;
56+
// alternate between 1 and 2 for the weight
57+
weight = weight === 1 ? 2 : 1;
5458
}
5559

5660
return sum % 10 === 0;
@@ -66,7 +70,7 @@ export function isValidDate(year: number, month: number, day: number): boolean {
6670
return (
6771
date &&
6872
// cannot do this for Norway
69-
// maybe do it for Sweden?
73+
// maybe do it for Sweden for long format?
7074
// date.getUTCFullYear() === year &&
7175
date.getUTCMonth() === month &&
7276
date.getUTCDate() === day

packages/validation/src/validation.test.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,17 @@ describe('se', () => {
180180
expect(se.validateObosMembershipNumber(input, options)).toBe(expected);
181181
});
182182

183-
test('validateNationalIdentityNumber() - validates leap years', () => {
183+
test('test with leap years', () => {
184+
expect(
185+
se.validateOrganizationNumber('000229-3017', { allowFormatting: true }),
186+
).toBe(true);
187+
188+
expect(
189+
se.validateOrganizationNumber('000229-5855', { allowFormatting: true }),
190+
).toBe(true);
191+
});
192+
193+
test('validateNationalIdentityNumber() - validates short format personnummer', () => {
184194
for (let i = 0; i < 1000; ++i) {
185195
const pnr = swedishPersonNummer({ format: 'short' });
186196
expect(
@@ -189,4 +199,16 @@ describe('se', () => {
189199
).toBe(true);
190200
}
191201
});
202+
203+
// 204101052241
204+
// 211802018075
205+
// 196304076083
206+
test.only('validateNationalIdentityNumber() - validates long format personnummer', () => {
207+
for (let i = 0; i < 1000; ++i) {
208+
const pnr = swedishPersonNummer({ format: 'long' });
209+
expect(se.validateNationalIdentityNumber(pnr), `${pnr} is valid`).toBe(
210+
true,
211+
);
212+
}
213+
});
192214
});

0 commit comments

Comments
 (0)