From 5dcc808514ba77c4fa022df5a85eb858def08141 Mon Sep 17 00:00:00 2001 From: Daniel Kostro Date: Tue, 10 Mar 2026 11:33:14 +0100 Subject: [PATCH 1/2] 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 --- src/parsers/__tests__/check.test.ts | 31 +++++++++++++++-- src/parsers/parseDocumentNumberCheckDigit.ts | 35 +++++++++----------- 2 files changed, 44 insertions(+), 22 deletions(-) diff --git a/src/parsers/__tests__/check.test.ts b/src/parsers/__tests__/check.test.ts index 82fcf66..7c998e4 100644 --- a/src/parsers/__tests__/check.test.ts +++ b/src/parsers/__tests__/check.test.ts @@ -12,7 +12,32 @@ test('check digits', () => { expect(() => check('592166111<773', 4)).toThrow(/invalid check digit/); }); -test('compute embedded TD1 check digit', () => { - expect(computeCheckDigit('123456789AAB')).toBe(8); - expect(computeCheckDigit('123456789 { + // https://www.consilium.europa.eu/prado/en/PRT-BO-04001/index.html + // I { + // 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 Date: Tue, 10 Mar 2026 16:25:49 +0100 Subject: [PATCH 2/2] fix: ensure start and end of check digit is correct in field details Closes: https://github.com/cheminfo/mrz/issues/72 --- src/parse/__tests__/td1.test.ts | 112 ++++++++++++++++++- src/parse/__tests__/td3.test.ts | 38 +++++++ src/parse/createFieldParser.ts | 25 ++++- src/parsers/__tests__/check.test.ts | 35 ++++-- src/parsers/check.ts | 10 +- src/parsers/parseCompositeCheckDigit.ts | 7 +- src/parsers/parseDateCheckDigit.ts | 6 +- src/parsers/parseDocumentNumberCheckDigit.ts | 25 +++-- src/parsers/parsePersonalNumberCheckDigit.ts | 12 +- 9 files changed, 235 insertions(+), 35 deletions(-) diff --git a/src/parse/__tests__/td1.test.ts b/src/parse/__tests__/td1.test.ts index 03e58f1..cec6f47 100644 --- a/src/parse/__tests__/td1.test.ts +++ b/src/parse/__tests__/td1.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest'; +import { computeCheckDigit } from '../../parsers/check.ts'; import parse from '../parse.ts'; describe('parse TD1', () => { @@ -336,7 +337,7 @@ describe('parse TD1', () => { }); }); - it('parse document number', () => { + it('Document number field details', () => { const MRZ = [ 'I { 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', @@ -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< !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< !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`, + }); }); }); diff --git a/src/parse/__tests__/td3.test.ts b/src/parse/__tests__/td3.test.ts index 1b8f018..e5ff434 100644 --- a/src/parse/__tests__/td3.test.ts +++ b/src/parse/__tests__/td3.test.ts @@ -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( @@ -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 !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 ParseResult | string; @@ -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; diff --git a/src/parsers/__tests__/check.test.ts b/src/parsers/__tests__/check.test.ts index 7c998e4..364d8dd 100644 --- a/src/parsers/__tests__/check.test.ts +++ b/src/parsers/__tests__/check.test.ts @@ -3,13 +3,34 @@ 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 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 { diff --git a/src/parsers/check.ts b/src/parsers/check.ts index af1c12f..25ce4fb 100644 --- a/src/parsers/check.ts +++ b/src/parsers/check.ts @@ -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}`, + }; } diff --git a/src/parsers/parseCompositeCheckDigit.ts b/src/parsers/parseCompositeCheckDigit.ts index 132445a..19aee4f 100644 --- a/src/parsers/parseCompositeCheckDigit.ts +++ b/src/parsers/parseCompositeCheckDigit.ts @@ -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, + }; } diff --git a/src/parsers/parseDateCheckDigit.ts b/src/parsers/parseDateCheckDigit.ts index fe983d6..d0ba53f 100644 --- a/src/parsers/parseDateCheckDigit.ts +++ b/src/parsers/parseDateCheckDigit.ts @@ -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), + }; } diff --git a/src/parsers/parseDocumentNumberCheckDigit.ts b/src/parsers/parseDocumentNumberCheckDigit.ts index 53f2869..7124f17 100644 --- a/src/parsers/parseDocumentNumberCheckDigit.ts +++ b/src/parsers/parseDocumentNumberCheckDigit.ts @@ -1,4 +1,4 @@ -import { check, computeCheckDigit } from './check.ts'; +import { check } from './check.ts'; export default function parseDocumentNumberCheckDigit( checkDigit: string, @@ -21,15 +21,22 @@ export default function parseDocumentNumberCheckDigit( // The specificiation is unclear about which one should be used. // See ICAO Doc 9303 Part 11 (https://www.icao.int/sites/default/files/publications/DocSeries/9303_p11_cons_en.pdf) // Page 88 has an example, but it yields the same check digit with or without the '<' character. - const isValid = - computeCheckDigit(`${source}<${tail}`) === Number(embeddedDigit); - if (isValid) { - return result; + let checkDigit = check(`${source}<${tail}`, embeddedDigit); + if (checkDigit.valid) { + return { + ...result, + ...checkDigit, + }; } - check(`${source}${tail}`, embeddedDigit); - return result; + checkDigit = check(`${source}${tail}`, embeddedDigit); + return { + ...result, + ...checkDigit, + }; } else { - check(source, checkDigit); - return checkDigit; + return { + value: checkDigit, + ...check(source, checkDigit), + }; } } diff --git a/src/parsers/parsePersonalNumberCheckDigit.ts b/src/parsers/parsePersonalNumberCheckDigit.ts index 7e73a81..19bbabc 100644 --- a/src/parsers/parsePersonalNumberCheckDigit.ts +++ b/src/parsers/parsePersonalNumberCheckDigit.ts @@ -8,11 +8,17 @@ export function parsePersonalNumberCheckDigit( const cleanNumber = cleanText(personalNumber); if (cleanNumber === '') { if (checkDigit !== '<' && checkDigit !== '0') { - throw new Error(`invalid check digit ${checkDigit}: must be 0 or <`); + return { + value: checkDigit, + valid: false, + error: `invalid check digit ${checkDigit}: must be 0 or <`, + }; } else { return checkDigit; } } - check(personalNumber, checkDigit); - return checkDigit; + return { + value: checkDigit, + ...check(personalNumber, checkDigit), + }; }