Skip to content

Commit 4ae81df

Browse files
authored
fix: calculation of the check digit when it is in the optional part (#66)
Based on examples from PRADO and ICAO examples (https://www.icao.int/sites/default/files/publications/DocSeries/9303_p11_cons_en.pdf), the `<` separator before the optional part should not be included in checksum calculation. Closes: #36
1 parent 72943aa commit 4ae81df

File tree

4 files changed

+252
-9
lines changed

4 files changed

+252
-9
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ const parse = require('mrz').parse;
2929

3030
const mrz = [
3131
'I<UTOD23145890<1233<<<<<<<<<<<',
32-
'7408122F1204159UTO<<<<<<<<<<<6',
32+
'7408122F1204159UTO<<<<<<<<<<<2',
3333
'ERIKSSON<<ANNA<MARIA<<<<<<<<<<',
3434
];
3535

src/parse/__tests__/td1.test.ts

Lines changed: 246 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,192 @@ describe('parse TD1', () => {
4848
});
4949
});
5050

51+
it('Portuguese ID - valid', () => {
52+
// source: https://media.timeout.com/images/106144795/image.jpg
53+
const MRZ = [
54+
'I<PRT007777779<ZZ92<<<<<<<<<<<',
55+
'8303143M3405282PRT<<<<<<<<<<<2',
56+
'CACADOR<DE<ARAUJO<<ANDRE<ESTEV',
57+
];
58+
59+
const result = parse(MRZ);
60+
61+
expect(result).toMatchObject({
62+
format: 'TD1',
63+
valid: true,
64+
documentNumber: result.fields.documentNumber,
65+
});
66+
67+
expect(result.fields).toStrictEqual({
68+
documentCode: 'I',
69+
issuingState: 'PRT',
70+
documentNumber: '007777779ZZ9',
71+
documentNumberCheckDigit: '2',
72+
birthDate: '830314',
73+
birthDateCheckDigit: '3',
74+
sex: 'male',
75+
expirationDate: '340528',
76+
expirationDateCheckDigit: '2',
77+
nationality: 'PRT',
78+
optional1: 'ZZ92',
79+
optional2: '',
80+
compositeCheckDigit: '2',
81+
lastName: 'CACADOR DE ARAUJO',
82+
firstName: 'ANDRE ESTEV',
83+
});
84+
85+
const optional1Details = result.details.find(
86+
(f) => f.field === 'optional1',
87+
);
88+
89+
expect(optional1Details).toMatchObject({
90+
value: 'ZZ92',
91+
line: 0,
92+
start: 15,
93+
end: 19,
94+
});
95+
});
96+
97+
it('Portuguese ID PRT-BO-04001 - valid', () => {
98+
// source: https://www.consilium.europa.eu/prado/en/PRT-BO-04001/index.html
99+
const MRZ = [
100+
'I<PRT007666667<ZZ00<<<<<<<<<<<',
101+
'8303143M3405293PRT<<<<<<<<<<<4',
102+
'CACADOR<DE<ARAUJO<<ANDRE<ESTEV',
103+
];
104+
105+
const result = parse(MRZ);
106+
107+
expect(result).toMatchObject({
108+
format: 'TD1',
109+
valid: true,
110+
documentNumber: result.fields.documentNumber,
111+
});
112+
113+
expect(result.fields).toStrictEqual({
114+
documentCode: 'I',
115+
issuingState: 'PRT',
116+
documentNumber: '007666667ZZ0',
117+
documentNumberCheckDigit: '0',
118+
birthDate: '830314',
119+
birthDateCheckDigit: '3',
120+
sex: 'male',
121+
expirationDate: '340529',
122+
expirationDateCheckDigit: '3',
123+
nationality: 'PRT',
124+
optional1: 'ZZ00',
125+
optional2: '',
126+
compositeCheckDigit: '4',
127+
lastName: 'CACADOR DE ARAUJO',
128+
firstName: 'ANDRE ESTEV',
129+
});
130+
131+
const optional1Details = result.details.find(
132+
(f) => f.field === 'optional1',
133+
);
134+
135+
expect(optional1Details).toMatchObject({
136+
value: 'ZZ00',
137+
line: 0,
138+
start: 15,
139+
end: 19,
140+
});
141+
});
142+
143+
it('Belgium ID BEL-BO-11005 - valid', () => {
144+
// source: https://www.consilium.europa.eu/prado/en/BEL-BO-11005/index.html
145+
// Changed the country code from UTO to BEL to make it pass the country validation,
146+
// but that this part does not affect any check digit checks.
147+
const MRZ = [
148+
'IDBEL600001795<0152<<<<<<<<<<<',
149+
'1301014F2311207BEL130101987398',
150+
'SPECIMEN<<SPECIMEN<<<<<<<<<<<<',
151+
];
152+
153+
const result = parse(MRZ);
154+
155+
expect(result).toMatchObject({
156+
format: 'TD1',
157+
valid: true,
158+
documentNumber: result.fields.documentNumber,
159+
});
160+
161+
expect(result.fields).toStrictEqual({
162+
documentCode: 'ID',
163+
issuingState: 'BEL',
164+
documentNumber: '600001795015',
165+
documentNumberCheckDigit: '2',
166+
birthDate: '130101',
167+
birthDateCheckDigit: '4',
168+
sex: 'female',
169+
expirationDate: '231120',
170+
expirationDateCheckDigit: '7',
171+
nationality: 'BEL',
172+
optional1: '0152',
173+
optional2: '13010198739',
174+
compositeCheckDigit: '8',
175+
lastName: 'SPECIMEN',
176+
firstName: 'SPECIMEN',
177+
});
178+
179+
const optional1Details = result.details.find(
180+
(f) => f.field === 'optional1',
181+
);
182+
183+
expect(optional1Details).toMatchObject({
184+
value: '0152',
185+
line: 0,
186+
start: 15,
187+
end: 19,
188+
});
189+
});
190+
191+
it('Finland ID FIN-BO-12001 - valid', () => {
192+
// source: https://www.consilium.europa.eu/prado/en/FIN-BO-12001/index.html
193+
const MRZ = [
194+
'I<FINXA10000585010195<112X<<<<',
195+
'9501016F2803135FIN<<<<<<<<<<<7',
196+
'SPECIMEN<TRAVEL<<VILMA<SOFIA<<',
197+
];
198+
199+
const result = parse(MRZ);
200+
201+
expect(result).toMatchObject({
202+
format: 'TD1',
203+
valid: true,
204+
documentNumber: result.fields.documentNumber,
205+
});
206+
207+
expect(result.fields).toStrictEqual({
208+
documentCode: 'I',
209+
issuingState: 'FIN',
210+
documentNumber: 'XA1000058',
211+
documentNumberCheckDigit: '5',
212+
birthDate: '950101',
213+
birthDateCheckDigit: '6',
214+
sex: 'female',
215+
expirationDate: '280313',
216+
expirationDateCheckDigit: '5',
217+
nationality: 'FIN',
218+
optional1: '010195 112X',
219+
optional2: '',
220+
compositeCheckDigit: '7',
221+
lastName: 'SPECIMEN TRAVEL',
222+
firstName: 'VILMA SOFIA',
223+
});
224+
225+
const optional1Details = result.details.find(
226+
(f) => f.field === 'optional1',
227+
);
228+
229+
expect(optional1Details).toMatchObject({
230+
value: '010195 112X',
231+
line: 0,
232+
start: 15,
233+
end: 26,
234+
});
235+
});
236+
51237
it('Utopia example', () => {
52238
const MRZ = [
53239
'I<UTOD231458907ABC<<<<<<<<<<<<',
@@ -107,8 +293,8 @@ describe('parse TD1', () => {
107293

108294
it('parse document number', () => {
109295
const MRZ = [
110-
'I<UTOD23145890<1240<XYZ<<<<<<<',
111-
'7408122F1204159UTO<<<<<<<<<<<8',
296+
'I<UTOD23145890<1233<<<<<<<<<<<',
297+
'7408122F1204159UTO<<<<<<<<<<<2',
112298
'ERIKSSON<<ANNA<MARIA<<<<<<<<<<',
113299
];
114300

@@ -129,20 +315,73 @@ describe('parse TD1', () => {
129315
expect(documentNumberDetails).toStrictEqual({
130316
label: 'Document number',
131317
field: 'documentNumber',
132-
value: 'D23145890124',
318+
value: 'D23145890123',
319+
valid: true,
320+
ranges: [
321+
{ line: 0, start: 5, end: 14, raw: 'D23145890' },
322+
{ line: 0, start: 14, end: 15, raw: '<' },
323+
{ line: 0, start: 15, end: 30, raw: '1233<<<<<<<<<<<' },
324+
],
325+
line: 0,
326+
start: 5,
327+
end: 18,
328+
autocorrect: [],
329+
});
330+
expect(result.fields.documentNumber).toBe('D23145890123');
331+
expect(result.fields.documentNumberCheckDigit).toBe('3');
332+
333+
const documentNumberCheckDigitDetails = result.details.find(
334+
(d) => d.field === 'documentNumberCheckDigit',
335+
);
336+
337+
expect(documentNumberCheckDigitDetails).toMatchObject({
338+
line: 0,
339+
start: 18,
340+
end: 19,
341+
value: '3',
342+
});
343+
});
344+
345+
it('parse document number ICAO Doc 9303 Sample', () => {
346+
// source: https://www.icao.int/sites/default/files/publications/DocSeries/9303_p11_cons_en.pdf
347+
// page 88, D.2 DERIVATION OF DOCUMENT BASIC ACCESS KEYS
348+
const MRZ = [
349+
'I<UTOD23145890<7349<<<<<<<<<<<',
350+
'3407127M9507122UTO<<<<<<<<<<<2',
351+
'STEVENSON<<PETER<JOHN<<<<<<<<<',
352+
];
353+
354+
const result = parse(MRZ);
355+
356+
expect(result).toMatchObject({
357+
format: 'TD1',
358+
valid: false,
359+
documentNumber: result.fields.documentNumber,
360+
});
361+
362+
expect(result.details.filter((f) => !f.valid)).toHaveLength(2);
363+
364+
const documentNumberDetails = result.details.find(
365+
(d) => d.field === 'documentNumber',
366+
);
367+
368+
expect(documentNumberDetails).toStrictEqual({
369+
label: 'Document number',
370+
field: 'documentNumber',
371+
value: 'D23145890734',
133372
valid: true,
134373
ranges: [
135374
{ line: 0, start: 5, end: 14, raw: 'D23145890' },
136375
{ line: 0, start: 14, end: 15, raw: '<' },
137-
{ line: 0, start: 15, end: 30, raw: '1240<XYZ<<<<<<<' },
376+
{ line: 0, start: 15, end: 30, raw: '7349<<<<<<<<<<<' },
138377
],
139378
line: 0,
140379
start: 5,
141380
end: 18,
142381
autocorrect: [],
143382
});
144-
expect(result.fields.documentNumber).toBe('D23145890124');
145-
expect(result.fields.documentNumberCheckDigit).toBe('0');
383+
expect(result.fields.documentNumber).toBe('D23145890734');
384+
expect(result.fields.documentNumberCheckDigit).toBe('9');
146385

147386
const documentNumberCheckDigitDetails = result.details.find(
148387
(d) => d.field === 'documentNumberCheckDigit',
@@ -152,7 +391,7 @@ describe('parse TD1', () => {
152391
line: 0,
153392
start: 18,
154393
end: 19,
155-
value: '0',
394+
value: '9',
156395
});
157396
});
158397

src/parsers/__tests__/check.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,9 @@ import { check } from '../check.ts';
55
test('check digits', () => {
66
expect(() => check('592166117<231', 8)).not.toThrow();
77
expect(() => check('592166111<773', 5)).not.toThrow();
8+
expect(() => check('007666667<ZZ0', 0)).not.toThrow();
9+
expect(() => check('007666667ZZ0', 0)).not.toThrow();
10+
expect(() => check('007777779ZZ9', 2)).not.toThrow();
11+
expect(() => check('600001795015', 2)).not.toThrow();
812
expect(() => check('592166111<773', 4)).toThrow(/invalid check digit/);
913
});

src/parsers/parseDocumentNumberCheckDigit.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export default function parseDocumentNumberCheckDigit(
88
if (checkDigit === '<' && optional) {
99
const firstFiller = optional.indexOf('<');
1010
const tail = optional.slice(0, firstFiller - 1);
11-
source = `${source}<${tail}`;
11+
source = `${source}${tail}`;
1212
checkDigit = optional.charAt(firstFiller - 1);
1313
check(source, checkDigit);
1414
return {

0 commit comments

Comments
 (0)