diff --git a/README.md b/README.md index 59d0e6e..3e43a18 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,12 @@ Object mapping field names to their respective value. The value is set to `null` if it is invalid. The value may be different from the raw value. For example, `result.fields.sex` will be "male" when the raw value was "M". +##### result.documentNumber + +The document number, as can be found in the visual elements of the document, outside the MRZ. For some documents, it may +be composed of multiple parsed fields. It may also not include the MRZ field named `documentNumber`. If any of the used +fields is invalid, this field will be set to `null`. + ##### result.details Array of objects describing all parsed fields. Its structure is: @@ -113,11 +119,11 @@ https://www.icao.int/publications/pages/publication.aspx?docnum=9303 ### Swiss driving license -http://www.astra2.admin.ch/media/pdfpub/2003-10-15_2262_f.pdf +https://www.astra.admin.ch/dam/astra/fr/dokumente/dokumente-strassenverkehr/kreisschreiben/ch-fak.pdf.download.pdf/Le%20permis%20de%20conduire%20suisse%20format%20carte%20de%20cr%C3%A9dit%20(PCC).pdf ### French national id -https://fr.wikipedia.org/wiki/Carte_nationale_d%27identit%C3%A9_en_France#Codage_Bande_MRZ_(lecture_optique) +https://fr.wikipedia.org/wiki/Carte_nationale_d%27identit%C3%A9_en_France#Codage_bande_%C3%A0_lecture_optique ## License diff --git a/src/parse/__tests__/frenchDrivingLicense.test.ts b/src/parse/__tests__/frenchDrivingLicense.test.ts index 66a4f9a..91ddfff 100644 --- a/src/parse/__tests__/frenchDrivingLicense.test.ts +++ b/src/parse/__tests__/frenchDrivingLicense.test.ts @@ -6,9 +6,12 @@ describe('parse French Driving License', () => { 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).toMatchObject({ + format: 'FRENCH_DRIVING_LICENSE', + valid: true, + documentNumber: result.fields.documentNumber, + }); + expect(result.fields).toStrictEqual({ documentCode: 'D1', issuingState: 'FRA', @@ -18,6 +21,8 @@ describe('parse French Driving License', () => { lastName: 'MARTIN', compositeCheckDigit: '9', }); + + expect(result.details.filter((a) => !a.valid)).toHaveLength(0); }); it('Use autocorrect', () => { diff --git a/src/parse/__tests__/frenchNationalId.test.ts b/src/parse/__tests__/frenchNationalId.test.ts index d56464e..d8ff6ef 100644 --- a/src/parse/__tests__/frenchNationalId.test.ts +++ b/src/parse/__tests__/frenchNationalId.test.ts @@ -8,9 +8,13 @@ describe('parse French National Id', () => { ]; const result = parse(MRZ); - expect(result.format).toBe('FRENCH_NATIONAL_ID'); - // expect(result.valid).toStrictEqual(true); - expect(result.details.filter((a) => !a.valid)).toHaveLength(0); + + expect(result).toMatchObject({ + format: 'FRENCH_NATIONAL_ID', + valid: true, + documentNumber: '1710GVA12345', + }); + expect(result.fields).toStrictEqual({ documentCode: 'ID', issuingState: 'FRA', @@ -26,21 +30,59 @@ describe('parse French National Id', () => { sex: 'female', compositeCheckDigit: '2', }); + + expect(result.details.filter((a) => !a.valid)).toHaveLength(0); }); + + it('invalid MRZ', () => { + const MRZ = [ + 'IDFRATEST !a.valid)).toHaveLength(3); + }); + it('Use autocorrect', () => { const MRZ = [ 'IDFRATEST autocorrect), ).toStrictEqual([ diff --git a/src/parse/__tests__/swissDrivingLicense.test.ts b/src/parse/__tests__/swissDrivingLicense.test.ts index cd70e5d..fab5b4f 100644 --- a/src/parse/__tests__/swissDrivingLicense.test.ts +++ b/src/parse/__tests__/swissDrivingLicense.test.ts @@ -9,8 +9,25 @@ describe('parse Swiss Driving License', () => { ]; const result = parse(MRZ); - expect(result.format).toBe('SWISS_DRIVING_LICENSE'); - expect(result.valid).toBe(true); + + expect(result).toMatchObject({ + format: 'SWISS_DRIVING_LICENSE', + valid: true, + documentNumber: '305142128097', + }); + + expect(result.fields).toStrictEqual({ + documentNumber: 'AAA001D', + languageCode: 'D', + documentCode: 'FA', + issuingState: 'CHE', + pinCode: '305142128', + versionNumber: '097', + birthDate: '800126', + firstName: 'FABIENNE', + lastName: 'MARCHAND', + }); + expect(result.details.filter((a) => !a.valid)).toHaveLength(0); expect(result.details[0]).toStrictEqual({ label: 'Document number', @@ -41,29 +58,36 @@ describe('parse Swiss Driving License', () => { end: 18, autocorrect: [], }); + }); + + it('invalid text', () => { + const MRZ = [ + 'AAA001D<<', + 'FACHE305142128?97<<800126<<<<<', + 'M4RCHAND< { - const MRZ = [ - 'AAA001D<<', - 'FACHE305142128097<<800126<<<<<', - 'M4RCHAND< !a.valid)).toHaveLength(2); + expect(result.details.filter((a) => !a.valid)).toHaveLength(3); expect(result.details[0]).toStrictEqual({ label: 'Document number', field: 'documentNumber', @@ -103,17 +127,6 @@ describe('parse Swiss Driving License', () => { start: 0, end: 8, }); - expect(result.fields).toStrictEqual({ - documentNumber: 'AAA001D', - languageCode: 'D', - documentCode: 'FA', - issuingState: 'CHE', - pinCode: '305142128', - versionNumber: '097', - birthDate: '800126', - firstName: null, - lastName: null, - }); }); it('Use autocorrect', () => { const MRZ = [ @@ -121,16 +134,18 @@ describe('parse Swiss Driving License', () => { 'FACHE305142128097<<800126<<<<<', 'MARCHAND< autocorrect), ).toStrictEqual([ diff --git a/src/parse/__tests__/td1.test.ts b/src/parse/__tests__/td1.test.ts index 5117aab..4fc0eda 100644 --- a/src/parse/__tests__/td1.test.ts +++ b/src/parse/__tests__/td1.test.ts @@ -2,17 +2,20 @@ import parse from '../parse'; describe('parse TD1', () => { it('swiss ID - valid', () => { - const data = [ + const MRZ = [ 'IDCHEA1234567<6<<<<<<<<<<<<<<<', '7510256M2009018CHE<<<<<<<<<<<8', 'SMITH< { ]; const result = parse(MRZ); - expect(result.details.filter((a) => !a.valid)).toHaveLength(2); + + expect(result).toMatchObject({ + format: 'TD1', + valid: false, + documentNumber: result.fields.documentNumber, + }); + expect(result.fields).toStrictEqual({ firstName: 'ANNA MARIA', lastName: 'ERIKSSON', @@ -68,7 +77,8 @@ describe('parse TD1', () => { optional2: '', compositeCheckDigit: '1', }); - expect(result.valid).toBe(false); + + expect(result.details.filter((a) => !a.valid)).toHaveLength(2); expect(result.details.find((a) => a.field === 'issuingState')?.valid).toBe( false, ); @@ -96,8 +106,15 @@ describe('parse TD1', () => { '7408122F1204159UTO<<<<<<<<<<<8', 'ERIKSSON< !f.valid)).toHaveLength(2); const documentNumberDetails = result.details.find( (d) => d.field === 'documentNumber', @@ -139,26 +156,36 @@ describe('parse TD1', () => { ]; const result = parse(MRZ); - expect(result.valid).toBe(true); + + expect(result).toMatchObject({ + format: 'TD1', + valid: true, + documentNumber: result.fields.documentNumber, + }); + expect(result.fields.lastName).toBe(''); expect(result.fields.firstName).toBe('ANNA MARIA'); }); + it('Use autocorrection', () => { - const data = [ + const MRZ = [ 'IDCHEA1234567<6<<<<<<<<<<<<<<<', '7510256M2009018CHE<<<<<<<<<<<8', 'SMITH< autocorrect), ).toStrictEqual([ diff --git a/src/parse/__tests__/td2.test.ts b/src/parse/__tests__/td2.test.ts index e7fc787..be3bb3c 100644 --- a/src/parse/__tests__/td2.test.ts +++ b/src/parse/__tests__/td2.test.ts @@ -8,12 +8,13 @@ describe('parse TD2', () => { ]; const result = parse(MRZ); - const failed = result.details.filter((a) => !a.valid); + expect(result).toMatchObject({ format: 'TD2', valid: false, + documentNumber: result.fields.documentNumber, }); - expect(failed).toHaveLength(2); + expect(result.fields).toStrictEqual({ firstName: 'ANNA MARIA', lastName: 'ERIKSSON', @@ -30,22 +31,28 @@ describe('parse TD2', () => { compositeCheckDigit: '6', optional: '', }); - expect(result.valid).toBe(false); + + const failed = result.details.filter((a) => !a.valid); + expect(failed).toHaveLength(2); }); + it('Use autocorrect', () => { const MRZ = [ 'I autocorrect), ).toStrictEqual([ diff --git a/src/parse/__tests__/td3.test.ts b/src/parse/__tests__/td3.test.ts index 7fbee79..6fa0843 100644 --- a/src/parse/__tests__/td3.test.ts +++ b/src/parse/__tests__/td3.test.ts @@ -8,13 +8,13 @@ describe('parse TD3', () => { ]; const result = parse(MRZ); + expect(result).toMatchObject({ - valid: false, format: 'TD3', + valid: false, + documentNumber: result.fields.documentNumber, }); - expect(result.valid).toBe(false); - const errors = result.details.filter((a) => !a.valid); - expect(errors).toHaveLength(2); + expect(result.fields).toStrictEqual({ documentCode: 'P', firstName: 'ANNA MARIA', @@ -33,6 +33,9 @@ describe('parse TD3', () => { compositeCheckDigit: '0', }); + const errors = result.details.filter((a) => !a.valid); + expect(errors).toHaveLength(2); + const personalNumberDetails = result.details.find( (d) => d.field === 'personalNumber', ); @@ -69,7 +72,13 @@ describe('parse TD3', () => { ]; const result = parse(MRZ); - expect(result.valid).toBe(true); + + expect(result).toMatchObject({ + format: 'TD3', + valid: true, + documentNumber: result.fields.documentNumber, + }); + expect(result.fields).toStrictEqual({ documentCode: 'P', issuingState: 'D', @@ -94,8 +103,15 @@ describe('parse TD3', () => { 'P { 'POCHNABULIKEMU< { 'PTCHNCESHI< { 'P autocorrect), ).toStrictEqual([ diff --git a/src/parse/getDocumentNumber.ts b/src/parse/getDocumentNumber.ts new file mode 100644 index 0000000..81f6550 --- /dev/null +++ b/src/parse/getDocumentNumber.ts @@ -0,0 +1,39 @@ +import { FieldRecords, MRZFormat } from '../types'; + +export function getDocumentNumber(format: MRZFormat, fields: FieldRecords) { + switch (format) { + case 'TD1': + case 'TD2': + case 'TD3': + case 'FRENCH_DRIVING_LICENSE': + return buildDocumentNumber(fields.documentNumber); + case 'FRENCH_NATIONAL_ID': + return buildDocumentNumber( + fields.issueDate, + fields.administrativeCode2, + fields.documentNumber, + ); + case 'SWISS_DRIVING_LICENSE': + return buildDocumentNumber(fields.pinCode, fields.versionNumber); + default: + assertUnreachableFormat(format); + } +} + +function buildDocumentNumber( + ...parts: Array +): string | null { + let result = ''; + for (const part of parts) { + if (!part) { + return null; + } + result += part; + } + return result; +} + +function assertUnreachableFormat(format: never): never { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + throw new Error(`unrecognized format: ${format}`); +} diff --git a/src/parse/getResult.ts b/src/parse/getResult.ts index 35e4150..fa4124b 100644 --- a/src/parse/getResult.ts +++ b/src/parse/getResult.ts @@ -2,6 +2,7 @@ import { FormatType } from '../formats'; import { Autocorrect, Details, FieldRecords, ParseResult } from '../types'; import { CreateFieldParserResult } from './createFieldParser'; +import { getDocumentNumber } from './getDocumentNumber'; import { ParseMRZOptions } from './parse'; function getDetails( @@ -71,6 +72,7 @@ export function getResult( format, details, fields: fields.fields, + documentNumber: getDocumentNumber(format, fields.fields), valid: fields.allValid, }; } diff --git a/src/types.ts b/src/types.ts index 3e85fb0..255d43c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -28,6 +28,7 @@ export interface ParseResult { format: MRZFormat; valid: boolean; fields: FieldRecords; + documentNumber: string | null; details: Details[]; }