@marketto/codice-fiscale-utils
Version:
TS & JS utilities to handle Italian Codice Fiscale
1 lines • 191 kB
Source Map (JSON)
{"version":3,"file":"index.mjs","sources":["../src/const/cf-offsets.const.ts","../src/date-utils/date-matcher.const.ts","../src/date-utils/date-utils.class.ts","../src/const/matcher.const.ts","../src/enums/crc.enum.ts","../src/enums/birth-month.enum.ts","../src/classes/check-digitizer.class.ts","../src/functions/generator-wrapper.function.ts","../src/enums/gender-weight.enum.ts","../src/enums/omocodes.enum.ts","../src/classes/gender.class.ts","../src/const/logic.const.ts","../src/classes/parser.class.ts","../src/const/error-messages.const.ts","../src/classes/cfu-error.class.ts","../src/classes/pattern.class.ts","../src/classes/cf-mismatch-validator.class.ts","../src/classes/validator.class.ts","../src/index.ts"],"sourcesContent":["export const LASTNAME_OFFSET = 0;\r\nexport const LASTNAME_SIZE = 3;\r\nexport const FIRSTNAME_OFFSET = 3;\r\nexport const FIRSTNAME_SIZE = 3;\r\nexport const YEAR_OFFSET = 6;\r\nexport const YEAR_SIZE = 2;\r\nexport const MONTH_OFFSET = 8;\r\nexport const MONTH_SIZE = 1;\r\nexport const DAY_OFFSET = 9;\r\nexport const DAY_SIZE = 2;\r\nexport const DATE_OFFSET = YEAR_OFFSET;\r\nexport const DATE_SIZE = YEAR_SIZE + MONTH_SIZE + DAY_SIZE;\r\nexport const GENDER_OFFSET = DAY_OFFSET;\r\nexport const GENDER_SIZE = 1;\r\nexport const PLACE_OFFSET = 11;\r\nexport const PLACE_SIZE = 4;\r\nexport const CRC_OFFSET = 15;\r\nexport const CRC_SIZE = 1;\r\nexport const CF_SIZE =\r\n\tLASTNAME_SIZE + FIRSTNAME_SIZE + DATE_SIZE + PLACE_SIZE + CRC_SIZE;\r\n","const YEAR: string = \"[12][0-9]{3}\";\r\nconst MONTH: string = \"0[1-9]|1[0-2]\";\r\nconst DAY: string = \"0[1-9]|[12][0-9]|3[01]\";\r\nconst LEAP_MONTH: string = \"02\";\r\nconst DAYS_30_MONTHS: string = \"0[469]|11\";\r\nconst DAYS_31_MONTHS: string = \"0[13578]|1[02]\";\r\nconst MONTH_DAY: string = `(?:${MONTH})-(?:0[1-9]|[12]\\\\d)|(?:${DAYS_30_MONTHS})-30|(?:${DAYS_31_MONTHS})-3[01]`;\r\nconst HOURS: string = \"[01]\\\\d|2[0-3]\";\r\nconst MINUTES: string = \"[0-5]\\\\d\";\r\nconst SECONDS: string = MINUTES;\r\nconst MILLISECONDS: string = \"\\\\d{3}\";\r\nconst TIMEZONE: string = `Z|[-+](?:${HOURS})(?::?${MINUTES})?`;\r\nconst TIME: string = `(?:${HOURS})(?::${MINUTES}(?::${SECONDS}(?:\\\\.${MILLISECONDS})?)?(?:${TIMEZONE})?)?`;\r\nconst ISO8601_SHORT_DATE: string = `${YEAR}-(?:${MONTH_DAY})(?:T${TIME})?`;\r\nconst ISO8601_DATE_TIME: string = `${YEAR}(?:-(?:(?:${MONTH})|(?:${MONTH_DAY})(?:T${TIME})?))?`;\r\n\r\n/**\r\n * Date Matcher consts\r\n * @property {Object} DATE_VALIDATOR\r\n * @property {string} DATE_VALIDATOR.YEAR Matcher for ISO8601 4 digits year (limited to 1000-2999)\r\n * @property {string} DATE_VALIDATOR.MONTH Matcher for ISO8601 2 digits month (01-12)\r\n * @property {string} DATE_VALIDATOR.DAY Matcher for ISO8601 2 digits day (01-31)\r\n * @property {string} DATE_VALIDATOR.LEAP_MONTH Matcher for ISO8601 2 digits leap month\r\n * @property {string} DATE_VALIDATOR.DAYS_30_MONTHS Matcher for ISO8601 2 digits 30 days month\r\n * @property {string} DATE_VALIDATOR.DAYS_31_MONTHS Matcher for ISO8601 2 digits 31 days month\r\n * @property {string} DATE_VALIDATOR.MONTH_DAY Matcher for ISO8601 2 + 2 digits (28~31)month + day\r\n * @property {string} DATE_VALIDATOR.ISO8601_SHORT_DATE Matcher for ISO8601 date: 4+2+2 digits year + (28~31)month + day\r\n * @property {string} DATE_VALIDATOR.HOURS Matcher for ISO8601 2 digits hours (00-23)\r\n * @property {string} DATE_VALIDATOR.MINUTES Matcher for ISO8601 2 digits minutes (00-59)\r\n * @property {string} DATE_VALIDATOR.SECONDS Matcher for ISO8601 2 digits seconds (00-59)\r\n * @property {string} DATE_VALIDATOR.MILLISECONDS Matcher for ISO8601 3 digits milliseconds (000-999)\r\n * @property {string} DATE_VALIDATOR.TIMEZONE Matcher for ISO8601 timezone (Z or ±## or ±##:## or ±####)\r\n * @property {string} DATE_VALIDATOR.TIME Matcher for ISO8601 for time (T## , T##:## , T##:##:## , T##:##:##.###)\r\n * @property {string} DATE_VALIDATOR.ISO8601_DATE_TIME Matcher for ISO8601 date/time format\r\n */\r\nexport {\r\n\tDAY,\r\n\tDAYS_30_MONTHS,\r\n\tDAYS_31_MONTHS,\r\n\tHOURS,\r\n\tISO8601_DATE_TIME,\r\n\tISO8601_SHORT_DATE,\r\n\tLEAP_MONTH,\r\n\tMILLISECONDS,\r\n\tMINUTES,\r\n\tMONTH,\r\n\tMONTH_DAY,\r\n\tSECONDS,\r\n\tTIME,\r\n\tTIMEZONE,\r\n\tYEAR,\r\n};\r\n","import dayjs, { Dayjs } from \"dayjs\";\r\nimport utc from \"dayjs/plugin/utc\";\r\nimport type DateDay from \"./date-day.type\";\r\nimport { ISO8601_DATE_TIME } from \"./date-matcher.const\";\r\nimport type DateMonth from \"./date-month.type\";\r\nimport type MultiFormatDate from \"./multi-format-date.type\";\r\n\r\ndayjs.extend(utc);\r\n\r\nexport default class DateUtils {\r\n\t/**\r\n\t * Parse a Dated and Gender information to create Date/Gender CF part\r\n\t * @param date Date instance, ISO8601 date string or array of numbers [year, month, day]\r\n\t * @returns Parsed Date or null if not valid\r\n\t */\r\n\tpublic static parseDate(date?: MultiFormatDate | null): Date | null {\r\n\t\tif (\r\n\t\t\t!(\r\n\t\t\t\tdate instanceof Date ||\r\n\t\t\t\t(typeof date === \"string\" &&\r\n\t\t\t\t\tnew RegExp(`^(?:${ISO8601_DATE_TIME})$`).test(date)) ||\r\n\t\t\t\t(Array.isArray(date) &&\r\n\t\t\t\t\tdate.length &&\r\n\t\t\t\t\t!date.some((value) => typeof value !== \"number\" || isNaN(value)))\r\n\t\t\t)\r\n\t\t) {\r\n\t\t\treturn null;\r\n\t\t}\r\n\t\ttry {\r\n\t\t\tlet parsedDate: Dayjs;\r\n\t\t\tif (Array.isArray(date)) {\r\n\t\t\t\tconst [year, month = 0, day = 1] = date;\r\n\t\t\t\tif (month >= 0 && month <= 11 && day > 0 && day <= 31) {\r\n\t\t\t\t\tparsedDate = dayjs.utc(Date.UTC(year, month || 0, day || 1));\r\n\t\t\t\t} else {\r\n\t\t\t\t\treturn null;\r\n\t\t\t\t}\r\n\t\t\t} else {\r\n\t\t\t\tparsedDate = dayjs.utc(date);\r\n\t\t\t}\r\n\t\t\treturn parsedDate.isValid() ? parsedDate.toDate() : null;\r\n\t\t} catch (err) {\r\n\t\t\treturn null;\r\n\t\t}\r\n\t}\r\n\r\n\tpublic static ymdToDate(\r\n\t\tyear?: number | null,\r\n\t\tmonth?: DateMonth | null,\r\n\t\tday?: DateDay | null\r\n\t): Date | null {\r\n\t\treturn this.parseDate([year, month, day] as number[]);\r\n\t}\r\n}\r\n","export const CONSONANT_LIST: string = \"B-DF-HJ-NP-TV-Z\";\r\nexport const VOWEL_LIST: string = \"AEIOU\";\r\nexport const OMOCODE_NUMBER_LIST: string = \"\\\\dLMNP-V\";\r\nexport const OMOCODE_NON_ZERO_NUMBER_LIST: string = \"1-9MNP-V\";\r\nexport const OMOCODE_ZERO_LIST: string = \"0L\";\r\nexport const MONTH_LIST: string = \"A-EHLMPR-T\";\r\nexport const MONTH_30DAYS_LIST: string = \"DHPS\";\r\nexport const MONTH_31DAYS_LIST: string = \"ACELMRT\";\r\nexport const CITY_CODE_LIST: string = \"A-M\";\r\nexport const COUNTRY_CODE_LIST: string = \"Z\";\r\n\r\nexport const CF_NAME_MATCHER: string = `[A-Z][${VOWEL_LIST}][${VOWEL_LIST}X]|[${VOWEL_LIST}]X{2}|[${CONSONANT_LIST}]{2}[A-Z]`;\r\nexport const CF_SURNAME_MATCHER: string = CF_NAME_MATCHER;\r\nexport const CF_FULL_NAME_MATCHER: string = `(?:${CF_NAME_MATCHER}){2}`;\r\n\r\nexport const YEAR_MATCHER: string = `[${OMOCODE_NUMBER_LIST}]{2}`;\r\nexport const LEAP_YEAR_MATCHER: string =\r\n\t\"[02468LNQSU][048LQU]|[13579MPRTV][26NS]\";\r\nexport const MONTH_MATCHER: string = `[${MONTH_LIST}]`;\r\nexport const DAY_2X_MATCHER: string = \"[26NS]\";\r\nexport const DAY_3X_MATCHER: string = \"[37PT]\";\r\nexport const DAY_29_MATCHER: string = `[${OMOCODE_ZERO_LIST}4Q][${OMOCODE_NON_ZERO_NUMBER_LIST}]|[1256MNRS][${OMOCODE_NUMBER_LIST}]`;\r\nexport const DAY_30_MATCHER: string = `[${DAY_3X_MATCHER}][${OMOCODE_ZERO_LIST}]`;\r\nexport const DAY_31_MATCHER: string = `[${DAY_3X_MATCHER}][${OMOCODE_ZERO_LIST}1M]`;\r\n\r\nexport const DAY_MATCHER: string = `(?:${DAY_29_MATCHER}|${DAY_3X_MATCHER}[${OMOCODE_ZERO_LIST}1M])`;\r\nexport const MALE_DAY_MATCHER: string = `(?:[${OMOCODE_ZERO_LIST}][${OMOCODE_NON_ZERO_NUMBER_LIST}]|[12MN][${OMOCODE_NUMBER_LIST}]|[3P][${OMOCODE_ZERO_LIST}1M])`;\r\nexport const FEMALE_DAY_MATCHER: string = `(?:[4Q][${OMOCODE_NON_ZERO_NUMBER_LIST}]|[56RS][${OMOCODE_NUMBER_LIST}]|[7T][${OMOCODE_ZERO_LIST}1M])`;\r\nexport const MONTH_DAY_MATCHER: string = `${MONTH_MATCHER}(?:${DAY_29_MATCHER})|[${MONTH_30DAYS_LIST}]${DAY_30_MATCHER}|[${MONTH_31DAYS_LIST}]${DAY_31_MATCHER}`;\r\nexport const FULL_DATE_MATCHER: string = `${YEAR_MATCHER}(?:${MONTH_MATCHER}(?:[${OMOCODE_ZERO_LIST}4Q][${OMOCODE_NON_ZERO_NUMBER_LIST}]|[15MR][${OMOCODE_NUMBER_LIST}]|${DAY_2X_MATCHER}[0-8LMNP-U])|[${MONTH_30DAYS_LIST}]${DAY_3X_MATCHER}[${OMOCODE_ZERO_LIST}]|[${MONTH_31DAYS_LIST}]${DAY_3X_MATCHER}[${OMOCODE_ZERO_LIST}1M]|[${MONTH_30DAYS_LIST}${MONTH_31DAYS_LIST}]${DAY_2X_MATCHER}[9V])|(?:${LEAP_YEAR_MATCHER})B${DAY_2X_MATCHER}[9V]`;\r\nexport const MALE_FULL_DATE_MATCHER: string = `${YEAR_MATCHER}(?:${MONTH_MATCHER}(?:[${OMOCODE_ZERO_LIST}][${OMOCODE_NON_ZERO_NUMBER_LIST}]|[1M][${OMOCODE_NUMBER_LIST}]|[2N][0-8LMNP-U])|[${MONTH_30DAYS_LIST}][3P][${OMOCODE_ZERO_LIST}]|[${MONTH_31DAYS_LIST}][3P][${OMOCODE_ZERO_LIST}1M]|[${MONTH_30DAYS_LIST}${MONTH_31DAYS_LIST}][2N][9V])|(?:${LEAP_YEAR_MATCHER})B[2N][9V]`;\r\nexport const FEMALE_FULL_DATE_MATCHER: string = `${YEAR_MATCHER}(?:${MONTH_MATCHER}(?:[4Q][${OMOCODE_NON_ZERO_NUMBER_LIST}]|[5R][${OMOCODE_NUMBER_LIST}]|[6S][0-8LMNP-U])|[${MONTH_30DAYS_LIST}][7T][${OMOCODE_ZERO_LIST}]|[${MONTH_31DAYS_LIST}][7T][${OMOCODE_ZERO_LIST}1M]|[${MONTH_30DAYS_LIST}${MONTH_31DAYS_LIST}][6S][9V])|(?:${LEAP_YEAR_MATCHER})B[6S][9V]`;\r\n\r\nexport const CITY_CODE_MATCHER: string = `[${CITY_CODE_LIST}](?:[${OMOCODE_NON_ZERO_NUMBER_LIST}][${OMOCODE_NUMBER_LIST}]{2}|[${OMOCODE_ZERO_LIST}](?:[${OMOCODE_NON_ZERO_NUMBER_LIST}][${OMOCODE_NUMBER_LIST}]|[${OMOCODE_ZERO_LIST}][${OMOCODE_NON_ZERO_NUMBER_LIST}]))`;\r\nexport const COUNTRY_CODE_MATCHER: string = `${COUNTRY_CODE_LIST}[${OMOCODE_NON_ZERO_NUMBER_LIST}][${OMOCODE_NUMBER_LIST}]{2}`;\r\nexport const BELFIORE_CODE_MATCHER: string = `(?:[${CITY_CODE_LIST}${COUNTRY_CODE_LIST}][${OMOCODE_NON_ZERO_NUMBER_LIST}][${OMOCODE_NUMBER_LIST}]{2})|(?:[${CITY_CODE_LIST}][${OMOCODE_ZERO_LIST}](?:[${OMOCODE_NON_ZERO_NUMBER_LIST}][${OMOCODE_NUMBER_LIST}]|[${OMOCODE_ZERO_LIST}][${OMOCODE_NON_ZERO_NUMBER_LIST}]))`;\r\n\r\nexport const CHECK_DIGIT: string = \"[A-Z]\";\r\n\r\nexport const CODICE_FISCALE: string = `${CF_FULL_NAME_MATCHER}(?:${FULL_DATE_MATCHER})(?:${BELFIORE_CODE_MATCHER})${CHECK_DIGIT}`;\r\n\r\nexport const PARTIAL_CF_NAME_MATCHER: string = `[A-Z][${VOWEL_LIST}]?|[${CONSONANT_LIST}]{1,2}`;\r\nexport const PARTIAL_CF_FULL_NAME: string = `(?:${PARTIAL_CF_NAME_MATCHER})|(?:(?:${CF_NAME_MATCHER})(?:${PARTIAL_CF_NAME_MATCHER})?)`;\r\nexport const PARTIAL_YEAR: string = `[${OMOCODE_NUMBER_LIST}]`;\r\nexport const PARTIAL_MONTH_DAY: string = `${MONTH_MATCHER}[${OMOCODE_ZERO_LIST}12456MNQRS]?|[${MONTH_30DAYS_LIST}${MONTH_31DAYS_LIST}]${DAY_3X_MATCHER}`;\r\nexport const PARTIAL_FULL_DATE: string = `${PARTIAL_YEAR}|(?:${YEAR_MATCHER}(?:${PARTIAL_MONTH_DAY})?)`;\r\nexport const PARTIAL_BELFIORE_CODE_MATCHER: string = `[${CITY_CODE_LIST}${COUNTRY_CODE_LIST}](?:[${OMOCODE_NON_ZERO_NUMBER_LIST}][${OMOCODE_NUMBER_LIST}]?)?|[${COUNTRY_CODE_LIST}](?:[${OMOCODE_ZERO_LIST}][${OMOCODE_NUMBER_LIST}]?)?`;\r\n\r\nexport const PARTIAL_CF: string = `${PARTIAL_CF_FULL_NAME}|(?:${CF_FULL_NAME_MATCHER}(?:(?:${PARTIAL_FULL_DATE})|(?:${FULL_DATE_MATCHER})(?:(?:${PARTIAL_BELFIORE_CODE_MATCHER})|(?:${BELFIORE_CODE_MATCHER})${CHECK_DIGIT}?)?)?)?`;\r\n","enum CRC {\r\n \"B\",\r\n \"A\",\r\n \"K\",\r\n \"P\",\r\n \"L\",\r\n \"C\",\r\n \"Q\",\r\n \"D\",\r\n \"R\",\r\n \"E\",\r\n \"V\",\r\n \"O\",\r\n \"S\",\r\n \"F\",\r\n \"T\",\r\n \"G\",\r\n \"U\",\r\n \"H\",\r\n \"M\",\r\n \"I\",\r\n \"N\",\r\n \"J\",\r\n \"W\",\r\n \"Z\",\r\n \"Y\",\r\n \"X\",\r\n}\r\n\r\nexport default CRC;\r\n","enum BirthMonth {\r\n\t\"A\",\r\n\t\"B\",\r\n\t\"C\",\r\n\t\"D\",\r\n\t\"E\",\r\n\t\"H\",\r\n\t\"L\",\r\n\t\"M\",\r\n\t\"P\",\r\n\t\"R\",\r\n\t\"S\",\r\n\t\"T\",\r\n}\r\n\r\nexport default BirthMonth;\r\n","import { CRC_OFFSET, LASTNAME_OFFSET } from \"../const/cf-offsets.const\";\r\nimport { PARTIAL_CF } from \"../const/matcher.const\";\r\nimport CRC from \"../enums/crc.enum\";\r\nimport generatorWrapper from \"../functions/generator-wrapper.function\";\r\nimport type IGeneratorWrapper from \"../interfaces/generator-wrapper.interface\";\r\nimport type CodiceFiscaleCRC from \"../types/codice-fiscale-crc.type\";\r\nclass CheckDigitizer {\r\n\t/**\r\n\t * Evaluate given partial CF to produce last check digit character\r\n\t * @param codiceFiscale Partial or complete Fiscal Code to evaluate to produce last character\r\n\t * @returns 16th CF char\r\n\t */\r\n\tpublic static checkDigit(codiceFiscale: string): CodiceFiscaleCRC | null {\r\n\t\tif (\r\n\t\t\ttypeof codiceFiscale === \"string\" &&\r\n\t\t\tnew RegExp(PARTIAL_CF).test(codiceFiscale)\r\n\t\t) {\r\n\t\t\tconst partialCF = codiceFiscale.substr(LASTNAME_OFFSET, CRC_OFFSET);\r\n\t\t\tlet partialCfValue = 0;\r\n\t\t\tfor (const charValue of this.evaluateChar(partialCF)) {\r\n\t\t\t\tpartialCfValue += charValue as number;\r\n\t\t\t}\r\n\t\t\treturn String.fromCharCode(\r\n\t\t\t\t(partialCfValue % this.CRC_MOD) + this.CHAR_OFFSET\r\n\t\t\t) as CodiceFiscaleCRC;\r\n\t\t}\r\n\t\treturn null;\r\n\t}\r\n\r\n\tpublic static evaluateChar(\r\n\t\tpartialCF: string = \"\"\r\n\t): IGeneratorWrapper<number, 0, void> {\r\n\t\treturn generatorWrapper(this.evaluateCharGenerator(partialCF));\r\n\t}\r\n\r\n\tprivate static CHAR_OFFSET: number = 65;\r\n\tprivate static CRC_MOD: number = 26;\r\n\r\n\t/**\r\n\t * Partial FiscalCode Evaluator\r\n\t * @param Partial Fiscal Code to evaluate\r\n\t * @yields character value odd/even\r\n\t */\r\n\tprivate static *evaluateCharGenerator(partialCF: string = \"\"): Generator {\r\n\t\tif (typeof partialCF === \"string\" && partialCF.length) {\r\n\t\t\tfor (let index = 0; index < partialCF.length; index++) {\r\n\t\t\t\tlet char: string = partialCF[index].toUpperCase();\r\n\t\t\t\tconst isNumber: boolean = /^\\d$/u.test(char);\r\n\t\t\t\tif (isNumber) {\r\n\t\t\t\t\t// Numbers have always (odd/even) the same values of corresponding letters (0-9 => A-J)\r\n\t\t\t\t\tchar = String.fromCharCode(parseInt(char, 10) + this.CHAR_OFFSET);\r\n\t\t\t\t}\r\n\t\t\t\t// Odd/Even are shifted/swapped\r\n\t\t\t\t// array starts from 0, \"Agenzia delle Entrate\" documentation counts the string from 1\r\n\t\t\t\tconst isOdd: boolean = !(index % 2); // Odd according to documentation\r\n\t\t\t\tif (isOdd) {\r\n\t\t\t\t\t// Odd positions\r\n\t\t\t\t\tyield parseInt(CRC[char as any], 10);\r\n\t\t\t\t} else {\r\n\t\t\t\t\t// Even positions\r\n\t\t\t\t\tyield char.charCodeAt(0) - this.CHAR_OFFSET;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t\treturn 0;\r\n\t}\r\n}\r\n\r\nexport default CheckDigitizer;\r\n","import type IGeneratorWrapper from \"../interfaces/generator-wrapper.interface\";\r\nexport default function generatorWrapper<\r\n\tT = unknown,\r\n\tTReturn = any,\r\n\tTNext = unknown\r\n>(generator: Generator): IGeneratorWrapper<T, TReturn, TNext> {\r\n\tgenerator[Symbol.iterator] = () => generator;\r\n\treturn generator as IGeneratorWrapper<T, TReturn, TNext>;\r\n}\r\n","enum GenderWeight {\r\n\t\"M\" = 0,\r\n\t\"F\" = 40,\r\n}\r\n\r\nexport default GenderWeight;\r\n","enum Omocodes {\r\n\t\"L\",\r\n\t\"M\",\r\n\t\"N\",\r\n\t\"P\",\r\n\t\"Q\",\r\n\t\"R\",\r\n\t\"S\",\r\n\t\"T\",\r\n\t\"U\",\r\n\t\"V\",\r\n}\r\n\r\nexport default Omocodes;\r\n","import { DateDay } from \"../date-utils/\";\r\nimport GenderWeight from \"../enums/gender-weight.enum\";\r\nimport type Genders from \"../types/genders.type\";\r\n\r\nclass Gender {\r\n\tpublic static getDay(genderDay: number): DateDay | null {\r\n\t\tconst plainDay = genderDay % GenderWeight.F;\r\n\t\treturn plainDay > 0 && plainDay <= this.MAX_MONTH_DAY\r\n\t\t\t? (plainDay as DateDay)\r\n\t\t\t: null;\r\n\t}\r\n\r\n\tpublic static getGender(genderDay: number): Genders | null {\r\n\t\treturn (\r\n\t\t\tthis.toArray().find(\r\n\t\t\t\t(gender) =>\r\n\t\t\t\t\tgenderDay >= GenderWeight[gender] &&\r\n\t\t\t\t\tgenderDay <= GenderWeight[gender] + this.MAX_MONTH_DAY\r\n\t\t\t) || null\r\n\t\t);\r\n\t}\r\n\r\n\tpublic static genderizeDay(day: number, gender: Genders): number {\r\n\t\treturn day + GenderWeight[gender];\r\n\t}\r\n\r\n\tpublic static toArray(): Genders[] {\r\n\t\treturn [\"M\", \"F\"];\r\n\t}\r\n\r\n\tprivate static MAX_MONTH_DAY: number = 31;\r\n}\r\n\r\nexport default Gender;\r\n","export const CF_INTRODUCTION_DATE = new Date(\"1973-09-29\");\r\n","import dayjs from \"dayjs\";\r\nimport {\r\n\tIBelfioreConnector,\r\n\tBelfiorePlace,\r\n} from \"@marketto/belfiore-connector\";\r\nimport DiacriticRemover from \"@marketto/diacritic-remover\";\r\nimport {\r\n\tCRC_OFFSET,\r\n\tCRC_SIZE,\r\n\tDAY_OFFSET,\r\n\tDAY_SIZE,\r\n\tFIRSTNAME_OFFSET,\r\n\tFIRSTNAME_SIZE,\r\n\tGENDER_OFFSET,\r\n\tGENDER_SIZE,\r\n\tLASTNAME_OFFSET,\r\n\tLASTNAME_SIZE,\r\n\tMONTH_OFFSET,\r\n\tMONTH_SIZE,\r\n\tPLACE_OFFSET,\r\n\tPLACE_SIZE,\r\n\tYEAR_OFFSET,\r\n\tYEAR_SIZE,\r\n} from \"../const/cf-offsets.const\";\r\nimport { CF_NAME_MATCHER, CF_SURNAME_MATCHER } from \"../const/matcher.const\";\r\nimport { CONSONANT_LIST, VOWEL_LIST } from \"../const/matcher.const\";\r\nimport { DateDay, DateMonth, DateUtils, MultiFormatDate } from \"../date-utils\";\r\nimport BirthMonth from \"../enums/birth-month.enum\";\r\nimport GenderWeight from \"../enums/gender-weight.enum\";\r\nimport Omocodes from \"../enums/omocodes.enum\";\r\nimport type IPersonalInfo from \"../interfaces/personal-info.interface\";\r\nimport type Genders from \"../types/genders.type\";\r\nimport CheckDigitizer from \"./check-digitizer.class\";\r\nimport Gender from \"./gender.class\";\r\nimport { CF_INTRODUCTION_DATE } from \"../const/logic.const\";\r\n\r\nconst diacriticRemover = new DiacriticRemover();\r\n\r\nexport default class Parser {\r\n\tconstructor(private readonly belfioreConnector: IBelfioreConnector) {}\r\n\r\n\t/**\r\n\t * Default omocode bitmap\r\n\t */\r\n\tpublic OMOCODE_BITMAP: number = 0b0111011011000000;\r\n\r\n\t/**\r\n\t * Convert omocode CF into plain one\r\n\t * @param codiceFiscale Partial or complete Omocode/Regular CF to parse, starting from LastName\r\n\t * @returns Regular CF w/o omocodes chars\r\n\t */\r\n\tpublic cfDeomocode(codiceFiscale: string): string {\r\n\t\tif (codiceFiscale && codiceFiscale.length <= YEAR_OFFSET) {\r\n\t\t\treturn codiceFiscale;\r\n\t\t}\r\n\t\tconst deomocodedCf = this.partialCfDeomocode(codiceFiscale);\r\n\t\tif (deomocodedCf.length < CRC_OFFSET) {\r\n\t\t\treturn deomocodedCf;\r\n\t\t}\r\n\t\tconst partialDeomocodedCf = deomocodedCf.substring(\r\n\t\t\tLASTNAME_OFFSET,\r\n\t\t\tCRC_OFFSET\r\n\t\t);\r\n\t\treturn (\r\n\t\t\tpartialDeomocodedCf +\r\n\t\t\tthis.appyCaseToChar(\r\n\t\t\t\tCheckDigitizer.checkDigit(deomocodedCf) || \"\",\r\n\t\t\t\tdeomocodedCf.substring(CRC_OFFSET, CRC_OFFSET + CRC_SIZE)\r\n\t\t\t)\r\n\t\t);\r\n\t}\r\n\r\n\tpublic cfOmocode(codiceFiscale: string, omocodeId: number): string {\r\n\t\tif (!omocodeId) {\r\n\t\t\treturn this.cfDeomocode(codiceFiscale);\r\n\t\t}\r\n\t\tconst omocodedCf = codiceFiscale.split(\"\");\r\n\t\t// tslint:disable-next-line: prefer-for-of\r\n\t\tfor (let i = codiceFiscale.length - 1, o = 0; i >= 0; i--) {\r\n\t\t\t// tslint:disable-next-line: no-bitwise\r\n\t\t\tif ((2 ** i) & this.OMOCODE_BITMAP) {\r\n\t\t\t\t// tslint:disable-next-line: no-bitwise\r\n\t\t\t\tconst charToEncode: boolean = !!(omocodeId & (2 ** o));\r\n\t\t\t\tconst isOmocode: boolean = isNaN(parseInt(omocodedCf[i], 10));\r\n\t\t\t\tif (charToEncode !== isOmocode) {\r\n\t\t\t\t\tconst char: any = omocodedCf[i].toUpperCase();\r\n\t\t\t\t\tomocodedCf[i] = Omocodes[char];\r\n\t\t\t\t}\r\n\t\t\t\to++;\r\n\t\t\t}\r\n\t\t}\r\n\t\tconst crc = omocodedCf[CRC_OFFSET];\r\n\t\tif (crc) {\r\n\t\t\tconst partialCf = omocodedCf.slice(LASTNAME_OFFSET, CRC_OFFSET).join(\"\");\r\n\t\t\tomocodedCf[CRC_OFFSET] = this.appyCaseToChar(\r\n\t\t\t\tCheckDigitizer.checkDigit(partialCf) || \"\",\r\n\t\t\t\tcrc\r\n\t\t\t);\r\n\t\t}\r\n\t\treturn omocodedCf.join(\"\");\r\n\t}\r\n\r\n\tpublic cfOmocodeId(codiceFiscale: string): number {\r\n\t\tconst cfOmocodeBitmap = codiceFiscale\r\n\t\t\t.split(\"\")\r\n\t\t\t// tslint:disable-next-line: no-bitwise\r\n\t\t\t.filter((char, index) => (2 ** index) & this.OMOCODE_BITMAP)\r\n\t\t\t.map((char) => (/^[a-z]$/i.test(diacriticRemover[char]) ? 1 : 0))\r\n\t\t\t.join(\"\");\r\n\t\treturn parseInt(cfOmocodeBitmap, 2);\r\n\t}\r\n\r\n\t/**\r\n\t * Parse lastName information\r\n\t * @param codiceFiscale Partial or complete CF to parse\r\n\t * @returns Partial/possible lastName\r\n\t */\r\n\tpublic cfToLastName(codiceFiscale: string): string | null {\r\n\t\tconst cfLastNamePart = codiceFiscale?.substring(\r\n\t\t\tLASTNAME_OFFSET,\r\n\t\t\tLASTNAME_OFFSET + LASTNAME_SIZE\r\n\t\t);\r\n\t\tif (\r\n\t\t\ttypeof codiceFiscale !== \"string\" ||\r\n\t\t\tcfLastNamePart.length !== LASTNAME_SIZE ||\r\n\t\t\t!new RegExp(`^(?:${CF_SURNAME_MATCHER})`, \"iu\").test(cfLastNamePart)\r\n\t\t) {\r\n\t\t\treturn null;\r\n\t\t}\r\n\r\n\t\tconst lastNameCf = codiceFiscale.substring(\r\n\t\t\tLASTNAME_OFFSET,\r\n\t\t\tLASTNAME_OFFSET + LASTNAME_SIZE\r\n\t\t);\r\n\r\n\t\tconst [cons = \"\"] =\r\n\t\t\tlastNameCf.match(new RegExp(`^[${CONSONANT_LIST}]{1,3}`, \"ig\")) || [];\r\n\t\tconst [vow = \"\"] =\r\n\t\t\tlastNameCf.match(new RegExp(`[${VOWEL_LIST}]{1,3}`, \"ig\")) || [];\r\n\r\n\t\tconst matchingLength = cons.length + vow.length;\r\n\r\n\t\tif (\r\n\t\t\tmatchingLength < 2 ||\r\n\t\t\t(matchingLength < 3 && lastNameCf[2].toUpperCase() !== \"X\")\r\n\t\t) {\r\n\t\t\treturn null;\r\n\t\t}\r\n\r\n\t\tswitch (cons.length) {\r\n\t\t\tcase 3:\r\n\t\t\t\treturn (cons + vow).split(\"\").join(this.JOLLY_CHAR) + this.JOLLY_CHAR;\r\n\t\t\tcase 2:\r\n\t\t\t\treturn `${cons[0]}${vow[0]}*${cons[1]}${this.JOLLY_CHAR}`;\r\n\t\t\tcase 1:\r\n\t\t\t\treturn `${cons[0]}${vow}${this.JOLLY_CHAR}`;\r\n\t\t\tdefault:\r\n\t\t\t\treturn `${vow}${vow.length === 3 ? this.JOLLY_CHAR : \"\"}`;\r\n\t\t}\r\n\t}\r\n\r\n\t/**\r\n\t * Parse firstName information\r\n\t * @param codiceFiscale Partial or complete CF to parse\r\n\t * @returns Partial/possible firstName\r\n\t */\r\n\tpublic cfToFirstName(codiceFiscale: string): string | null {\r\n\t\tconst cfFirstNamePart = codiceFiscale?.substring(\r\n\t\t\tFIRSTNAME_OFFSET,\r\n\t\t\tFIRSTNAME_OFFSET + FIRSTNAME_SIZE\r\n\t\t);\r\n\t\tif (\r\n\t\t\ttypeof codiceFiscale !== \"string\" ||\r\n\t\t\tcfFirstNamePart?.length !== FIRSTNAME_SIZE ||\r\n\t\t\t!new RegExp(`^(${CF_NAME_MATCHER})$`, \"iu\").test(cfFirstNamePart)\r\n\t\t) {\r\n\t\t\treturn null;\r\n\t\t}\r\n\t\treturn this.cfToLastName(cfFirstNamePart);\r\n\t}\r\n\r\n\t/**\r\n\t * Parse gender information\r\n\t * @param codiceFiscale Partial or complete CF to parse\r\n\t * @returns Male or female\r\n\t */\r\n\tpublic cfToGender(codiceFiscale: string): Genders | null {\r\n\t\tif (\r\n\t\t\ttypeof codiceFiscale !== \"string\" ||\r\n\t\t\tcodiceFiscale.length < GENDER_OFFSET + GENDER_SIZE\r\n\t\t) {\r\n\t\t\treturn null;\r\n\t\t}\r\n\t\tconst cfGenderPart = codiceFiscale.substring(\r\n\t\t\tGENDER_OFFSET,\r\n\t\t\tGENDER_OFFSET + GENDER_SIZE\r\n\t\t);\r\n\t\tconst genderInt =\r\n\t\t\tparseInt(this.partialCfDeomocode(cfGenderPart, GENDER_OFFSET), 10) * 10;\r\n\t\treturn Gender.getGender(genderInt);\r\n\t}\r\n\r\n\t/**\r\n\t * Parse birth year information\r\n\t * @param codiceFiscale Partial or complete CF to parse\r\n\t * @returns Birth Year (4 digits)\r\n\t */\r\n\tpublic cfToBirthYear(codiceFiscale: string): number | null {\r\n\t\tif (\r\n\t\t\ttypeof codiceFiscale !== \"string\" ||\r\n\t\t\tcodiceFiscale.length < YEAR_OFFSET + YEAR_SIZE\r\n\t\t) {\r\n\t\t\treturn null;\r\n\t\t}\r\n\t\tconst cfBirthYearPart = codiceFiscale.substring(\r\n\t\t\tYEAR_OFFSET,\r\n\t\t\tYEAR_OFFSET + YEAR_SIZE\r\n\t\t);\r\n\t\tconst birthYear: number = parseInt(\r\n\t\t\tthis.partialCfDeomocode(cfBirthYearPart, YEAR_OFFSET),\r\n\t\t\t10\r\n\t\t);\r\n\r\n\t\tif (isNaN(birthYear)) {\r\n\t\t\treturn null;\r\n\t\t}\r\n\r\n\t\tconst current2DigitsYear: number = parseInt(dayjs().format(\"YY\"), 10);\r\n\r\n\t\tconst century: number = (birthYear > current2DigitsYear ? 1 : 0) * 100;\r\n\t\treturn dayjs()\r\n\t\t\t.subtract(current2DigitsYear - birthYear + century, \"years\")\r\n\t\t\t.year();\r\n\t}\r\n\r\n\t/**\r\n\t * Parse birth month information\r\n\t * @param codiceFiscale Partial or complete CF to parse\r\n\t * @returns Birth Month (0...11 - Date notation)\r\n\t */\r\n\tpublic cfToBirthMonth(codiceFiscale: string): DateMonth | null {\r\n\t\tif (\r\n\t\t\ttypeof codiceFiscale !== \"string\" ||\r\n\t\t\tcodiceFiscale.length < MONTH_OFFSET + MONTH_SIZE\r\n\t\t) {\r\n\t\t\treturn null;\r\n\t\t}\r\n\r\n\t\tconst cfBirthMonthPart: any = codiceFiscale\r\n\t\t\t.substring(MONTH_OFFSET, MONTH_OFFSET + MONTH_SIZE)\r\n\t\t\t.toUpperCase();\r\n\t\tconst birthMonth = BirthMonth[cfBirthMonthPart];\r\n\t\tif (typeof birthMonth !== \"number\" || birthMonth < 0 || birthMonth > 11) {\r\n\t\t\treturn null;\r\n\t\t}\r\n\t\treturn birthMonth as DateMonth;\r\n\t}\r\n\r\n\t/**\r\n\t * Parse birth day information\r\n\t * @param codiceFiscale Partial or complete CF to parse\r\n\t * @returns Birth day (1..31)\r\n\t */\r\n\tpublic cfToBirthDay(codiceFiscale: string): DateDay | null {\r\n\t\tif (\r\n\t\t\ttypeof codiceFiscale !== \"string\" ||\r\n\t\t\tcodiceFiscale.length < DAY_OFFSET + DAY_SIZE\r\n\t\t) {\r\n\t\t\treturn null;\r\n\t\t}\r\n\r\n\t\tconst cfBirthDayPart = codiceFiscale.substring(\r\n\t\t\tDAY_OFFSET,\r\n\t\t\tDAY_OFFSET + DAY_SIZE\r\n\t\t);\r\n\t\tconst birthDay: number = parseInt(\r\n\t\t\tthis.partialCfDeomocode(cfBirthDayPart, DAY_OFFSET),\r\n\t\t\t10\r\n\t\t);\r\n\r\n\t\tif (isNaN(birthDay)) {\r\n\t\t\treturn null;\r\n\t\t}\r\n\t\treturn Gender.getDay(birthDay);\r\n\t}\r\n\r\n\t/**\r\n\t * Parse birth date information\r\n\t * @param codiceFiscale Partial or complete CF to parse\r\n\t * @returns Birth Date\r\n\t */\r\n\tpublic cfToBirthDate(codiceFiscale: string): Date | null {\r\n\t\tconst birthDay = this.cfToBirthDay(codiceFiscale);\r\n\t\tif (!birthDay) {\r\n\t\t\treturn null;\r\n\t\t}\r\n\r\n\t\tconst birthMonth = this.cfToBirthMonth(codiceFiscale);\r\n\t\tif (typeof birthMonth !== \"number\") {\r\n\t\t\treturn null;\r\n\t\t}\r\n\r\n\t\tconst birthYear = this.cfToBirthYear(codiceFiscale);\r\n\r\n\t\treturn DateUtils.ymdToDate(birthYear, birthMonth, birthDay);\r\n\t}\r\n\r\n\t/**\r\n\t * Parse birth place information\r\n\t * @param codiceFiscale Partial or complete CF to parse\r\n\t * @param checkBirthDateConsistency Ensure birthday is between creation and expiran date of the cf city or country, default value: true\r\n\t * @returns Birth place\r\n\t */\r\n\tpublic async cfToBirthPlace(\r\n\t\tcodiceFiscale: string,\r\n\t\tcheckBirthDateConsistency: boolean = true\r\n\t): Promise<BelfiorePlace | null> {\r\n\t\tif (\r\n\t\t\ttypeof codiceFiscale !== \"string\" ||\r\n\t\t\tcodiceFiscale.length < PLACE_OFFSET + PLACE_SIZE\r\n\t\t) {\r\n\t\t\treturn null;\r\n\t\t}\r\n\r\n\t\tconst cfBirthPlacePart = codiceFiscale.substring(\r\n\t\t\tPLACE_OFFSET,\r\n\t\t\tPLACE_OFFSET + PLACE_SIZE\r\n\t\t);\r\n\t\tconst belfioreCode: string = this.partialCfDeomocode(\r\n\t\t\tcfBirthPlacePart,\r\n\t\t\tPLACE_OFFSET\r\n\t\t);\r\n\r\n\t\tconst birthPlace: BelfiorePlace | undefined | null =\r\n\t\t\tawait this.belfioreConnector.findByCode(belfioreCode);\r\n\t\tif (!birthPlace) {\r\n\t\t\treturn null;\r\n\t\t}\r\n\r\n\t\tconst { creationDate, expirationDate } = birthPlace;\r\n\t\tif ((creationDate || expirationDate) && checkBirthDateConsistency) {\r\n\t\t\tconst birthDate = this.cfToBirthDate(codiceFiscale);\r\n\t\t\tconst isBirthDateAfterCfIntroduction = dayjs(CF_INTRODUCTION_DATE)\r\n\t\t\t\t// Adding some tolerance\r\n\t\t\t\t.add(5, \"years\")\r\n\t\t\t\t.isBefore(birthDate, \"day\");\r\n\r\n\t\t\t// Skipping birthDate vs Creation/Expiration check for people born up to 5y after cf introduction\r\n\t\t\tif (birthDate && isBirthDateAfterCfIntroduction) {\r\n\t\t\t\tconst datePlaceConsistency =\r\n\t\t\t\t\t// BirthDay is before expiration date\r\n\t\t\t\t\t(!expirationDate ||\r\n\t\t\t\t\t\tdayjs(birthDate).isBefore(expirationDate, \"day\")) &&\r\n\t\t\t\t\t// BirthDay is after creation date\r\n\t\t\t\t\t(!creationDate || dayjs(birthDate).isAfter(creationDate, \"day\"));\r\n\t\t\t\tif (!datePlaceConsistency) {\r\n\t\t\t\t\treturn null;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t\treturn birthPlace;\r\n\t}\r\n\r\n\t/**\r\n\t * @param fiscalCode 16 character Codice Fiscale to decode\r\n\t * @returns Decoded CF Info\r\n\t */\r\n\tpublic async cfDecode(fiscalCode: string): Promise<IPersonalInfo> {\r\n\t\tconst year = this.cfToBirthYear(fiscalCode) || undefined;\r\n\t\t// 0 is a month\r\n\t\tconst month = this.cfToBirthMonth(fiscalCode) ?? undefined;\r\n\t\tconst day = this.cfToBirthDay(fiscalCode) || undefined;\r\n\t\tconst date = DateUtils.ymdToDate(year, month, day) || undefined;\r\n\t\tconst place = await this.cfToBirthPlace(fiscalCode);\r\n\t\tconst personalInfo: IPersonalInfo = {\r\n\t\t\tfirstName: this.cfToFirstName(fiscalCode) || undefined,\r\n\t\t\tlastName: this.cfToLastName(fiscalCode) || undefined,\r\n\r\n\t\t\tday,\r\n\t\t\tmonth,\r\n\t\t\tyear,\r\n\r\n\t\t\tdate,\r\n\r\n\t\t\tgender: this.cfToGender(fiscalCode) || undefined,\r\n\t\t\tplace: place || undefined,\r\n\r\n\t\t\tomocodeId: this.cfOmocodeId(fiscalCode),\r\n\t\t};\r\n\r\n\t\tif (year && month && day) {\r\n\t\t\tpersonalInfo.date = new Date(Date.UTC(year, month, day));\r\n\t\t}\r\n\r\n\t\treturn personalInfo;\r\n\t}\r\n\r\n\t/**\r\n\t * Parse lastName to cf part\r\n\t * @param lastName Partial or complete CF to parse\r\n\t * @returns partial cf\r\n\t */\r\n\tpublic lastNameToCf(lastName?: string | null): string | null {\r\n\t\tif (!lastName || (lastName || \"\").trim().length < 2) {\r\n\t\t\treturn null;\r\n\t\t}\r\n\r\n\t\tif (!/^[A-Z ']{1,32}$/iu.test(diacriticRemover.replace(lastName))) {\r\n\t\t\treturn null;\r\n\t\t}\r\n\r\n\t\tconst consonants = this.charExtractor(lastName, CONSONANT_LIST);\r\n\t\tconst vowels = this.charExtractor(lastName, VOWEL_LIST);\r\n\r\n\t\tconst partialCf = (consonants + vowels).padEnd(3, \"X\").substring(0, 3);\r\n\r\n\t\tif (partialCf.length < 3) {\r\n\t\t\treturn null;\r\n\t\t}\r\n\t\treturn partialCf.toUpperCase();\r\n\t}\r\n\r\n\t/**\r\n\t * Parse firstName to cf part\r\n\t * @param firstName Partial or complete CF to parse\r\n\t * @returns partial cf\r\n\t */\r\n\tpublic firstNameToCf(firstName?: string | null): string | null {\r\n\t\tif (!firstName || (firstName || \"\").trim().length < 2) {\r\n\t\t\treturn null;\r\n\t\t}\r\n\t\tconst consonants = this.charExtractor(firstName, CONSONANT_LIST);\r\n\t\tif (consonants.length >= 4) {\r\n\t\t\treturn (consonants[0] + consonants.substring(2, 4)).toUpperCase();\r\n\t\t}\r\n\t\treturn this.lastNameToCf(firstName);\r\n\t}\r\n\r\n\t/**\r\n\t * Parse year to cf part\r\n\t * @param year Birth year 2 or 4 digit string, number above 19XX or below 100\r\n\t * @returns partial cf\r\n\t */\r\n\tpublic yearToCf(year: string | number): string | null {\r\n\t\tlet parsedYear: number;\r\n\t\tif (typeof year === \"string\") {\r\n\t\t\tparsedYear = parseInt(year, 10);\r\n\t\t} else {\r\n\t\t\tparsedYear = year;\r\n\t\t}\r\n\r\n\t\tif (\r\n\t\t\t!(\r\n\t\t\t\ttypeof parsedYear === \"number\" &&\r\n\t\t\t\t!isNaN(parsedYear) &&\r\n\t\t\t\t(parsedYear >= 1900 || parsedYear < 100)\r\n\t\t\t)\r\n\t\t) {\r\n\t\t\treturn null;\r\n\t\t}\r\n\t\treturn `0${parsedYear}`.substr(-2);\r\n\t}\r\n\r\n\t/**\r\n\t * Parse month information\r\n\t * @param month Month number 0..11\r\n\t * @returns Birth Month CF code\r\n\t */\r\n\tpublic monthToCf(month: DateMonth | number): string | null {\r\n\t\tif (month < 0 || month > 11) {\r\n\t\t\treturn null;\r\n\t\t}\r\n\r\n\t\treturn BirthMonth[month] || null;\r\n\t}\r\n\r\n\t/**\r\n\t * Parse day information\r\n\t * @param day Day number 1..31\r\n\t * @param gender Gender enum value\r\n\t * @returns Birth Day CF code\r\n\t */\r\n\tpublic dayGenderToCf(day: DateDay | number, gender: Genders): string | null {\r\n\t\tif (day < 1 || day > 31) {\r\n\t\t\treturn null;\r\n\t\t}\r\n\r\n\t\tconst genderValue = GenderWeight[gender as any];\r\n\t\tif (typeof genderValue !== \"number\") {\r\n\t\t\treturn null;\r\n\t\t}\r\n\t\treturn `0${day + genderValue}`.substr(-2);\r\n\t}\r\n\r\n\t/**\r\n\t * Parse Year, Month, Day to Dated\r\n\t * @param year 4 digits Year\r\n\t * @param month 1 or 2 digits Month 0..11\r\n\t * @param day 1,2 digits Day 1..31\r\n\t * @returns Date or null if provided year/month/day are not valid\r\n\t */\r\n\tpublic yearMonthDayToDate(\r\n\t\tyear: number | null | undefined,\r\n\t\tmonth: DateMonth | null | undefined = 0,\r\n\t\tday: DateDay | null | undefined = 1\r\n\t): Date | null {\r\n\t\tif (\r\n\t\t\t!year ||\r\n\t\t\tyear < 1861 ||\r\n\t\t\t[month, day].some((param) => typeof param !== \"number\")\r\n\t\t) {\r\n\t\t\treturn null;\r\n\t\t}\r\n\t\tconst date = dayjs(Date.UTC(year, month || 0, day || 1));\r\n\t\tif (\r\n\t\t\t!date.isValid() ||\r\n\t\t\tdate.year() !== year ||\r\n\t\t\tdate.month() !== month ||\r\n\t\t\tdate.date() !== day\r\n\t\t) {\r\n\t\t\treturn null;\r\n\t\t}\r\n\t\treturn date.toDate();\r\n\t}\r\n\r\n\t/**\r\n\t * Parse Place information to return city or country details\r\n\t * @param place Belfiore place instance, belfiore code or city/country name\r\n\t * @returns BelfiorePlace instance with the target city or country details\r\n\t */\r\n\tpublic async parsePlace(\r\n\t\tplace: BelfiorePlace | string\r\n\t): Promise<BelfiorePlace | null> {\r\n\t\tlet verifiedBirthPlace: BelfiorePlace | null | undefined;\r\n\t\tif (!place) {\r\n\t\t\treturn null;\r\n\t\t} else if (typeof place === \"object\" && place.belfioreCode) {\r\n\t\t\tverifiedBirthPlace = await this.belfioreConnector.findByCode(\r\n\t\t\t\tplace.belfioreCode\r\n\t\t\t);\r\n\t\t} else if (typeof place === \"string\") {\r\n\t\t\tverifiedBirthPlace =\r\n\t\t\t\t(await this.belfioreConnector.findByCode(place)) ||\r\n\t\t\t\t(await this.belfioreConnector.findByName(place));\r\n\t\t}\r\n\t\treturn verifiedBirthPlace || null;\r\n\t}\r\n\r\n\t/**\r\n\t * Parse Date and Gender information to create Date/Gender CF part\r\n\t * @param date Date instance, ISO8601 date string or array of numbers [year, month, day]\r\n\t * @param gender Gender enum value\r\n\t * @returns Birth date and Gender CF code\r\n\t */\r\n\tpublic dateGenderToCf(date: MultiFormatDate, gender: Genders): string | null {\r\n\t\tconst parsedDate = DateUtils.parseDate(date);\r\n\t\tif (!parsedDate) {\r\n\t\t\treturn null;\r\n\t\t}\r\n\r\n\t\tconst cfYear = this.yearToCf(parsedDate.getFullYear());\r\n\t\tconst cfMonth = this.monthToCf(parsedDate.getMonth());\r\n\t\tconst cfDayGender = this.dayGenderToCf(parsedDate.getDate(), gender);\r\n\r\n\t\treturn `${cfYear}${cfMonth}${cfDayGender}`;\r\n\t}\r\n\r\n\t/**\r\n\t * Parse place name and province to Belfiore code\r\n\t * @param cityOrCountryName City or Country name\r\n\t * @param provinceId Province code for cities\r\n\t * @returns Matching place belfiore code, if only once is matching criteria\r\n\t */\r\n\t/**\r\n\t * Parse a Date and Gender information to create Date/Gender CF part\r\n\t * @param birthDate Date instance, ISO8601 date string or array of numbers [year, month, day]\r\n\t * @param cityOrCountryName City or Country name\r\n\t * @param provinceId Province code for cities\r\n\t * @returns Matching place belfiore code, if only once is matching criteria\r\n\t */\r\n\tpublic async placeToCf(\r\n\t\tcityOrCountryName: string,\r\n\t\tprovinceId?: string\r\n\t): Promise<string | null>;\r\n\tpublic async placeToCf(\r\n\t\tbirthDate: MultiFormatDate,\r\n\t\tcityOrCountryName: string,\r\n\t\tprovinceId?: string\r\n\t): Promise<string | null>;\r\n\tpublic async placeToCf(\r\n\t\tdateOrName: MultiFormatDate,\r\n\t\tnameOrProvince?: string,\r\n\t\tprovinceId?: string\r\n\t): Promise<string | null> {\r\n\t\tconst birthDate: Date | null = DateUtils.parseDate(dateOrName);\r\n\t\tlet name: string;\r\n\t\tlet province: string | undefined;\r\n\t\tif (!birthDate && typeof dateOrName === \"string\") {\r\n\t\t\tname = dateOrName;\r\n\t\t\tprovince = nameOrProvince;\r\n\t\t} else if (nameOrProvince) {\r\n\t\t\tname = nameOrProvince;\r\n\t\t\tprovince = provinceId;\r\n\t\t} else {\r\n\t\t\treturn null;\r\n\t\t}\r\n\r\n\t\tlet placeFinder: IBelfioreConnector | undefined = this.belfioreConnector;\r\n\t\tif (province) {\r\n\t\t\tplaceFinder = placeFinder.byProvince(province);\r\n\t\t}\r\n\t\tif (birthDate && placeFinder) {\r\n\t\t\tplaceFinder = placeFinder.from(birthDate);\r\n\t\t}\r\n\t\tif (placeFinder) {\r\n\t\t\tconst foundPlace: BelfiorePlace | null = await new Parser(\r\n\t\t\t\tplaceFinder\r\n\t\t\t).parsePlace(name);\r\n\t\t\tif (foundPlace) {\r\n\t\t\t\treturn foundPlace.belfioreCode;\r\n\t\t\t}\r\n\t\t}\r\n\t\treturn null;\r\n\t}\r\n\r\n\t/**\r\n\t * Generates full CF\r\n\t * @returns Complete CF\r\n\t */\r\n\tpublic async encodeCf({\r\n\t\tlastName,\r\n\t\tfirstName,\r\n\r\n\t\tyear,\r\n\t\tmonth,\r\n\t\tday,\r\n\t\tdate,\r\n\r\n\t\tgender,\r\n\t\tplace,\r\n\r\n\t\tomocodeId = 0,\r\n\t}: Omit<IPersonalInfo, \"place\"> & {\r\n\t\tplace?: BelfiorePlace | string | undefined;\r\n\t}): Promise<string | null> {\r\n\t\tconst dtParams =\r\n\t\t\tDateUtils.parseDate(date) || this.yearMonthDayToDate(year, month, day);\r\n\t\tif (!(dtParams && lastName && firstName && gender && place)) {\r\n\t\t\treturn null;\r\n\t\t}\r\n\t\tconst generator = [\r\n\t\t\tasync () => this.lastNameToCf(lastName),\r\n\t\t\tasync () => this.firstNameToCf(firstName),\r\n\t\t\tasync () => this.dateGenderToCf(dtParams, gender),\r\n\t\t\tasync () =>\r\n\t\t\t\tawait this.placeToCf(\r\n\t\t\t\t\tdtParams,\r\n\t\t\t\t\t(place as BelfiorePlace)?.belfioreCode || (place as string)\r\n\t\t\t\t),\r\n\t\t];\r\n\t\tlet cf = \"\";\r\n\t\tfor (const cfPartGenerator of generator) {\r\n\t\t\tconst cfValue = await cfPartGenerator();\r\n\t\t\tif (!cfValue) {\r\n\t\t\t\treturn null;\r\n\t\t\t}\r\n\t\t\tcf += cfValue;\r\n\t\t}\r\n\r\n\t\treturn this.cfOmocode(cf, omocodeId);\r\n\t}\r\n\r\n\tprivate JOLLY_CHAR: string = \"*\";\r\n\r\n\tprivate checkBitmap(offset: number): boolean {\r\n\t\t// tslint:disable-next-line: no-bitwise\r\n\t\treturn !!((2 ** offset) & this.OMOCODE_BITMAP);\r\n\t}\r\n\r\n\tprivate charOmocode(char: string, offset: number): string {\r\n\t\tif (/^[A-Z]$/giu.test(char) && this.checkBitmap(offset)) {\r\n\t\t\treturn Omocodes[char.toUpperCase() as any];\r\n\t\t}\r\n\r\n\t\treturn char;\r\n\t}\r\n\r\n\tprivate charExtractor(text: string, CHAR_LIST: string): string {\r\n\t\tconst charMatcher = new RegExp(`[${CHAR_LIST}]{1,24}`, \"ig\");\r\n\t\tconst diacriticFreeText = diacriticRemover.replace(text).trim();\r\n\t\tconst matchingChars = diacriticFreeText.match(charMatcher);\r\n\t\treturn (matchingChars || []).join(\"\");\r\n\t}\r\n\r\n\t/**\r\n\t * Convert omocode full or chunk CF into plain one\r\n\t * @param partialCodiceFiscale Partial or complete Omocode/Regular CF to parse\r\n\t * @param offset starting point of the given chunk in the 16 char CF\r\n\t * @returns Regular version w/o omocodes chars of the given chunk\r\n\t */\r\n\tprivate partialCfDeomocode(\r\n\t\tpartialCodiceFiscale: string,\r\n\t\toffset: number = 0\r\n\t): string {\r\n\t\tconst charReplacer = (char: string, position: number) =>\r\n\t\t\tthis.charOmocode(char, position + offset);\r\n\t\treturn partialCodiceFiscale.replace(/[\\dA-Z]/giu, charReplacer);\r\n\t}\r\n\r\n\tprivate appyCaseToChar(targetChar: string, counterCaseChar: string): string {\r\n\t\tif (targetChar && counterCaseChar) {\r\n\t\t\tconst isUpperCase =\r\n\t\t\t\tcounterCaseChar[0] === counterCaseChar[0].toUpperCase();\r\n\t\t\tconst isLowerCase =\r\n\t\t\t\tcounterCaseChar[0] === counterCaseChar[0].toLowerCase();\r\n\r\n\t\t\tif (isUpperCase && !isLowerCase) {\r\n\t\t\t\treturn targetChar[0].toUpperCase();\r\n\t\t\t} else if (!isUpperCase && isLowerCase) {\r\n\t\t\t\treturn targetChar[0].toLowerCase();\r\n\t\t\t}\r\n\t\t}\r\n\t\treturn targetChar[0];\r\n\t}\r\n}\r\n","const INVALID_SURNAME: string =\r\n\t\"Provided lastName is not valid, only letters, diacritics and apostrophe allowed\";\r\nconst INVALID_NAME: string =\r\n\t\"Provided name is not valid, only letters, diacritics and apostrophe allowed\";\r\nconst INVALID_DAY: string = \"Provided day is not valid\";\r\nconst INVALID_GENDER: string = \"Provided gender is not valid\";\r\nconst INVALID_DAY_OR_GENDER: string =\r\n\t\"Provided day and/or gender are not valid\";\r\nconst INVALID_YEAR: string =\r\n\t\"Provided year is not valid, only 2 or 4 digit numbers are allowed\";\r\nconst INVALID_DATE: string = \"Provided date is not valid\";\r\nconst INVALID_PLACE_NAME: string = \"Proviced City/Country name is not valid\";\r\n\r\nexport {\r\n\tINVALID_DATE,\r\n\tINVALID_DAY,\r\n\tINVALID_DAY_OR_GENDER,\r\n\tINVALID_GENDER,\r\n\tINVALID_NAME,\r\n\tINVALID_PLACE_NAME,\r\n\tINVALID_SURNAME,\r\n\tINVALID_YEAR,\r\n};\r\n","import * as ErrorMessages from \"../const/error-messages.const\";\r\n\r\nclass CfuError extends Error {\r\n constructor(errorMessage: string)\r\n constructor(errorCode: string) {\r\n super((Object.entries(ErrorMessages).find(([errId]) => errId === errorCode) || [])[1] || errorCode);\r\n }\r\n}\r\n\r\nexport default CfuError;\r\n","import {\r\n\tBelfiorePlace,\r\n\tIBelfioreConnector,\r\n} from \"@marketto/belfiore-connector\";\r\nimport DiacriticRemover from \"@marketto/diacritic-remover\";\r\nimport {\r\n\tINVALID_DATE,\r\n\tINVALID_DAY,\r\n\tINVALID_DAY_OR_GENDER,\r\n\tINVALID_GENDER,\r\n\tINVALID_NAME,\r\n\tINVALID_SURNAME,\r\n\tINVALID_YEAR,\r\n} from \"../const/error-messages.const\";\r\nimport {\r\n\tBELFIORE_CODE_MATCHER,\r\n\tCF_NAME_MATCHER,\r\n\tCF_SURNAME_MATCHER,\r\n\tCHECK_DIGIT,\r\n\tCODICE_FISCALE,\r\n\tCONSONANT_LIST,\r\n\tDAY_MATCHER,\r\n\tFEMALE_DAY_MATCHER,\r\n\tFEMALE_FULL_DATE_MATCHER,\r\n\tFULL_DATE_MATCHER,\r\n\tMALE_DAY_MATCHER,\r\n\tMALE_FULL_DATE_MATCHER,\r\n\tMONTH_MATCHER,\r\n\tVOWEL_LIST,\r\n\tYEAR_MATCHER,\r\n} from \"../const/matcher.const\";\r\nimport {\r\n\tDATE_MATCHER,\r\n\tDateDay,\r\n\tDateMonth,\r\n\tDateUtils,\r\n\tMultiFormatDate,\r\n} from \"../date-utils/\";\r\nimport Omocodes from \"../enums/omocodes.enum\";\r\nimport type IPersonalInfo from \"../interfaces/personal-info.interface\";\r\nimport type Genders from \"../types/genders.type\";\r\nimport CfuError from \"./cfu-error.class\";\r\nimport Gender from \"./gender.class\";\r\nimport Parser from \"./parser.class\";\r\nimport dayjs from \"dayjs\";\r\n\r\nconst diacriticRemover = new DiacriticRemover();\r\n\r\nexport default class Pattern {\r\n\tprivate parser: Parser;\r\n\r\n\tconstructor(private readonly belfioreConnector: IBelfioreConnector) {\r\n\t\tthis.parser = new Parser(belfioreConnector);\r\n\t}\r\n\r\n\t/**\r\n\t * Validation regexp for the given lastName or generic\r\n\t * @param lastName Optional lastName to generate validation regexp\r\n\t * @return CF Surname matcher\r\n\t * @throw INVALID_SURNAME\r\n\t */\r\n\tpublic cfLastName(lastName?: string): RegExp {\r\n\t\tlet matcher: string = CF_SURNAME_MATCHER;\r\n\t\tif (lastName) {\r\n\t\t\tif (!this.lastName().test(lastName)) {\r\n\t\t\t\tthrow new CfuError(INVALID_SURNAME);\r\n\t\t\t}\r\n\t\t\tmatcher = this.parser.lastNameToCf(lastName) || matcher;\r\n\t\t}\r\n\t\treturn this.isolatedInsensitiveTailor(matcher);\r\n\t}\r\n\r\n\t/**\r\n\t * Validation regexp for the given name or generic\r\n\t * @param name Optional name to generate validation regexp\r\n\t * @return CF name matcher\r\n\t * @throw INVALID_NAME\r\n\t */\r\n\tpublic cfFirstName(name?: string): RegExp {\r\n\t\tlet matcher: string = CF_NAME_MATCHER;\r\n\t\tif (name) {\r\n\t\t\tif (!this.lastName().test(name)) {\r\n\t\t\t\tthrow new CfuError(INVALID_NAME);\r\n\t\t\t}\r\n\t\t\tmatcher = this.parser.firstNameToCf(name) || matcher;\r\n\t\t}\r\n\t\treturn this.isolatedInsensitiveTailor(matcher);\r\n\t}\r\n\r\n\t/**\r\n\t * Validation regexp for the given year or generic\r\n\t * @param year Optional year to generate validation regexp\r\n\t * @return CF year matcher\r\n\t */\r\n\tpublic cfYear(year?: number): RegExp {\r\n\t\tlet matcher: string = YEAR_MATCHER;\r\n\t\tif (year) {\r\n\t\t\tconst parsedYear = this.parser.yearToCf(year);\r\n\t\t\tif (parsedYear) {\r\n\t\t\t\tmatcher = this.deomocode(parsedYear);\r\n\t\t\t} else {\r\n\t\t\t\tthrow new CfuError(INVALID_YEAR);\r\n\t\t\t}\r\n\t\t}\r\n\t\treturn this.isolatedInsensitiveTailor(matcher);\r\n\t}\r\n\r\n\t/**\r\n\t * Validation regexp for the given month or generic\r\n\t * @param month Optional month to generate validation regexp\r\n\t * @return CF month matcher\r\n\t */\r\n\tpublic cfMonth(month?: DateMonth) {\r\n\t\tlet matcher: string = MONTH_MATCHER;\r\n\t\tif (month) {\r\n\t\t\tmatcher = this.parser.monthToCf(month) || matcher;\r\n\t\t}\r\n\t\treturn this.isolatedInsensitiveTailor(matcher);\r\n\t}\r\n\r\n\t/**\r\n\t * Validation regexp for the given day or generic\r\n\t * @param day Optional day to generate validation regexp\r\n\t * @return CF day matcher\r\n\t */\r\n\tpublic cfDay(day?: DateDay): RegExp {\r\n\t\tlet matcher = DAY_MATCHER;\r\n\t\tif (day) {\r\n\t\t\tconst parsedDayM = this.parser.dayGenderToCf(day, \"M\");\r\n\t\t\tconst parsedDayF = this.parser.dayGenderToCf(day, \"F\");\r\n\t\t\tif (parsedDayM && parsedDayF) {\r\n\t\t\t\tconst matcherM: string = this.deomocode(parsedDayM);\r\n\t\t\t\tconst matcherF: string = this.deomocode(parsedDayF);\r\n\t\t\t\tmatcher = `(?:${matcherM})|(?:${matcherF})`;\r\n\t\t\t} else {\r\n\t\t\t\tthrow new CfuError(INVALID_DAY);\r\n\t\t\t}\r\n\t\t}\r\n\t\treturn this.isolatedInsensitiveTailor(matcher);\r\n\t}\r\n\r\n\t/**\r\n\t * Validation regexp for the given year or generic\r\n\t * @param day Optional day to generate validation regexp\r\n\t * @param gender Gender @see Genders\r\n\t * @return CF day and gender matcher\r\n\t */\r\n\tpublic cfDayGender(day?: DateDay, gender?: Genders): RegExp {\r\n\t\tif (!gender) {\r\n\t\t\treturn this.cfDay(day);\r\n\t\t}\r\n\t\tlet matcher;\r\n\t\tif (day) {\r\n\t\t\tconst parsedDayGender = this.parser.dayGenderToCf(day, gender);\r\n\t\t\tif (parsedDayGender) {\r\n\t\t\t\tmatcher = this.deomocode(parsedDayGender);\r\n\t\t\t} else {\r\n\t\t\t\tthrow new CfuError(INVALID_DAY_OR_GENDER);\r\n\t\t\t}\r\n\t\t} else {\r\n\t\t\tswitch (gender) {\r\n\t\t\t\tcase \"M\":\r\n\t\t\t\t\tmatcher = MALE_DAY_MATCHER;\r\n\t\t\t\t\tbreak;\r\n\t\t\t\tcase \"F\":\r\n\t\t\t\t\tmatcher = FEMALE_DAY_MATCHER;\r\n\t\t\t\t\tbreak;\r\n\t\t\t\tdefault:\r\n\t\t\t\t\tthrow new CfuError(INVALID_GENDER);\r\n\t\t\t}\r\n\t\t}\r\n\t\treturn this.isolatedInsensitiveTailor(matcher);\r\n\t}\r\n\r\n\t/**\r\n\t * Validation regexp for the given year or generic\r\n\t * @param date Optional date to generate validation regexp\r\n\t * @param gender @see Genders\r\n\t * @return CF date and gender matcher\r\n\t */\r\n\tpublic cfDateGender(\r\n\t\tdate?: MultiFormatDate | null,\r\n\t\tgender?: Genders | null\r\n\t): RegExp {\r\n\t\tif (date && !DateUtils.parseDate(date)) {\r\n\t\t\tthrow new CfuError(INVALID_DATE);\r\n\t\t}\r\n\t\tif (gender && !Gender.toArray().includes(gender)) {\r\n\t\t\tthrow new CfuError(INVALID_GENDER);\r\n\t\t}\r\n\t\tlet matcher = FULL_DATE_MATCHER;\r\n\t\tif (date) {\r\n\t\t\tconst parsedDateGender =\r\n\t\t\t\tgender && this.parser.dateGenderToCf(date, gender);\r\n\t\t\tif (parsedDateGender) {\r\n\t\t\t\tmatcher = this.deomocode(parsedDateGender);\r\n\t\t\t} else {\r\n\t\t\t\tconst parseDeomocode = (g: Genders): string => {\r\n\t\t\t\t\tconst parsedGender = this.parser.dateGenderToCf(date, g);\r\n\t\t\t\t\tif (!parsedGender) {\r\n\t\t\t\t\t\tthrow new CfuError(INVALID_DATE);\r\n\t\t\t\t\t}\r\n\t\t\t\t\treturn parsedGender && this.deomocode(parsedGender);\r\n\t\t\t\t};\r\n\t\t\t\tmatcher = `(?:${Gender.toArray().map(parseDeomocode).join(\"|\")})`;\r\n\t\t\t}\r\n\t\t} else if (gender === \"M\") {\r\n\t\t\tmatcher = MALE_FULL_DATE_MATCHER;\r\n\t\t} else if (gender === \"F\") {\r\n\t\t\tmatcher = FEMALE_FULL_DATE_MATCHER;\r\n\t\t}\r\n\t\treturn this.isolatedInsensitiveTailor(matcher);\r\n\t}\r\n\r\n\t/**\r\n\t * @param placeName Optional place name to generate validation regexp\r\n\t * @return CF place matcher\r\n\t */\r\n\t/**\r\n\t * @param date Optional date to generate validation regexp\r\n\t * @param placeName Optional place name to generate validation regexp\r\n\t * @return CF place matcher\r\n\t */\r\n\tpublic async cfPlace(placeName?: string | null): Promise<RegExp>;\r\n\tpublic async cfPlace(\r\n\t\tbirthDate?: MultiFormatDate | null,\r\n\t\tplaceName?: string | null\r\n\t): Promise<RegExp>;\r\n\tpublic async cfPlace(\r\n\t\tbirthDateOrName?: MultiFormatDate | null,\r\n\t\tplaceName?: string | null\r\n\t): Promise<RegExp> {\r\n\t\tlet matcher = BELFIORE_CODE_MATCHER;\r\n\t\tif (birthDateOrName) {\r\n\t\t\tconst birthDate: Date | null = DateUtils.parseDate(birthDateOrName);\r\n\r\n\t\t\tif (birthDate && placeName) {\r\n\t\t\t\tconst place: string = placeName;\r\n\t\t\t\tconst parsedPlace = await this.parser.placeToCf(birthDate, place);\r\n\t\t\t\tmatcher = this.deomocode(parsedPlace || \"\");\r\n\t\t\t} else if (!birthDate && typeof birthDateOrName === \"string\") {\r\n\t\t\t\tconst place: string = birthDateOrName;\r\n\t\t\t\tconst parsedPlace = await this.parser.placeToCf(place);\r\n\t\t\t\tmatcher = this.deomocode(parsedPlace || \"\");\r\n\t\t\t}\r\n\t\t}\r\n\t\treturn this.isolatedInsensitiveTailor(matcher);\r\n\t}\r\n\r\n\t/**\r\n\t * Generates full CF validator based on given optional input or generic\r\n\t * @param personalInfo Input Object\r\n\t * @return CodiceFiscale matcher\r\n\t */\r\n\tpublic async codiceFiscale(\r\n\t\tpersonalInfo?: Omit<IPersonalInfo, \"place\"> & {\r\n\t\t\tplace?: BelfiorePlace | string | undefined;\r\n\t\t}\r\n\t): Promise<RegExp> {\r\n\t\tlet matcher = CODICE_FISCALE;\r\n\t\tif (personalInfo) {\r\n\t\t\tconst parsedCf = await this.parser.encodeCf(personalInfo);\r\n\r\n\t\t\ti