From f37f6e69e04914558170cb65dc0b78c547d7a522 Mon Sep 17 00:00:00 2001 From: tpoisseau <22891227+tpoisseau@users.noreply.github.com> Date: Thu, 11 Apr 2024 15:06:17 +0200 Subject: [PATCH] feat: parse french driving licences * /!\ target es2021 (instead es2020) to be able to use Intl.ListFormat * add documentCodeAlphaNumTemplate * add tests and fix errors.test.ts * without proper resources, I'm not sure about the real existence of A1, A2 or B1, but I let the parser flexible Closes: https://github.com/cheminfo/mrz/issues/54 --- src/formats.ts | 1 + src/parse/__tests__/errors.test.ts | 4 +- .../__tests__/frenchDrivingLicense.test.ts | 33 +++++++++++ src/parse/fieldTemplates.ts | 7 +++ src/parse/frenchDrivingLicence.ts | 31 ++++++++++ src/parse/frenchDrivingLicenceFields.ts | 57 +++++++++++++++++++ src/parse/parse.ts | 12 +++- src/parse/parsers.ts | 2 + .../frenchDrivingLicence/parseDocumentCode.ts | 42 ++++++++++++++ src/types.ts | 1 + tsconfig.json | 2 +- 11 files changed, 188 insertions(+), 4 deletions(-) create mode 100644 src/parse/__tests__/frenchDrivingLicense.test.ts create mode 100644 src/parse/frenchDrivingLicence.ts create mode 100644 src/parse/frenchDrivingLicenceFields.ts create mode 100644 src/parsers/frenchDrivingLicence/parseDocumentCode.ts diff --git a/src/formats.ts b/src/formats.ts index 3c3c257..f0fcc73 100644 --- a/src/formats.ts +++ b/src/formats.ts @@ -8,4 +8,5 @@ export const formats = { TD3: 'TD3', SWISS_DRIVING_LICENSE: 'SWISS_DRIVING_LICENSE', FRENCH_NATIONAL_ID: 'FRENCH_NATIONAL_ID', + FRENCH_DRIVING_LICENSE: 'FRENCH_DRIVING_LICENSE', } as const; diff --git a/src/parse/__tests__/errors.test.ts b/src/parse/__tests__/errors.test.ts index 681d3f3..6af58c1 100644 --- a/src/parse/__tests__/errors.test.ts +++ b/src/parse/__tests__/errors.test.ts @@ -3,7 +3,7 @@ import parse from '../parse'; describe('Bad MRZ', () => { - it('More then 3 lines', () => { + it('More than 3 lines', () => { const MRZ = [ 'IDFRATEST { '1710GVA123451ROBERTA<<<<<<<9112311F2', ]; expect(() => parse(MRZ)).toThrow( - 'unrecognized document format. Input must have two or three lines, found 4', + 'unrecognized document format. Input must have one, two or three lines, found 4', ); }); it('Wrong format', () => { diff --git a/src/parse/__tests__/frenchDrivingLicense.test.ts b/src/parse/__tests__/frenchDrivingLicense.test.ts new file mode 100644 index 0000000..9e311f8 --- /dev/null +++ b/src/parse/__tests__/frenchDrivingLicense.test.ts @@ -0,0 +1,33 @@ +'use strict'; + +import parse from '../parse'; + +describe('parse French Driving License', () => { + it('valid MRZ', () => { + const MRZ = 'D1FRA13AA000026181231MARTIN<<9'; + + const result = parse(MRZ); + + expect(result.format).toBe('FRENCH_DRIVING_LICENSE'); + expect(result.valid).toBe(true); + expect(result.details.filter((a) => !a.valid)).toHaveLength(0); + expect(result.fields).toStrictEqual({ + documentCode: 'D1', + issuingState: 'FRA', + documentNumber: '13AA00002', + documentNumberCheckDigit: '6', + expirationDate: '181231', + lastName: 'MARTIN', + compositeCheckDigit: '9', + }); + }); + + it('Use autocorrect', () => { + const MRZok = 'D1FRA13AA000026181231MARTIN<<9'; + const MRZko = 'D1FRA13AA0000261B1231MART1N<<9'; + + const result = parse(MRZok); + const correctedResult = parse(MRZko, { autocorrect: true }); + expect(correctedResult.fields).toStrictEqual(result.fields); + }); +}); diff --git a/src/parse/fieldTemplates.ts b/src/parse/fieldTemplates.ts index f5b122e..7df5d6f 100644 --- a/src/parse/fieldTemplates.ts +++ b/src/parse/fieldTemplates.ts @@ -34,6 +34,12 @@ const documentCodeTemplate = { type: fieldTypes.ALPHABETIC, } satisfies FieldOptionTemplate; +const documentCodeAlphaNumTemplate = { + label: 'Document code', + field: 'documentCode', + type: fieldTypes.ALPHANUMERIC, +} satisfies FieldOptionTemplate; + const nationalityTemplate = { label: 'Nationality', field: 'nationality', @@ -115,6 +121,7 @@ export { documentNumberTemplate, documentNumberCheckDigitTemplate, documentCodeTemplate, + documentCodeAlphaNumTemplate, nationalityTemplate, sexTemplate, expirationDateTemplate, diff --git a/src/parse/frenchDrivingLicence.ts b/src/parse/frenchDrivingLicence.ts new file mode 100644 index 0000000..8f6e358 --- /dev/null +++ b/src/parse/frenchDrivingLicence.ts @@ -0,0 +1,31 @@ +'use strict'; + +import { formats } from '../formats'; + +import frenchDrivingLicenceFields from './frenchDrivingLicenceFields'; +import { getResult } from './getResult'; +import { ParseMRZOptions } from './parse'; + +const FRENCH_DRIVING_LICENSE = formats.FRENCH_DRIVING_LICENSE; +export default function parseFrenchDrivingLicense( + lines: string[], + options: ParseMRZOptions, +) { + if (lines.length !== 1) { + throw new Error( + `invalid number of lines: ${lines.length}: Must be 1 for ${FRENCH_DRIVING_LICENSE}`, + ); + } + if (lines[0].length !== 30) { + throw new Error( + `invalid number of characters for line 1: ${lines[0].length}. Must be 30 for ${FRENCH_DRIVING_LICENSE}`, + ); + } + + return getResult( + FRENCH_DRIVING_LICENSE, + lines, + frenchDrivingLicenceFields, + options, + ); +} diff --git a/src/parse/frenchDrivingLicenceFields.ts b/src/parse/frenchDrivingLicenceFields.ts new file mode 100644 index 0000000..864faa4 --- /dev/null +++ b/src/parse/frenchDrivingLicenceFields.ts @@ -0,0 +1,57 @@ +'use strict'; + +import parseDocumentCode from '../parsers/frenchDrivingLicence/parseDocumentCode'; +import { parseAlpha } from '../parsers/parseAlpha'; + +import createFieldParser, { FieldOptions } from './createFieldParser'; +import { + compositeCheckDigitTemplate, + documentCodeAlphaNumTemplate, + documentNumberCheckDigitTemplate, + documentNumberTemplate, + expirationDateTemplate, + issuingStateTemplate, + lastNameTemplate, +} from './fieldTemplates'; + +const fields: FieldOptions[] = [ + { + ...documentCodeAlphaNumTemplate, + line: 0, + start: 0, + end: 2, + parser: parseDocumentCode, + }, + { ...issuingStateTemplate, line: 0, start: 2, end: 5 }, + { ...documentNumberTemplate, line: 0, start: 5, end: 14 }, + { + ...documentNumberCheckDigitTemplate, + line: 0, + start: 14, + end: 15, + related: [ + { + line: 0, + start: 0, + end: 14, + }, + ], + }, + { ...expirationDateTemplate, line: 0, start: 15, end: 21 }, + { ...lastNameTemplate, line: 0, start: 21, end: 29, parser: parseAlpha }, + { + ...compositeCheckDigitTemplate, + line: 0, + start: 29, + end: 30, + related: [ + { + line: 0, + start: 0, + end: 29, + }, + ], + }, +]; + +export default fields.map(createFieldParser); diff --git a/src/parse/parse.ts b/src/parse/parse.ts index e8a612e..d06e419 100644 --- a/src/parse/parse.ts +++ b/src/parse/parse.ts @@ -12,6 +12,16 @@ function parseMRZ( ) { const lines = checkLines(inputLines); switch (lines.length) { + case 1: { + switch (lines[0].length) { + case 30: + return parsers.frenchDrivingLicense(lines, options); + default: + throw new Error( + `unrecognized document format. First line of input must have 30 (French Driving License) characters and it has a length of ${lines[0].length}`, + ); + } + } case 2: case 3: { switch (lines[0].length) { @@ -37,7 +47,7 @@ function parseMRZ( } default: throw new Error( - `unrecognized document format. Input must have two or three lines, found ${lines.length}`, + `unrecognized document format. Input must have one, two or three lines, found ${lines.length}`, ); } } diff --git a/src/parse/parsers.ts b/src/parse/parsers.ts index a99ca29..d3113e9 100644 --- a/src/parse/parsers.ts +++ b/src/parse/parsers.ts @@ -1,5 +1,6 @@ 'use strict'; +import parseFrenchDrivingLicense from './frenchDrivingLicence'; import parseFrenchNationalId from './frenchNationalId'; import parseSwissDrivingLicense from './swissDrivingLicense'; import parseTD1 from './td1'; @@ -12,4 +13,5 @@ export const parsers = { td3: parseTD3, swissDrivingLicense: parseSwissDrivingLicense, frenchNationalId: parseFrenchNationalId, + frenchDrivingLicense: parseFrenchDrivingLicense, }; diff --git a/src/parsers/frenchDrivingLicence/parseDocumentCode.ts b/src/parsers/frenchDrivingLicence/parseDocumentCode.ts new file mode 100644 index 0000000..7628fd2 --- /dev/null +++ b/src/parsers/frenchDrivingLicence/parseDocumentCode.ts @@ -0,0 +1,42 @@ +'use strict'; + +/** + * D1 seems to be the most common, + * but with Google image search we can find some B1, A1, A2 + */ +const knownPrefixCode = ['A', 'B', 'D']; + +export default function parseDocumentCode(source: string) { + if (source.length !== 2) { + throw new Error(`invalid document code: ${source}. must be 2 char length`); + } + + const [first, second] = source; + + validateFirstChar(first, source); + validateSecondChar(second, source); + + return `${first}${second}`; +} + +function validateFirstChar(char: string, source: string) { + if (!knownPrefixCode.includes(char)) { + const formatter = new Intl.ListFormat('en-US', { + style: 'short', + type: 'disjunction', + }); + throw new Error( + `invalid document code: ${source}. First character must be ${formatter.format(knownPrefixCode)}`, + ); + } +} + +function validateSecondChar(char: string, source: string) { + const num = parseInt(char, 10); + + if (Number.isNaN(num)) { + throw new Error( + `invalid document code number: ${source}. Second character must be a number`, + ); + } +} diff --git a/src/types.ts b/src/types.ts index 6211151..314f196 100644 --- a/src/types.ts +++ b/src/types.ts @@ -66,4 +66,5 @@ export type MRZFormat = | 'TD3' | 'TD2' | 'FRENCH_NATIONAL_ID' + | 'FRENCH_DRIVING_LICENSE' | 'SWISS_DRIVING_LICENSE'; diff --git a/tsconfig.json b/tsconfig.json index b3f64a1..8b6f075 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,7 +7,7 @@ "outDir": "lib", "sourceMap": true, "strict": true, - "target": "es2020", + "target": "es2021", "useUnknownInCatchVariables": false }, "include": ["./src/**/*"]