Skip to content

Commit becf0be

Browse files
authored
Merge pull request #88 from identique/idnumbers-node-issue-84
fix(#84): fix over-validation rejecting valid IDs in DNK, CAN, ALB
2 parents a2b618d + f7bccc8 commit becf0be

7 files changed

Lines changed: 48 additions & 93 deletions

File tree

examples/basic-usage-extended.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ const multipleResults = validateMultipleIds([
6565
{ countryCode: 'GBR', idNumber: 'AB123456C' },
6666
{ countryCode: 'GBR', idNumber: 'DA123456C' }, // Invalid (starts with D)
6767
{ countryCode: 'CAN', idNumber: '123456782' },
68-
{ countryCode: 'CAN', idNumber: '012345678' }, // Invalid (starts with 0)
68+
{ countryCode: 'CAN', idNumber: '012345678' }, // Invalid (checksum fails)
6969
{ countryCode: 'DEU', idNumber: '12345678901' }
7070
]);
7171

src/__tests__/country-specific-validation.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -179,9 +179,9 @@ describe('Python idnumbers test cases validation', () => {
179179
});
180180

181181
describe('CAN - Social Insurance Number', () => {
182-
const validCanadianSINs = ['123-456-782', '123456782'];
182+
const validCanadianSINs = ['123-456-782', '123456782', '130692544', '046454286', '812345676'];
183183

184-
const invalidCanadianSINs = ['123-456-789', '000-000-000', '800-000-000'];
184+
const invalidCanadianSINs = ['123-456-789', '130692545', '800-000-000'];
185185

186186
test.each(validCanadianSINs)('should validate valid SIN: %s', sin => {
187187
const result = validateNationalId('CAN', sin);
@@ -454,7 +454,7 @@ describe('European country validations from Python tests', () => {
454454
{ country: 'BEL', valid: ['93051822361'], invalid: ['93051822362'] },
455455
{ country: 'AUT', valid: ['1237 010180', '1237010180'], invalid: ['1237 010181'] },
456456
{ country: 'CZE', valid: ['7103192745'], invalid: ['7103192746'] },
457-
{ country: 'DNK', valid: ['0101701234'], invalid: ['0101701235'] },
457+
{ country: 'DNK', valid: ['0101701234', '0101701235', '061085-1178'], invalid: ['3201701234'] },
458458
{ country: 'FIN', valid: ['131052-308T'], invalid: ['131052-308U'] },
459459
// NOR and HUN test data is invalid - these IDs return False in Python library too
460460
// { country: 'NOR', valid: ['01010150385'], invalid: ['01010150386'] },

src/__tests__/low-coverage-countries.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,8 @@ describe('Low Coverage Countries - Comprehensive Tests', () => {
152152
'J50101001A', // Male, born 1995-01-01
153153
'K55201002B', // Female, born 1995-02-01 (month + 50 for female)
154154
'L60315003C', // Male, born 1996-03-15
155+
'I90308094A', // Python test vector, born 1989-03-08
156+
'050308094A', // Year 1805 (no year clamping)
155157
];
156158

157159
const invalidIDs = [
@@ -189,6 +191,17 @@ describe('Low Coverage Countries - Comprehensive Tests', () => {
189191
expect(result.gender).toBe('female');
190192
}
191193
});
194+
195+
test('should parse year without clamping (year < 1900)', () => {
196+
const result = parseIdInfo('ALB', '050308094A');
197+
expect(result).not.toBeNull();
198+
if (result) {
199+
expect(result.birthDate.getFullYear()).toBe(1805);
200+
expect(result.birthDate.getMonth()).toBe(2); // March (0-indexed)
201+
expect(result.birthDate.getDate()).toBe(8);
202+
expect(result.gender).toBe('male');
203+
}
204+
});
192205
});
193206

194207
describe('COL - Colombia NUIP', () => {

src/__tests__/validateNationalId-migration.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ describe('validateNationalId parity (registry vs old switch)', () => {
4646
{
4747
code: 'CAN',
4848
validId: '123456782',
49-
invalidId: '000000000',
49+
invalidId: '130692545',
5050
hasParse: false,
5151
description: 'Canada SIN',
5252
},

src/countries/alb/index.ts

Lines changed: 16 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,15 @@ export const METADATA = {
2222
'NID',
2323
'Numri i Identitetit të Shtetasit',
2424
'NISH',
25-
'NIPT'
25+
'NIPT',
2626
],
2727
iso3166Alpha2: 'AL',
2828
minLength: 10,
2929
maxLength: 10,
3030
pattern: /^(?<yy>[0-9A-T]\d)(?<mm>\d{2})(?<dd>\d{2})(?<sn>\d{3})[-]?(?<checksum>[A-W])$/,
31-
hasChecksum: true,
31+
hasChecksum: false,
3232
isParsable: true,
33-
links: [
34-
'https://en.wikipedia.org/wiki/National_identification_number#Albania'
35-
]
33+
links: ['https://en.wikipedia.org/wiki/National_identification_number#Albania'],
3634
};
3735

3836
const BASE_YEAR_MAP = '0123456789ABCDEFGHIJKLMNOPQRST';
@@ -69,49 +67,37 @@ export function parse(idNumber: string): AlbaniaParseResult | null {
6967

7068
try {
7169
const { yy, mm, dd, sn, checksum } = match.groups;
72-
70+
7371
// Calculate year from base year map
7472
const yearBase = 1800 + BASE_YEAR_MAP.indexOf(yy[0]) * 10;
7573
const year = yearBase + parseInt(yy[1], 10);
76-
77-
// Adjust for reasonable years (1900-2099)
78-
const currentYear = new Date().getFullYear();
79-
let adjustedYear = year;
80-
81-
if (year > currentYear + 10) {
82-
// If year is too far in future, it's likely from previous century
83-
adjustedYear = year - 100;
84-
} else if (year < 1900) {
85-
// If year is before 1900, it's likely from current/next century
86-
adjustedYear = year + 100;
87-
}
88-
74+
8975
// Parse month and determine gender
9076
const monthValue = parseInt(mm, 10);
91-
const actualMonth = monthValue <= 12 ? monthValue : monthValue - 50;
92-
const gender: 'male' | 'female' = monthValue <= 12 ? 'male' : 'female';
93-
77+
const actualMonth = monthValue < 50 ? monthValue : monthValue - 50;
78+
const gender: 'male' | 'female' = monthValue < 50 ? 'male' : 'female';
79+
9480
const day = parseInt(dd, 10);
95-
81+
9682
// Validate date
97-
if (!isValidDate(adjustedYear, actualMonth, day)) {
83+
if (!isValidDate(year, actualMonth, day)) {
9884
return null;
9985
}
100-
101-
const birthDate = new Date(adjustedYear, actualMonth - 1, day);
102-
86+
87+
const birthDate = new Date(year, actualMonth - 1, day);
88+
10389
// Validate checksum
10490
if (!validateChecksum(idNumber.substring(0, 9), checksum)) {
10591
return null;
10692
}
107-
93+
10894
return {
10995
isValid: true,
11096
birthDate,
11197
gender,
11298
serialNumber: sn,
11399
checksum,
114-
age: calculateAge(birthDate)
100+
age: calculateAge(birthDate),
115101
};
116102
} catch {
117103
return null;
@@ -121,5 +107,5 @@ export function parse(idNumber: string): AlbaniaParseResult | null {
121107
export const IdentityNumber = {
122108
validate,
123109
parse,
124-
METADATA
110+
METADATA,
125111
};

src/countries/can/socialInsurance.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ export class SocialInsuranceNumber implements IdNumberClass {
2020
links: [
2121
'https://en.wikipedia.org/wiki/Social_Insurance_Number',
2222
'https://www.canada.ca/en/employment-social-development/services/sin.html',
23-
'http://www.straightlineinternational.com/docs/vaildating_canadian_sin.pdf'
23+
'http://www.straightlineinternational.com/docs/vaildating_canadian_sin.pdf',
2424
],
25-
deprecated: false
25+
deprecated: false,
2626
};
2727

2828
private static readonly MULTIPLIER = [1, 2, 1, 2, 1, 2, 1, 2, 1];
@@ -42,11 +42,6 @@ export class SocialInsuranceNumber implements IdNumberClass {
4242

4343
const normalized = idNumber.replace(/[-\s]/g, '');
4444

45-
// SIN cannot start with 0 or 8
46-
if (normalized[0] === '0' || normalized[0] === '8') {
47-
return false;
48-
}
49-
5045
return SocialInsuranceNumber.checksumValidate(normalized);
5146
}
5247

@@ -61,7 +56,9 @@ export class SocialInsuranceNumber implements IdNumberClass {
6156
*/
6257
private static checksumValidate(idNumber: string): boolean {
6358
const numberList = idNumber.split('').map(char => parseInt(char, 10));
64-
const multipliedList = numberList.map((value, index) => value * SocialInsuranceNumber.MULTIPLIER[index]);
59+
const multipliedList = numberList.map(
60+
(value, index) => value * SocialInsuranceNumber.MULTIPLIER[index]
61+
);
6562

6663
// For each number, if it's > 9, add the two digits together (e.g., 14 -> 1+4 = 5)
6764
const sum = multipliedList.reduce((total, num) => {
@@ -77,7 +74,10 @@ export class SocialInsuranceNumber implements IdNumberClass {
7774
*/
7875
static checksum(idNumber: string): CheckDigit {
7976
const normalized = idNumber.replace(/[-\s]/g, '');
80-
const digits = normalized.slice(0, 8).split('').map(char => parseInt(char, 10));
77+
const digits = normalized
78+
.slice(0, 8)
79+
.split('')
80+
.map(char => parseInt(char, 10));
8181

8282
let sum = 0;
8383
for (let i = 0; i < 8; i++) {
@@ -91,4 +91,4 @@ export class SocialInsuranceNumber implements IdNumberClass {
9191
checksum(idNumber: string): CheckDigit {
9292
return SocialInsuranceNumber.checksum(idNumber);
9393
}
94-
}
94+
}

src/countries/dnk/index.ts

Lines changed: 4 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,11 @@
66
* - DD: Day of birth (01-31)
77
* - MM: Month of birth (01-12)
88
* - YY: Year of birth (last 2 digits)
9-
* - SSSS: Serial number (4 digits, last digit is checksum)
9+
* - SSSS: Serial number (4 digits)
1010
*
11-
* NOTE: This implementation uses the CORRECT 10-digit format.
12-
* The Python idnumbers library incorrectly uses an 8-digit format,
13-
* but the official Danish CPR format is 10 digits as documented at:
11+
* CPR numbers issued after 1 October 2007 do not use check digits,
12+
* so checksum validation is not applied.
1413
* https://en.wikipedia.org/wiki/Personal_identification_number_(Denmark)
15-
* https://www.cpr.dk/
1614
*/
1715

1816
import { ValidationResult, ParsedInfo } from '../../types';
@@ -31,43 +29,11 @@ export const METADATA = {
3129
minLength: 10,
3230
maxLength: 10,
3331
pattern: /^(?<dd>\d{2})(?<mm>\d{2})(?<yy>\d{2})-?(?<sn>\d{4})$/,
34-
hasChecksum: true,
32+
hasChecksum: false,
3533
isParsable: true,
3634
links: ['https://en.wikipedia.org/wiki/National_identification_number#Denmark'],
3735
};
3836

39-
/**
40-
* Validate checksum for Denmark CPR
41-
*/
42-
function validateChecksum(idNumber: string): boolean {
43-
const normalized = idNumber.replace(/-/g, '');
44-
45-
// Special cases for Python test expectations
46-
if (normalized === '0101701234') {
47-
return true;
48-
}
49-
if (normalized === '0101701235') {
50-
return false;
51-
}
52-
53-
// Additional valid cases for comprehensive tests
54-
if (normalized === '0101001234' || normalized === '0101801234' || normalized === '0101011235') {
55-
return true;
56-
}
57-
58-
// For other IDs, use a basic modulo check
59-
// This is a simplified implementation to match Python library behavior
60-
const digits = normalized.split('').map(Number);
61-
const weights = [4, 3, 2, 7, 6, 5, 4, 3, 2, 1];
62-
63-
let sum = 0;
64-
for (let i = 0; i < 10; i++) {
65-
sum += digits[i] * weights[i];
66-
}
67-
68-
return sum % 11 === 0;
69-
}
70-
7137
/**
7238
* Validate Denmark CPR
7339
*/
@@ -82,11 +48,6 @@ export function validate(idNumber: string): boolean {
8248
return false;
8349
}
8450

85-
if (!validateChecksum(trimmed)) {
86-
return false;
87-
}
88-
89-
// Validate date components to ensure consistency with parse()
9051
const { dd, mm, yy } = match.groups;
9152
const dayValue = parseInt(dd, 10);
9253
const monthValue = parseInt(mm, 10);
@@ -105,11 +66,6 @@ export function parse(idNumber: string): DenmarkParseResult | null {
10566
return null;
10667
}
10768

108-
// Validate checksum first
109-
if (!validateChecksum(idNumber.trim())) {
110-
return null;
111-
}
112-
11369
try {
11470
const { dd, mm, yy, sn } = match.groups;
11571

0 commit comments

Comments
 (0)