Skip to content

Commit ac0cfbb

Browse files
committed
getting there
1 parent 57022e1 commit ac0cfbb

File tree

2 files changed

+53
-21
lines changed

2 files changed

+53
-21
lines changed

packages/validation/src/se.ts

Lines changed: 39 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ type NationalIdenityNumberOptions = ValidatorOptions & {
9090
format?: NationalIdentityNumberFormat;
9191
};
9292

93+
// the first two digts are optional, as they're the century in the long format version
94+
const PERSONNUMMER_FORMAT = /^(\d{2}){0,1}(\d{2})(\d{2})(\d{2})([+-]?)(\d{4})$/;
95+
9396
/**
9497
* Validates that the input value is a Swedish national identity number (personnummer or samordningsnummer).
9598
*
@@ -108,40 +111,55 @@ export function validateNationalIdentityNumber(
108111
value: string,
109112
options: NationalIdenityNumberOptions = {},
110113
): boolean {
111-
if (options.allowFormatting) {
112-
// biome-ignore lint/style/noParameterAssign:
113-
value = stripFormatting(value);
114+
const match = PERSONNUMMER_FORMAT.exec(value);
115+
116+
if (!match) {
117+
return false;
114118
}
115119

116-
const isLongFormat = value.length === 12;
120+
const [_, century, year, month, day, separator, rest] = match;
117121

118-
// when verifying the value, we must always use the short format.
119-
// because the long format would generate a different checksum
120-
const isValid = mod10(isLongFormat ? value.substring(2) : value);
121-
if (!isValid) {
122+
if (century && options.format === 'short') {
122123
return false;
123124
}
124125

125-
// this allows us to handle both YYYYMMDD and YYMMDD when extracting the date
126-
const offset = isLongFormat ? 2 : 0;
127-
// copy/inspiration from NAV https://github.com/navikt/fnrvalidator/blob/77e57f0bc8e3570ddc2f0a94558c58d0f7259fe0/src/validator.ts#L108
128-
let year = Number(value.substring(0, 2 + offset));
129-
const month = Number(value.substring(2 + offset, 4 + offset));
130-
let day = Number(value.substring(4 + offset, 6 + offset));
126+
if (separator && !options.allowFormatting) {
127+
return false;
128+
}
131129

132-
// 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
133-
// we need to special handle that case. For other cases it doesn't really matter if the year is 1925 or 2025.
134-
if (!isLongFormat && year === 0) {
135-
year = 2000;
130+
// when verifying the value, we must always use the short format, discaring the century
131+
// if included it would generate a different checksum
132+
const isValid = mod10(`${year}${month}${day}${rest}`);
133+
if (!isValid) {
134+
return false;
136135
}
137136

137+
// // this allows us to handle both YYYYMMDD and YYMMDD when extracting the date
138+
// const offset = isLongFormat ? 2 : 0;
139+
// // copy/inspiration from NAV https://github.com/navikt/fnrvalidator/blob/77e57f0bc8e3570ddc2f0a94558c58d0f7259fe0/src/validator.ts#L108
140+
// let year = Number(value.substring(0, 2 + offset));
141+
// const month = Number(value.substring(2 + offset, 4 + offset));
142+
// let day = Number(value.substring(4 + offset, 6 + offset));
143+
//
144+
145+
const month2 = Number(month);
146+
let day2 = Number(day);
147+
let year2 = Number(century ? century + year : year);
138148
// for a samordningsnummer the day is increased by 60. Eg the 31st of a month would be 91, or the 3rd would be 63.
139149
// thus we need to subtract 60 to get the correct day of the month
140-
if (day > 60) {
141-
day = day - 60;
150+
if (day2 > 60) {
151+
day2 = day2 - 60;
152+
}
153+
154+
// 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
155+
// we need to special handle that case. For other cases it doesn't really matter if the year is 1925 or 2025.
156+
if (!separator && !century && year === '00') {
157+
year2 = 2000;
158+
} else if (century) {
159+
year2 = Number(century + year);
142160
}
143161

144-
return isValidDate(year, month, day);
162+
return isValidDate(year2, month2, day2);
145163
}
146164

147165
// just reexport the no method for API feature parity

packages/validation/src/validation.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,4 +212,18 @@ describe('se', () => {
212212
).toBe(true);
213213
}
214214
});
215+
216+
test('validateNationalIdentityNumber() - validates long format personnummer', () => {
217+
for (let i = 0; i < 1000; ++i) {
218+
const pnr = swedishPersonNummer({ format: 'long' });
219+
expect(
220+
se.validateNationalIdentityNumber(pnr, { format: 'long' }),
221+
`${pnr} is valid`,
222+
).toBe(true);
223+
}
224+
});
225+
226+
test('validateNationalIdentityNumber() - handles leap years', () => {
227+
expect(se.validateNationalIdentityNumber('0002297422')).toBe(true);
228+
});
215229
});

0 commit comments

Comments
 (0)