UNPKG

@marketto/codice-fiscale-utils

Version:

TS & JS utilities to handle Italian Codice Fiscale

510 lines (475 loc) 15.4 kB
import { BelfiorePlace, IBelfioreConnector, } from "@marketto/belfiore-connector"; import DiacriticRemover from "@marketto/diacritic-remover"; import { INVALID_DATE, INVALID_DAY, INVALID_DAY_OR_GENDER, INVALID_GENDER, INVALID_NAME, INVALID_SURNAME, INVALID_YEAR, } from "../const/error-messages.const"; import { BELFIORE_CODE_MATCHER, CF_NAME_MATCHER, CF_SURNAME_MATCHER, CHECK_DIGIT, CODICE_FISCALE, CONSONANT_LIST, DAY_MATCHER, FEMALE_DAY_MATCHER, FEMALE_FULL_DATE_MATCHER, FULL_DATE_MATCHER, MALE_DAY_MATCHER, MALE_FULL_DATE_MATCHER, MONTH_MATCHER, VOWEL_LIST, YEAR_MATCHER, } from "../const/matcher.const"; import { DATE_MATCHER, DateDay, DateMonth, DateUtils, MultiFormatDate, } from "../date-utils/"; import Omocodes from "../enums/omocodes.enum"; import type IPersonalInfo from "../interfaces/personal-info.interface"; import type Genders from "../types/genders.type"; import CfuError from "./cfu-error.class"; import Gender from "./gender.class"; import Parser from "./parser.class"; import dayjs from "dayjs"; const diacriticRemover = new DiacriticRemover(); export default class Pattern { private parser: Parser; constructor(private readonly belfioreConnector: IBelfioreConnector) { this.parser = new Parser(belfioreConnector); } /** * Validation regexp for the given lastName or generic * @param lastName Optional lastName to generate validation regexp * @return CF Surname matcher * @throw INVALID_SURNAME */ public cfLastName(lastName?: string): RegExp { let matcher: string = CF_SURNAME_MATCHER; if (lastName) { if (!this.lastName().test(lastName)) { throw new CfuError(INVALID_SURNAME); } matcher = this.parser.lastNameToCf(lastName) || matcher; } return this.isolatedInsensitiveTailor(matcher); } /** * Validation regexp for the given name or generic * @param name Optional name to generate validation regexp * @return CF name matcher * @throw INVALID_NAME */ public cfFirstName(name?: string): RegExp { let matcher: string = CF_NAME_MATCHER; if (name) { if (!this.lastName().test(name)) { throw new CfuError(INVALID_NAME); } matcher = this.parser.firstNameToCf(name) || matcher; } return this.isolatedInsensitiveTailor(matcher); } /** * Validation regexp for the given year or generic * @param year Optional year to generate validation regexp * @return CF year matcher */ public cfYear(year?: number): RegExp { let matcher: string = YEAR_MATCHER; if (year) { const parsedYear = this.parser.yearToCf(year); if (parsedYear) { matcher = this.deomocode(parsedYear); } else { throw new CfuError(INVALID_YEAR); } } return this.isolatedInsensitiveTailor(matcher); } /** * Validation regexp for the given month or generic * @param month Optional month to generate validation regexp * @return CF month matcher */ public cfMonth(month?: DateMonth) { let matcher: string = MONTH_MATCHER; if (month) { matcher = this.parser.monthToCf(month) || matcher; } return this.isolatedInsensitiveTailor(matcher); } /** * Validation regexp for the given day or generic * @param day Optional day to generate validation regexp * @return CF day matcher */ public cfDay(day?: DateDay): RegExp { let matcher = DAY_MATCHER; if (day) { const parsedDayM = this.parser.dayGenderToCf(day, "M"); const parsedDayF = this.parser.dayGenderToCf(day, "F"); if (parsedDayM && parsedDayF) { const matcherM: string = this.deomocode(parsedDayM); const matcherF: string = this.deomocode(parsedDayF); matcher = `(?:${matcherM})|(?:${matcherF})`; } else { throw new CfuError(INVALID_DAY); } } return this.isolatedInsensitiveTailor(matcher); } /** * Validation regexp for the given year or generic * @param day Optional day to generate validation regexp * @param gender Gender @see Genders * @return CF day and gender matcher */ public cfDayGender(day?: DateDay, gender?: Genders): RegExp { if (!gender) { return this.cfDay(day); } let matcher; if (day) { const parsedDayGender = this.parser.dayGenderToCf(day, gender); if (parsedDayGender) { matcher = this.deomocode(parsedDayGender); } else { throw new CfuError(INVALID_DAY_OR_GENDER); } } else { switch (gender) { case "M": matcher = MALE_DAY_MATCHER; break; case "F": matcher = FEMALE_DAY_MATCHER; break; default: throw new CfuError(INVALID_GENDER); } } return this.isolatedInsensitiveTailor(matcher); } /** * Validation regexp for the given year or generic * @param date Optional date to generate validation regexp * @param gender @see Genders * @return CF date and gender matcher */ public cfDateGender( date?: MultiFormatDate | null, gender?: Genders | null ): RegExp { if (date && !DateUtils.parseDate(date)) { throw new CfuError(INVALID_DATE); } if (gender && !Gender.toArray().includes(gender)) { throw new CfuError(INVALID_GENDER); } let matcher = FULL_DATE_MATCHER; if (date) { const parsedDateGender = gender && this.parser.dateGenderToCf(date, gender); if (parsedDateGender) { matcher = this.deomocode(parsedDateGender); } else { const parseDeomocode = (g: Genders): string => { const parsedGender = this.parser.dateGenderToCf(date, g); if (!parsedGender) { throw new CfuError(INVALID_DATE); } return parsedGender && this.deomocode(parsedGender); }; matcher = `(?:${Gender.toArray().map(parseDeomocode).join("|")})`; } } else if (gender === "M") { matcher = MALE_FULL_DATE_MATCHER; } else if (gender === "F") { matcher = FEMALE_FULL_DATE_MATCHER; } return this.isolatedInsensitiveTailor(matcher); } /** * @param placeName Optional place name to generate validation regexp * @return CF place matcher */ /** * @param date Optional date to generate validation regexp * @param placeName Optional place name to generate validation regexp * @return CF place matcher */ public async cfPlace(placeName?: string | null): Promise<RegExp>; public async cfPlace( birthDate?: MultiFormatDate | null, placeName?: string | null ): Promise<RegExp>; public async cfPlace( birthDateOrName?: MultiFormatDate | null, placeName?: string | null ): Promise<RegExp> { let matcher = BELFIORE_CODE_MATCHER; if (birthDateOrName) { const birthDate: Date | null = DateUtils.parseDate(birthDateOrName); if (birthDate && placeName) { const place: string = placeName; const parsedPlace = await this.parser.placeToCf(birthDate, place); matcher = this.deomocode(parsedPlace || ""); } else if (!birthDate && typeof birthDateOrName === "string") { const place: string = birthDateOrName; const parsedPlace = await this.parser.placeToCf(place); matcher = this.deomocode(parsedPlace || ""); } } return this.isolatedInsensitiveTailor(matcher); } /** * Generates full CF validator based on given optional input or generic * @param personalInfo Input Object * @return CodiceFiscale matcher */ public async codiceFiscale( personalInfo?: Omit<IPersonalInfo, "place"> & { place?: BelfiorePlace | string | undefined; } ): Promise<RegExp> { let matcher = CODICE_FISCALE; if (personalInfo) { const parsedCf = await this.parser.encodeCf(personalInfo); if (parsedCf) { matcher = this.deomocode(parsedCf); } else { const { lastName, firstName, year, month, day, date, gender, place } = personalInfo; if ( lastName || firstName || year || month || day || date || gender || place ) { let dtParams: Date | null = null; if (date) { dtParams = DateUtils.parseDate(date); } else if (year) { dtParams = this.parser.yearMonthDayToDate(year, month, day); } const generator: (() => Promise<RegExp>)[] = [ async () => this.cfLastName(lastName), async () => this.cfFirstName(firstName), async () => this.cfDateGender(dtParams, gender), async () => await this.cfPlace( dtParams, (place as BelfiorePlace)?.belfioreCode || (place as string) ), ]; matcher = ""; for (const validator of generator) { const cfMatcher = (await validator()).toString(); const match = cfMatcher.match(/\^(.{1,256})\$/); const cfValue: string | null | undefined = match && match[1]; if (!cfValue) { throw new Error(`Unable to handle [${cfMatcher}]`); } matcher += `(?:${cfValue})`; } // Final addition of CheckDigit matcher += CHECK_DIGIT; } } } return this.isolatedInsensitiveTailor(matcher); } private LETTER_SET: string = `[A-Z${diacriticRemover.matcherBy( /^[A-Z]$/iu )}]`; private SEPARATOR_SET: string = "(?:'?\\s{0,4})"; /** * Returns lastName validator based on given cf or generic * @param codiceFiscale Partial or complete CF to parse * @return Generic or specific regular expression */ public lastName(codiceFiscale?: string): RegExp { let matcher: string = `${this.LETTER_SET}{1,24}`; if (codiceFiscale && /^[A-Z]{1,3}/iu.test(codiceFiscale)) { const lastNameCf: string = codiceFiscale.substr(0, 3); const diacriticizer = (matchingChars: string) => matchingChars .split("") .map((char) => `[${diacriticRemover.insensitiveMatcher[char]}]`); const [cons, vow] = [ `^[${CONSONANT_LIST}]{1,3}`, `[${VOWEL_LIST}]{1,3}`, ].map((charMatcher) => diacriticizer( (lastNameCf.match(new RegExp(charMatcher, "ig")) || [])[0] || "" ) ); const diacriticsVowelList: string = VOWEL_LIST + diacriticRemover.matcherBy(new RegExp(`^[${VOWEL_LIST}]$`, "ui")); const diacriticsVowelMatcher: string = `[${diacriticsVowelList}]`; const midDiacriticVowelMatcher: string = `(?:${diacriticsVowelMatcher}${this.SEPARATOR_SET}){0,24}`; const endingDiacritcVowelMatcher: string = `(?:${this.SEPARATOR_SET}${midDiacriticVowelMatcher}${diacriticsVowelMatcher})?`; switch (cons.length) { case 3: { const divider = midDiacriticVowelMatcher; matcher = divider + cons.join(`${this.SEPARATOR_SET}${divider}`) + `(?:${this.SEPARATOR_SET}${this.LETTER_SET}{0,24}${this.LETTER_SET})?`; break; } case 2: { const possibilities = [ `${vow[0]}${midDiacriticVowelMatcher}${this.SEPARATOR_SET}${cons[0]}${midDiacriticVowelMatcher}${cons[1]}`, `${cons[0]}${this.SEPARATOR_SET}` + vow.join(`${this.SEPARATOR_SET}`) + `${this.SEPARATOR_SET}${midDiacriticVowelMatcher}${cons[1]}`, cons.join(`${this.SEPARATOR_SET}`) + `${this.SEPARATOR_SET}${vow[0]}`, ]; matcher = `(?:${possibilities.join( "|" )})${endingDiacritcVowelMatcher}`; break; } case 1: { const possibilities = [ vow.slice(0, 2).join(`${this.SEPARATOR_SET}`) + midDiacriticVowelMatcher + cons.join(`${this.SEPARATOR_SET}`), `${vow[0]}${this.SEPARATOR_SET}` + cons.join(`${this.SEPARATOR_SET}`) + vow[1], [cons[0], ...vow.slice(0, 2)].join(`${this.SEPARATOR_SET}`), ]; matcher = `(?:${possibilities.join( "|" )})${endingDiacritcVowelMatcher}`; break; } default: matcher = `${vow.join( `${this.SEPARATOR_SET}` )}${endingDiacritcVowelMatcher}`; } if (vow?.length + cons?.length < 3) { return this.isolatedInsensitiveTailor(`\\s{0,4}(${matcher})\\s{0,4}`); } } return this.isolatedInsensitiveTailor( `\\s{0,4}((?:${matcher})(?:${this.SEPARATOR_SET}${this.LETTER_SET}{1,24}){0,24})\\s{0,4}` ); } /** * Returns name validator based on given cf or generic * @param codiceFiscale Partial or complete CF to parse * @return Generic or specific regular expression */ public firstName(codiceFiscale?: string): RegExp { if ( codiceFiscale && new RegExp(`^[A-Z]{3}[${CONSONANT_LIST}]{3}`, "iu").test(codiceFiscale) ) { const nameCf: string = codiceFiscale.substr(3, 3); const cons: string[] = ( (nameCf.match(new RegExp(`^[${CONSONANT_LIST}]{1,3}`, "ig")) || [])[0] || "" ) .split("") .map((char) => `[${diacriticRemover.insensitiveMatcher[char]}]`); const [diacriticsVowelList, diacriticsConsonantList]: string[] = [ VOWEL_LIST, CONSONANT_LIST, ].map( (chars) => chars + diacriticRemover.matcherBy(new RegExp(`^[${chars}]$`, "ui")) ); const matcher: string = `(?:[${diacriticsVowelList}]{1,24}${this.SEPARATOR_SET}){0,24}${cons[0]}${this.SEPARATOR_SET}(?:[${diacriticsVowelList}]{1,24}${this.SEPARATOR_SET}){0,24}(?:[${diacriticsConsonantList}]${this.SEPARATOR_SET}(?:[${diacriticsVowelList}]{1,24}${this.SEPARATOR_SET}){0,24})?` + cons .slice(1, 3) .join( `${this.SEPARATOR_SET}(?:[${diacriticsVowelList}]{1,24}${this.SEPARATOR_SET}){0,24}` ) + `(?:${this.SEPARATOR_SET}${this.LETTER_SET}{1,24}){0,24}`; return this.isolatedInsensitiveTailor(matcher); } return this.lastName((codiceFiscale || "").substr(3, 3)); } /** * Returns iso8601 date validator based on given cf or generic * @param codiceFiscale Partial or complete CF to parse * @return Generic or specific regular expression */ public date(codiceFiscale?: string): RegExp { let matcher: string = DATE_MATCHER.ISO8601_DATE_TIME; if (codiceFiscale) { const parsedDate = this.parser.cfToBirthDate(codiceFiscale); if (parsedDate) { const dateIso8601: string = parsedDate.toJSON(); if (dayjs().diff(dayjs(parsedDate), "y") < 50) { const century: number = parseInt(dateIso8601.substr(0, 2), 10); const centuries: string[] = [century - 1, century].map((year) => year.toString().padStart(2, "0") ); matcher = `(?:${centuries.join("|")})` + dateIso8601.substr(2, 8); } else { matcher = dateIso8601.substr(0, 10); } } } return this.isolatedInsensitiveTailor( `${matcher}(?:T${DATE_MATCHER.TIME}(?:${DATE_MATCHER.TIMEZONE})?)?` ); } /** * Returns gender validator based on given cf or generic * @param codiceFiscale Partial or complete CF to parse * @return Generic or specific regular expression */ public gender(codiceFiscale?: string): RegExp { const parsedGender = codiceFiscale && this.parser.cfToGender(codiceFiscale); const matcher: string = parsedGender || `[${Gender.toArray().join("")}]`; return this.isolatedInsensitiveTailor(matcher); } /** * Returns place validator based on given cf or generic * @param codiceFiscale Partial or complete CF to parse * @return Generic or specific regular expression */ public async place(codiceFiscale?: string): Promise<RegExp> { let matcher: string = ".{1,32}"; const parsedPlace = codiceFiscale && (await this.parser.cfToBirthPlace(codiceFiscale)); if (parsedPlace) { const nameMatcher: string = parsedPlace.name.replace(/./gu, (c: string) => diacriticRemover[c] === c ? c : `[${c}${diacriticRemover[c]}]` ); matcher = `(?:(?:${nameMatcher})|${parsedPlace.belfioreCode})`; } return this.isolatedInsensitiveTailor(matcher); } public deomocode(omocode: string): string { return omocode.replace(/\d/gu, (n: any) => `[${n}${Omocodes[n]}]`); } private isolatedInsensitiveTailor(matcher: string): RegExp { return new RegExp(`^(?:${matcher})$`, "iu"); } }