Skip to content

Commit

Permalink
feat: parse french driving licences
Browse files Browse the repository at this point in the history
* /!\ 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: #54
  • Loading branch information
tpoisseau committed Apr 11, 2024
1 parent f424c4e commit f37f6e6
Show file tree
Hide file tree
Showing 11 changed files with 188 additions and 4 deletions.
1 change: 1 addition & 0 deletions src/formats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
4 changes: 2 additions & 2 deletions src/parse/__tests__/errors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@
import parse from '../parse';

describe('Bad MRZ', () => {
it('More then 3 lines', () => {
it('More than 3 lines', () => {
const MRZ = [
'IDFRATEST<NAME<<<<<<<<<<<<<<<<0CHE02',
'1710GVA123451ROBERTA<<<<<<<9112311F2',
'1710GVA123451ROBERTA<<<<<<<9112311F2',
'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', () => {
Expand Down
33 changes: 33 additions & 0 deletions src/parse/__tests__/frenchDrivingLicense.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
7 changes: 7 additions & 0 deletions src/parse/fieldTemplates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -115,6 +121,7 @@ export {
documentNumberTemplate,
documentNumberCheckDigitTemplate,
documentCodeTemplate,
documentCodeAlphaNumTemplate,
nationalityTemplate,
sexTemplate,
expirationDateTemplate,
Expand Down
31 changes: 31 additions & 0 deletions src/parse/frenchDrivingLicence.ts
Original file line number Diff line number Diff line change
@@ -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,
);
}
57 changes: 57 additions & 0 deletions src/parse/frenchDrivingLicenceFields.ts
Original file line number Diff line number Diff line change
@@ -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);
12 changes: 11 additions & 1 deletion src/parse/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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}`,
);
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/parse/parsers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use strict';

import parseFrenchDrivingLicense from './frenchDrivingLicence';
import parseFrenchNationalId from './frenchNationalId';
import parseSwissDrivingLicense from './swissDrivingLicense';
import parseTD1 from './td1';
Expand All @@ -12,4 +13,5 @@ export const parsers = {
td3: parseTD3,
swissDrivingLicense: parseSwissDrivingLicense,
frenchNationalId: parseFrenchNationalId,
frenchDrivingLicense: parseFrenchDrivingLicense,
};
42 changes: 42 additions & 0 deletions src/parsers/frenchDrivingLicence/parseDocumentCode.ts
Original file line number Diff line number Diff line change
@@ -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`,
);
}
}
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,5 @@ export type MRZFormat =
| 'TD3'
| 'TD2'
| 'FRENCH_NATIONAL_ID'
| 'FRENCH_DRIVING_LICENSE'
| 'SWISS_DRIVING_LICENSE';
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"outDir": "lib",
"sourceMap": true,
"strict": true,
"target": "es2020",
"target": "es2021",
"useUnknownInCatchVariables": false
},
"include": ["./src/**/*"]
Expand Down

0 comments on commit f37f6e6

Please sign in to comment.