stdnum
Version:
Standard Number Validation
130 lines (110 loc) • 3.42 kB
text/typescript
/**
* CPR (personnummer, the Danish citizen number).
*
* The CPR is the national number to identify Danish citizens and is stored in
* the Det Centrale Personregister (Civil Registration System). The number
* consists of 10 digits in the format DDMMYY-SSSS where the first part
* represents the birth date and the second a sequence number. The first digit
* of the sequence number indicates the century.
*
* The numbers used to validate using a checksum but since the sequence numbers
* ran out this was abandoned in 2007. It is also not possible to use the
* checksum only for numbers that have a birth date before that because the
* numbers are also assigned to immigrants.
*
* More information:
*
* https://en.wikipedia.org/wiki/Personal_identification_number_(Denmark)
* https://da.wikipedia.org/wiki/CPR-nummer
* https://cpr.dk/
*/
import * as exceptions from '../exceptions';
import { isValidDate, buildDate, validBirthdate, strings } from '../util';
import { Validator, ValidateReturn } from '../types';
export function getBirthDate(value: string): Date {
const [dob] = strings.splitAt(value, 6);
const [day, month, yearStr] = strings.splitAt(dob, 2, 4);
let year = parseInt(yearStr);
if ('5678'.includes(value[6]) && year >= 58) {
year += 1800;
} else if (
'0123'.includes(value[6]) ||
('49'.includes(value[6]) && year >= 37)
) {
year += 1900;
} else {
year += 2000;
}
const d = buildDate(String(year), month, day);
if (d === null || !isValidDate(String(year), month, day)) {
throw new exceptions.InvalidComponent(
'The number does not contain valid birth date information.',
);
}
return d;
}
function clean(input: string): ReturnType<typeof strings.cleanUnicode> {
const [value, err] = strings.cleanUnicode(input, ' -');
if (err !== null) {
return [value, err];
}
return [value, null];
}
const impl: Validator = {
name: 'Danish Citizen Number',
localName: 'Personnummer',
abbreviation: 'CPR',
compact(input: string): string {
const [value, err] = clean(input);
if (err) {
throw err;
}
return value;
},
format(input: string): string {
const [value] = clean(input);
return strings.splitAt(value, 6).join('-');
},
validate(input: string): ValidateReturn {
const [value, error] = clean(input);
if (error) {
return { isValid: false, error };
}
if (!strings.isdigits(value)) {
return { isValid: false, error: new exceptions.InvalidFormat() };
}
if (value.length !== 10) {
return { isValid: false, error: new exceptions.InvalidLength() };
}
try {
const date = getBirthDate(value);
if (!validBirthdate(date)) {
return {
isValid: false,
error: new exceptions.InvalidComponent(
'The birth date information is valid, but this person has not been born yet.',
),
};
}
} catch (err) {
if (err instanceof exceptions.ValidationError) {
return {
isValid: false,
error: err,
};
}
return {
isValid: false,
error: new exceptions.InvalidComponent(String(err)),
};
}
return {
isValid: true,
compact: value,
isIndividual: true,
isCompany: false,
};
},
};
export const { name, localName, abbreviation, validate, format, compact } =
impl;