Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 110 additions & 2 deletions src/parse/__tests__/td1.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, expect, it } from 'vitest';

import { computeCheckDigit } from '../../parsers/check.ts';
import parse from '../parse.ts';

describe('parse TD1', () => {
Expand Down Expand Up @@ -336,7 +337,7 @@ describe('parse TD1', () => {
});
});

it('parse document number', () => {
it('Document number field details', () => {
const MRZ = [
'I<UTOD23145890<1233<<<<<<<<<<<',
'7408122F1204159UTO<<<<<<<<<<<2',
Expand All @@ -351,7 +352,7 @@ describe('parse TD1', () => {
documentNumber: result.fields.documentNumber,
});

expect(result.details.filter((f) => !f.valid)).toHaveLength(2);
expect(result.details.filter((item) => !item.valid)).toHaveLength(2);

const documentNumberDetails = result.details.find(
(d) => d.field === 'documentNumber',
Expand Down Expand Up @@ -550,5 +551,112 @@ describe('parse TD1', () => {
lastName: 'REINARTZ',
firstName: 'ULRIKE KATIA E',
});

const detailedCheckDigit = result.details.find(
(item) => item.field === 'documentNumberCheckDigit',
);
expect(detailedCheckDigit).toMatchObject({
valid: true,
start: 18,
end: 19,
value: '3',
});
});
it('Belgium ID BEL-BO-03003, with wrong document number check digit', () => {
// source: https://www.consilium.europa.eu/prado/en/BEL-BO-03003/index.html
// This Belgian ID has the document number check digit embedded in optional1
const MRZ = [
'IDBEL000590240<6016<<<<<<<<<<<',
'8512017F1311048BEL851201002007',
'REINARTZ<<ULRIKE<KATIA<E<<<<<<',
];

const result = parse(MRZ);

expect(result).toMatchObject({
format: 'TD1',
valid: false,
documentNumber: result.fields.documentNumber,
});

const expectedCheckDigit = computeCheckDigit('000590240601');

const wrongFields = result.details.filter((item) => !item.valid);
expect(wrongFields).toHaveLength(2);

const documentNumberDetails = wrongFields.find(
(item) => item.field === 'documentNumberCheckDigit',
);

expect(documentNumberDetails).toStrictEqual({
valid: false,
start: 18,
end: 19,
value: '6',
field: 'documentNumberCheckDigit',
label: 'Document number check digit',
line: 0,
ranges: [
{
line: 0,
start: 14,
end: 15,
raw: '<',
},
{
line: 0,
start: 5,
end: 14,
raw: '000590240',
},
{
line: 0,
start: 15,
end: 30,
raw: '6016<<<<<<<<<<<',
},
],
autocorrect: [],
error: `invalid check digit: 6. Must be ${expectedCheckDigit}`,
});

const compositeCheckDigitDetails = wrongFields.find(
(item) => item.field === 'compositeCheckDigit',
);

expect(compositeCheckDigitDetails).toBeDefined();
});

it('Belgium ID BEL-BO-03003, with wrong birth date check digit', () => {
// source: https://www.consilium.europa.eu/prado/en/BEL-BO-03003/index.html
// The birthdate check digit was changed
const MRZ = [
'IDBEL000590240<6013<<<<<<<<<<<',
'8512018F1311048BEL851201002007',
'REINARTZ<<ULRIKE<KATIA<E<<<<<<',
];

const result = parse(MRZ);

expect(result).toMatchObject({
format: 'TD1',
valid: false,
documentNumber: result.fields.documentNumber,
});

const wrongDetails = result.details.filter((item) => !item.valid);
expect(wrongDetails).toHaveLength(2);

const birthDateDetails = wrongDetails.find(
(item) => item.field === 'birthDateCheckDigit',
);

expect(birthDateDetails).toMatchObject({
valid: false,
start: 6,
end: 7,
value: '8',
error: `invalid check digit: 8. Must be 7`,
});
});
});
38 changes: 38 additions & 0 deletions src/parse/__tests__/td3.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ describe('parse TD3', () => {

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

// Issuing state and nationality
expect(errors).toHaveLength(2);

const personalNumberDetails = result.details.find(
Expand Down Expand Up @@ -69,6 +70,43 @@ describe('parse TD3', () => {
});
});

it('Utopia example - wrong personal number check digit', () => {
// The same example as the previous one, but the 2nd character from the end was changed from a '1' to a '2'.
const MRZ = [
'P<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<',
'L898902C36UTO7408122F1204159ZE184226B<<<<<20',
];

const result = parse(MRZ);

expect(result).toMatchObject({
format: 'TD3',
valid: false,
documentNumber: result.fields.documentNumber,
});

const wrongDetails = result.details.filter((item) => !item.valid);
// Issuing state, nationality, personal number check digit and composite check digit
expect(wrongDetails).toHaveLength(4);
expect(
wrongDetails.find((item) => item.field === 'personalNumberCheckDigit'),
).toMatchObject({
start: 42,
end: 43,
value: '2',
error: 'invalid check digit: 2. Must be 1',
});

expect(
wrongDetails.find((item) => item.field === 'compositeCheckDigit'),
).toMatchObject({
start: 43,
end: 44,
value: '0',
error: 'invalid check digit: 0. Must be 1',
});
});

it('German example', () => {
const MRZ = [
'P<D<<MUSTERMANN<<ERIKA<<<<<<<<<<<<<<<<<<<<<<',
Expand Down
25 changes: 19 additions & 6 deletions src/parse/createFieldParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import { autoCorrection } from './autoCorrection.ts';

interface ParseResult {
value: string;
start: number;
end: number;
start?: number;
end?: number;
valid?: boolean;
error?: string | null;
}

type Parser = (source: string, ...related: string[]) => ParseResult | string;
Expand Down Expand Up @@ -96,11 +98,22 @@ export default function createFieldParser(
result.end = range.end;
try {
const parsed = fieldOptions.parser(source, ...textRelated);
result.value = typeof parsed === 'object' ? parsed.value : parsed;
result.valid = true;

if (typeof parsed === 'object') {
result.start = range.start + parsed.start;
result.end = range.start + parsed.end;
result.value = parsed.value;
result.valid = parsed.valid ?? true;
if (parsed.start !== undefined) {
result.start = range.start + parsed.start;
}
if (parsed.end !== undefined) {
result.end = range.start + parsed.end;
}
if (parsed.valid === false && parsed.error) {
result.error = parsed.error;
}
} else {
result.value = parsed;
result.valid = true;
}
} catch (error) {
result.error = error.message;
Expand Down
66 changes: 56 additions & 10 deletions src/parsers/__tests__/check.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,62 @@ import { expect, test } from 'vitest';
import { check, computeCheckDigit } from '../check.ts';

test('check digits', () => {
expect(() => check('592166117<231', 8)).not.toThrow();
expect(() => check('592166111<773', 5)).not.toThrow();
expect(() => check('007666667<ZZ0', 0)).not.toThrow();
expect(() => check('007666667ZZ0', 0)).not.toThrow();
expect(() => check('007777779ZZ9', 2)).not.toThrow();
expect(() => check('600001795015', 2)).not.toThrow();
expect(() => check('592166111<773', 4)).toThrow(/invalid check digit/);
expect(check('592166117<231', '8')).toStrictEqual({
valid: true,
error: null,
});
expect(check('592166111<773', '5')).toStrictEqual({
valid: true,
error: null,
});
expect(check('007666667<ZZ0', '0')).toStrictEqual({
valid: true,
error: null,
});
expect(check('007666667ZZ0', '0')).toStrictEqual({
valid: true,
error: null,
});
expect(check('007777779ZZ9', '2')).toStrictEqual({
valid: true,
error: null,
});
expect(check('600001795015', '2')).toStrictEqual({
valid: true,
error: null,
});
expect(check('592166111<773', '4')).toStrictEqual({
valid: false,
error: 'invalid check digit: 4. Must be 5',
});
});

test('compute embedded TD1 check digit', () => {
expect(computeCheckDigit('123456789AAB')).toBe(8);
expect(computeCheckDigit('123456789<AAB')).toBe(4);
test('compute embedded TD1 check digit - undetermined', () => {
// https://www.consilium.europa.eu/prado/en/PRT-BO-04001/index.html
// I<PRT007666667<ZZ00<<<<<<<<<<<
expect(computeCheckDigit('007666667<ZZ0')).toBe(0);
expect(computeCheckDigit('007666667ZZ0')).toBe(0);
});

test('compute embedded TD1 check digit - must include < character', () => {
// https://www.consilium.europa.eu/prado/en/BEL-BO-03003/index.html
// IDBEL000590240<6013<<<<<<<<<<<
expect(computeCheckDigit('000590240<601')).toBe(3);
expect(computeCheckDigit('000590240601')).not.toBe(3);
});

test('compute embedded TD1 check digit - must not include < character', () => {
// https://www.consilium.europa.eu/prado/en/BEL-BO-11005/index.html
// IDBEL600001795<0152<<<<<<<<<<<
expect(computeCheckDigit('600001795015')).toBe(2);
expect(computeCheckDigit('600001795<015')).not.toBe(2);
});

test('embedded check digit - ICAO', () => {
// https://www.icao.int/sites/default/files/publications/DocSeries/9303_p11_cons_en.pdf
// Page 88
// I<UTOD23145890<7349<<<<<<<<<<<
// The ICAO document says it uses the number without the '<' character, but both yield the same result.
expect(computeCheckDigit('D23145890734')).toBe(9);
expect(computeCheckDigit('D23145890<734')).toBe(9);
});
10 changes: 6 additions & 4 deletions src/parsers/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ export function computeCheckDigit(string: string) {
return code % 10;
}

export function check(string: string, value: string | number) {
export function check(string: string, input: string) {
const code = computeCheckDigit(string);
if (code !== Number(value)) {
throw new Error(`invalid check digit: ${value}. Must be ${code}`);
}
const valid = code === Number(input);
return {
valid,
error: valid ? null : `invalid check digit: ${input}. Must be ${code}`,
};
}
7 changes: 5 additions & 2 deletions src/parsers/parseCompositeCheckDigit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ export default function parseCompositeCheckDigit(
...sources: string[]
) {
const source = sources.join('');
check(source, checkDigit);
return checkDigit;
const checkResult = check(source, checkDigit);
return {
value: checkDigit,
...checkResult,
};
}
6 changes: 4 additions & 2 deletions src/parsers/parseDateCheckDigit.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { check } from './check.ts';

export default function parseCheckDigit(checkDigit: string, value: string) {
check(value, checkDigit);
return checkDigit;
return {
value: checkDigit,
...check(value, checkDigit),
};
}
Loading