Skip to content

Commit 90d4eac

Browse files
committed
more tests
1 parent 6512ffb commit 90d4eac

File tree

5 files changed

+88
-45
lines changed

5 files changed

+88
-45
lines changed

packages/validation/README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ The methods are "strict" by default, meaning no formatting characters in the inp
4040
This is preferrable, for instance when doing server-side validation, where the input is often expected to be a "clean" value.
4141

4242
If you want to allow formatting characters in the input, you can pass `allowFormatting: true` in the options object to the method.
43-
Note that this currently allows any formatting characters, not just the just the "expected" ones for the input type.
4443

4544

4645
```js

packages/validation/src/no.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -113,11 +113,7 @@ type NationalIdentityNumberOptions = ValidatorOptions;
113113
*
114114
* @example
115115
* ```
116-
* // Fødselsnummer
117-
* validatePersonalIdentityNumber('21075417753') // => true
118-
*
119-
* // D-nummer
120-
* validatePersonalIdentityNumber('53097248016') // => true
116+
* validatePersonalIdentityNumber('DDMMYYXXXXX') // => true
121117
* ```
122118
*/
123119
export function validateNationalIdentityNumber(

packages/validation/src/se.ts

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,8 @@ export function validateOrganizationNumber(
8585
}
8686

8787
type NationalIdentityNumberFormat = 'short' | 'long';
88-
type NationalIdenityNumberOptions = ValidatorOptions & {
89-
/** By default, both formats are allowed */
88+
type NationalIdentityNumberOptions = ValidatorOptions & {
89+
/** Specify this if you want to format to be only long (12 digits) or short (10 digits). By default, both formats are allowed */
9090
format?: NationalIdentityNumberFormat;
9191
};
9292

@@ -100,16 +100,18 @@ const PERSONNUMMER_FORMAT = /^(\d{2}){0,1}(\d{2})(\d{2})(\d{2})([+-]?)(\d{4})$/;
100100
*
101101
* @example
102102
* ```
103-
* // Personnummer
104-
* validatePersonalIdentityNumber('21075417753') // => true
103+
* // Short format
104+
* validatePersonalIdentityNumber('YYMMDDXXXX') // => true
105+
* validatePersonalIdentityNumber('YYMMDD-XXXX', { allowFormatting: true }) // => true
105106
*
106-
* // Samordningsnummer
107-
* validatePersonalIdentityNumber('53097248016') // => true
107+
* // Long format
108+
* validatePersonalIdentityNumber('YYYYMMDDXXXX') // => true
109+
* validatePersonalIdentityNumber('YYYYMMDD-XXXX', { allowFormatting: true }) // => true
108110
* ```
109111
*/
110112
export function validateNationalIdentityNumber(
111113
value: string,
112-
options: NationalIdenityNumberOptions = {},
114+
options: NationalIdentityNumberOptions = {},
113115
): boolean {
114116
const match = PERSONNUMMER_FORMAT.exec(value);
115117

@@ -123,6 +125,10 @@ export function validateNationalIdentityNumber(
123125
return false;
124126
}
125127

128+
if (!centuryStr && options.format === 'long') {
129+
return false;
130+
}
131+
126132
if (separator && !options.allowFormatting) {
127133
return false;
128134
}
@@ -137,13 +143,13 @@ export function validateNationalIdentityNumber(
137143
let year = 0;
138144
switch (true) {
139145
// if we have the long format version, we already have the full year
140-
case centuryStr:
146+
case !!centuryStr:
141147
year = Number(centuryStr + yearStr);
142148
break;
143149
// otherwise, we can use the separator to determine the century of the personnummer
144-
// if the separator is '+', the person is over a 100 years old
145-
// we can then get
146-
case separator: {
150+
// if the separator is '+', we know person is over a 100 years old
151+
// we can then calculate the full year
152+
case !!separator: {
147153
const date = new Date();
148154
const baseYear =
149155
separator === '+' ? date.getUTCFullYear() - 100 : date.getUTCFullYear();
@@ -170,7 +176,7 @@ export function validateNationalIdentityNumber(
170176
day = day - 60;
171177
}
172178

173-
return isValidDate(year, month, day);
179+
return isValidDate(year, month, day, centuryStr || separator);
174180
}
175181

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

packages/validation/src/utils.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,18 +60,24 @@ export function mod10(value: string): boolean {
6060
return sum % 10 === 0;
6161
}
6262

63-
export function isValidDate(year: number, month: number, day: number): boolean {
63+
export function isValidDate(
64+
year: number,
65+
month: number,
66+
day: number,
67+
/** Whether to check the year as part of the date validation. */
68+
validateYear = false,
69+
): boolean {
6470
// biome-ignore lint/style/noParameterAssign: months are zero index 🤷‍♂️
6571
month -= 1;
6672

6773
// important to use UTC so the user's timezone doesn't affect the validation
6874
const date = new Date(Date.UTC(year, month, day));
6975

76+
const validYear = validateYear ? date.getUTCFullYear() === year : true;
77+
7078
return (
7179
date &&
72-
// cannot do this for Norway
73-
// maybe do it for Sweden for long format?
74-
// date.getUTCFullYear() === year &&
80+
validYear &&
7581
date.getUTCMonth() === month &&
7682
date.getUTCDate() === day
7783
);

packages/validation/src/validation.test.ts

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

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', () => {
183+
test('validateNationalIdentityNumber() - validates short format (YYMMDDXXXX) personnummer', () => {
194184
for (let i = 0; i < 1000; ++i) {
195-
const pnr = swedishPersonNummer({ format: 'short' });
185+
const pnrWithSeparator = swedishPersonNummer({ format: 'short' });
186+
const pnrWithoutSeparator = pnrWithSeparator.replace(/[-+]/, '');
187+
196188
expect(
197-
se.validateNationalIdentityNumber(pnr, {
189+
se.validateNationalIdentityNumber(pnrWithSeparator, {
198190
allowFormatting: true,
199191
format: 'short',
200192
}),
201-
`${pnr} is valid`,
193+
`${pnrWithSeparator} is valid with separator`,
202194
).toBe(true);
203-
}
204-
});
205195

206-
test('validateNationalIdentityNumber() - validates long format personnummer', () => {
207-
for (let i = 0; i < 1000; ++i) {
208-
const pnr = swedishPersonNummer({ format: 'long' });
209196
expect(
210-
se.validateNationalIdentityNumber(pnr, { format: 'long' }),
211-
`${pnr} is valid`,
197+
se.validateNationalIdentityNumber(pnrWithoutSeparator, {
198+
format: 'short',
199+
}),
200+
`${pnrWithSeparator} is valid without separator`,
212201
).toBe(true);
213202
}
214203
});
215204

216-
test('validateNationalIdentityNumber() - validates long format personnummer', () => {
205+
test('validateNationalIdentityNumber() - validates long format (YYYYMMDDXXXX) personnummer', () => {
217206
for (let i = 0; i < 1000; ++i) {
218207
const pnr = swedishPersonNummer({ format: 'long' });
208+
219209
expect(
220210
se.validateNationalIdentityNumber(pnr, { format: 'long' }),
221211
`${pnr} is valid`,
222212
).toBe(true);
223213
}
224214
});
225215

226-
test.only('validateNationalIdentityNumber() - handles leap years', () => {
216+
test('validateNationalIdentityNumber() - handles separator/leap years', () => {
217+
// 29th of February is the best way to test whether the separator and long/short handling works correctly.
218+
// The 29th of February year 2000 is valid a valid date, while the 29th of February year 1900 is not.
219+
// That means we get different results based on the separator.
227220
expect(se.validateNationalIdentityNumber('0002297422')).toBe(true);
221+
expect(
222+
se.validateNationalIdentityNumber('000229-7422', {
223+
allowFormatting: true,
224+
}),
225+
).toBe(true);
226+
227+
expect(
228+
se.validateNationalIdentityNumber('000229+7422', {
229+
allowFormatting: true,
230+
}),
231+
).toBe(false);
232+
233+
expect(se.validateNationalIdentityNumber('190002297422')).toBe(false);
234+
});
235+
236+
test('validateNationalIdentityNumber() - validates samordningsnummer', () => {
237+
expect(
238+
se.validateNationalIdentityNumber('701063-2391', {
239+
allowFormatting: true,
240+
}),
241+
).toBe(true);
242+
});
243+
244+
test('validateNationalIdentityNumber() - respects format modifier', () => {
245+
expect(
246+
se.validateNationalIdentityNumber(
247+
swedishPersonNummer({ format: 'short' }),
248+
{
249+
allowFormatting: true,
250+
format: 'long',
251+
},
252+
),
253+
).toBe(false);
254+
255+
expect(
256+
se.validateNationalIdentityNumber(
257+
swedishPersonNummer({ format: 'long' }),
258+
{
259+
allowFormatting: true,
260+
format: 'short',
261+
},
262+
),
263+
).toBe(false);
228264
});
229265
});

0 commit comments

Comments
 (0)