Skip to content

Commit 06eb7be

Browse files
authored
fix: check digit position when it is not valid (#74)
* tests: clarify TD1 with document number check digit in optional part Add tests, references, and clarify in comments. Expected check digit error based on document number input without the '<' character * fix: ensure start and end of check digit is correct in field details Closes: #72
1 parent 8d0aa52 commit 06eb7be

File tree

9 files changed

+271
-49
lines changed

9 files changed

+271
-49
lines changed

src/parse/__tests__/td1.test.ts

Lines changed: 110 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { describe, expect, it } from 'vitest';
22

3+
import { computeCheckDigit } from '../../parsers/check.ts';
34
import parse from '../parse.ts';
45

56
describe('parse TD1', () => {
@@ -336,7 +337,7 @@ describe('parse TD1', () => {
336337
});
337338
});
338339

339-
it('parse document number', () => {
340+
it('Document number field details', () => {
340341
const MRZ = [
341342
'I<UTOD23145890<1233<<<<<<<<<<<',
342343
'7408122F1204159UTO<<<<<<<<<<<2',
@@ -351,7 +352,7 @@ describe('parse TD1', () => {
351352
documentNumber: result.fields.documentNumber,
352353
});
353354

354-
expect(result.details.filter((f) => !f.valid)).toHaveLength(2);
355+
expect(result.details.filter((item) => !item.valid)).toHaveLength(2);
355356

356357
const documentNumberDetails = result.details.find(
357358
(d) => d.field === 'documentNumber',
@@ -550,5 +551,112 @@ describe('parse TD1', () => {
550551
lastName: 'REINARTZ',
551552
firstName: 'ULRIKE KATIA E',
552553
});
554+
555+
const detailedCheckDigit = result.details.find(
556+
(item) => item.field === 'documentNumberCheckDigit',
557+
);
558+
expect(detailedCheckDigit).toMatchObject({
559+
valid: true,
560+
start: 18,
561+
end: 19,
562+
value: '3',
563+
});
564+
});
565+
it('Belgium ID BEL-BO-03003, with wrong document number check digit', () => {
566+
// source: https://www.consilium.europa.eu/prado/en/BEL-BO-03003/index.html
567+
// This Belgian ID has the document number check digit embedded in optional1
568+
const MRZ = [
569+
'IDBEL000590240<6016<<<<<<<<<<<',
570+
'8512017F1311048BEL851201002007',
571+
'REINARTZ<<ULRIKE<KATIA<E<<<<<<',
572+
];
573+
574+
const result = parse(MRZ);
575+
576+
expect(result).toMatchObject({
577+
format: 'TD1',
578+
valid: false,
579+
documentNumber: result.fields.documentNumber,
580+
});
581+
582+
const expectedCheckDigit = computeCheckDigit('000590240601');
583+
584+
const wrongFields = result.details.filter((item) => !item.valid);
585+
expect(wrongFields).toHaveLength(2);
586+
587+
const documentNumberDetails = wrongFields.find(
588+
(item) => item.field === 'documentNumberCheckDigit',
589+
);
590+
591+
expect(documentNumberDetails).toStrictEqual({
592+
valid: false,
593+
start: 18,
594+
end: 19,
595+
value: '6',
596+
field: 'documentNumberCheckDigit',
597+
label: 'Document number check digit',
598+
line: 0,
599+
ranges: [
600+
{
601+
line: 0,
602+
start: 14,
603+
end: 15,
604+
raw: '<',
605+
},
606+
{
607+
line: 0,
608+
start: 5,
609+
end: 14,
610+
raw: '000590240',
611+
},
612+
{
613+
line: 0,
614+
start: 15,
615+
end: 30,
616+
raw: '6016<<<<<<<<<<<',
617+
},
618+
],
619+
autocorrect: [],
620+
error: `invalid check digit: 6. Must be ${expectedCheckDigit}`,
621+
});
622+
623+
const compositeCheckDigitDetails = wrongFields.find(
624+
(item) => item.field === 'compositeCheckDigit',
625+
);
626+
627+
expect(compositeCheckDigitDetails).toBeDefined();
628+
});
629+
630+
it('Belgium ID BEL-BO-03003, with wrong birth date check digit', () => {
631+
// source: https://www.consilium.europa.eu/prado/en/BEL-BO-03003/index.html
632+
// The birthdate check digit was changed
633+
const MRZ = [
634+
'IDBEL000590240<6013<<<<<<<<<<<',
635+
'8512018F1311048BEL851201002007',
636+
'REINARTZ<<ULRIKE<KATIA<E<<<<<<',
637+
];
638+
639+
const result = parse(MRZ);
640+
641+
expect(result).toMatchObject({
642+
format: 'TD1',
643+
valid: false,
644+
documentNumber: result.fields.documentNumber,
645+
});
646+
647+
const wrongDetails = result.details.filter((item) => !item.valid);
648+
expect(wrongDetails).toHaveLength(2);
649+
650+
const birthDateDetails = wrongDetails.find(
651+
(item) => item.field === 'birthDateCheckDigit',
652+
);
653+
654+
expect(birthDateDetails).toMatchObject({
655+
valid: false,
656+
start: 6,
657+
end: 7,
658+
value: '8',
659+
error: `invalid check digit: 8. Must be 7`,
660+
});
553661
});
554662
});

src/parse/__tests__/td3.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ describe('parse TD3', () => {
3737

3838
const errors = result.details.filter((a) => !a.valid);
3939

40+
// Issuing state and nationality
4041
expect(errors).toHaveLength(2);
4142

4243
const personalNumberDetails = result.details.find(
@@ -69,6 +70,43 @@ describe('parse TD3', () => {
6970
});
7071
});
7172

73+
it('Utopia example - wrong personal number check digit', () => {
74+
// The same example as the previous one, but the 2nd character from the end was changed from a '1' to a '2'.
75+
const MRZ = [
76+
'P<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<',
77+
'L898902C36UTO7408122F1204159ZE184226B<<<<<20',
78+
];
79+
80+
const result = parse(MRZ);
81+
82+
expect(result).toMatchObject({
83+
format: 'TD3',
84+
valid: false,
85+
documentNumber: result.fields.documentNumber,
86+
});
87+
88+
const wrongDetails = result.details.filter((item) => !item.valid);
89+
// Issuing state, nationality, personal number check digit and composite check digit
90+
expect(wrongDetails).toHaveLength(4);
91+
expect(
92+
wrongDetails.find((item) => item.field === 'personalNumberCheckDigit'),
93+
).toMatchObject({
94+
start: 42,
95+
end: 43,
96+
value: '2',
97+
error: 'invalid check digit: 2. Must be 1',
98+
});
99+
100+
expect(
101+
wrongDetails.find((item) => item.field === 'compositeCheckDigit'),
102+
).toMatchObject({
103+
start: 43,
104+
end: 44,
105+
value: '0',
106+
error: 'invalid check digit: 0. Must be 1',
107+
});
108+
});
109+
72110
it('German example', () => {
73111
const MRZ = [
74112
'P<D<<MUSTERMANN<<ERIKA<<<<<<<<<<<<<<<<<<<<<<',

src/parse/createFieldParser.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import { autoCorrection } from './autoCorrection.ts';
55

66
interface ParseResult {
77
value: string;
8-
start: number;
9-
end: number;
8+
start?: number;
9+
end?: number;
10+
valid?: boolean;
11+
error?: string | null;
1012
}
1113

1214
type Parser = (source: string, ...related: string[]) => ParseResult | string;
@@ -96,11 +98,22 @@ export default function createFieldParser(
9698
result.end = range.end;
9799
try {
98100
const parsed = fieldOptions.parser(source, ...textRelated);
99-
result.value = typeof parsed === 'object' ? parsed.value : parsed;
100-
result.valid = true;
101+
101102
if (typeof parsed === 'object') {
102-
result.start = range.start + parsed.start;
103-
result.end = range.start + parsed.end;
103+
result.value = parsed.value;
104+
result.valid = parsed.valid ?? true;
105+
if (parsed.start !== undefined) {
106+
result.start = range.start + parsed.start;
107+
}
108+
if (parsed.end !== undefined) {
109+
result.end = range.start + parsed.end;
110+
}
111+
if (parsed.valid === false && parsed.error) {
112+
result.error = parsed.error;
113+
}
114+
} else {
115+
result.value = parsed;
116+
result.valid = true;
104117
}
105118
} catch (error) {
106119
result.error = error.message;

src/parsers/__tests__/check.test.ts

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,62 @@ import { expect, test } from 'vitest';
33
import { check, computeCheckDigit } from '../check.ts';
44

55
test('check digits', () => {
6-
expect(() => check('592166117<231', 8)).not.toThrow();
7-
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();
12-
expect(() => check('592166111<773', 4)).toThrow(/invalid check digit/);
6+
expect(check('592166117<231', '8')).toStrictEqual({
7+
valid: true,
8+
error: null,
9+
});
10+
expect(check('592166111<773', '5')).toStrictEqual({
11+
valid: true,
12+
error: null,
13+
});
14+
expect(check('007666667<ZZ0', '0')).toStrictEqual({
15+
valid: true,
16+
error: null,
17+
});
18+
expect(check('007666667ZZ0', '0')).toStrictEqual({
19+
valid: true,
20+
error: null,
21+
});
22+
expect(check('007777779ZZ9', '2')).toStrictEqual({
23+
valid: true,
24+
error: null,
25+
});
26+
expect(check('600001795015', '2')).toStrictEqual({
27+
valid: true,
28+
error: null,
29+
});
30+
expect(check('592166111<773', '4')).toStrictEqual({
31+
valid: false,
32+
error: 'invalid check digit: 4. Must be 5',
33+
});
1334
});
1435

15-
test('compute embedded TD1 check digit', () => {
16-
expect(computeCheckDigit('123456789AAB')).toBe(8);
17-
expect(computeCheckDigit('123456789<AAB')).toBe(4);
36+
test('compute embedded TD1 check digit - undetermined', () => {
37+
// https://www.consilium.europa.eu/prado/en/PRT-BO-04001/index.html
38+
// I<PRT007666667<ZZ00<<<<<<<<<<<
39+
expect(computeCheckDigit('007666667<ZZ0')).toBe(0);
40+
expect(computeCheckDigit('007666667ZZ0')).toBe(0);
41+
});
42+
43+
test('compute embedded TD1 check digit - must include < character', () => {
44+
// https://www.consilium.europa.eu/prado/en/BEL-BO-03003/index.html
45+
// IDBEL000590240<6013<<<<<<<<<<<
46+
expect(computeCheckDigit('000590240<601')).toBe(3);
47+
expect(computeCheckDigit('000590240601')).not.toBe(3);
48+
});
49+
50+
test('compute embedded TD1 check digit - must not include < character', () => {
51+
// https://www.consilium.europa.eu/prado/en/BEL-BO-11005/index.html
52+
// IDBEL600001795<0152<<<<<<<<<<<
53+
expect(computeCheckDigit('600001795015')).toBe(2);
54+
expect(computeCheckDigit('600001795<015')).not.toBe(2);
55+
});
56+
57+
test('embedded check digit - ICAO', () => {
58+
// https://www.icao.int/sites/default/files/publications/DocSeries/9303_p11_cons_en.pdf
59+
// Page 88
60+
// I<UTOD23145890<7349<<<<<<<<<<<
61+
// The ICAO document says it uses the number without the '<' character, but both yield the same result.
62+
expect(computeCheckDigit('D23145890734')).toBe(9);
63+
expect(computeCheckDigit('D23145890<734')).toBe(9);
1864
});

src/parsers/check.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@ export function computeCheckDigit(string: string) {
1313
return code % 10;
1414
}
1515

16-
export function check(string: string, value: string | number) {
16+
export function check(string: string, input: string) {
1717
const code = computeCheckDigit(string);
18-
if (code !== Number(value)) {
19-
throw new Error(`invalid check digit: ${value}. Must be ${code}`);
20-
}
18+
const valid = code === Number(input);
19+
return {
20+
valid,
21+
error: valid ? null : `invalid check digit: ${input}. Must be ${code}`,
22+
};
2123
}

src/parsers/parseCompositeCheckDigit.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ export default function parseCompositeCheckDigit(
55
...sources: string[]
66
) {
77
const source = sources.join('');
8-
check(source, checkDigit);
9-
return checkDigit;
8+
const checkResult = check(source, checkDigit);
9+
return {
10+
value: checkDigit,
11+
...checkResult,
12+
};
1013
}

src/parsers/parseDateCheckDigit.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { check } from './check.ts';
22

33
export default function parseCheckDigit(checkDigit: string, value: string) {
4-
check(value, checkDigit);
5-
return checkDigit;
4+
return {
5+
value: checkDigit,
6+
...check(value, checkDigit),
7+
};
68
}

0 commit comments

Comments
 (0)