diff --git a/README.md b/README.md
index 20a615489..17120c698 100644
--- a/README.md
+++ b/README.md
@@ -152,7 +152,7 @@ Validator | Description
**isUppercase(str)** | check if the string is uppercase.
**isSlug** | Check if the string is of type slug. `Options` allow a single hyphen between string. e.g. [`cn-cn`, `cn-c-c`]
**isStrongPassword(str [, options])** | Check if a password is strong or not. Allows for custom requirements or scoring rules. If `returnScore` is true, then the function returns an integer score for the password rather than a boolean.
Default options:
`{ minLength: 8, minLowercase: 1, minUppercase: 1, minNumbers: 1, minSymbols: 1, returnScore: false, pointsPerUnique: 1, pointsPerRepeat: 0.5, pointsForContainingLower: 10, pointsForContainingUpper: 10, pointsForContainingNumber: 10, pointsForContainingSymbol: 10 }`
-**isTaxID(str, locale)** | Check if the given value is a valid Tax Identification Number. Default locale is `en-US`
+**isTaxID(str, locale)** | Check if the given value is a valid Tax Identification Number. Default locale is `en-US`.
More info about exact TIN support can be found in `src/lib/isTaxID.js`
Supported locales: `[ 'bg-BG', 'cs-CZ', 'de-AT', 'de-DE', 'dk-DK', 'el-CY', 'el-GR', 'en-GB', 'en-IE', 'en-US', 'es-ES', 'et-EE', 'fi-FI', 'fr-BE', 'fr-FR', 'fr-LU', 'hr-HR', 'hu-HU', 'it-IT', 'lb-LU', 'lt-LT', 'lv-LV' 'mt-MT', 'nl-BE', 'nl-NL', 'pl-PL', 'pt-PT', 'ro-RO', 'sk-SK', 'sl-SI', 'sv-SE' ]`
**isURL(str [, options])** | check if the string is an URL.
`options` is an object which defaults to `{ protocols: ['http','https','ftp'], require_tld: true, require_protocol: false, require_host: true, require_valid_protocol: true, allow_underscores: false, host_whitelist: false, host_blacklist: false, allow_trailing_dot: false, allow_protocol_relative_urls: false, disallow_auth: false }`.
require_protocol - if set as true isURL will return false if protocol is not present in the URL.
require_valid_protocol - isURL will check if the URL's protocol is present in the protocols option.
protocols - valid protocols can be modified with this option.
require_host - if set as false isURL will not check if host is present in the URL.
require_port - if set as true isURL will check if port is present in the URL.
allow_protocol_relative_urls - if set as true protocol relative URLs will be allowed.
validate_length - if set as false isURL will skip string length validation (2083 characters is IE max URL length).
**isUUID(str [, version])** | check if the string is a UUID (version 3, 4 or 5).
**isVariableWidth(str)** | check if the string contains a mixture of full and half-width chars.
diff --git a/src/lib/isTaxID.js b/src/lib/isTaxID.js
index 446ebd088..40edfe381 100644
--- a/src/lib/isTaxID.js
+++ b/src/lib/isTaxID.js
@@ -1,8 +1,17 @@
import assertString from './util/assertString';
+import * as algorithms from './util/algorithms';
+import isDate from './isDate';
/**
- * en-US TIN Validation
+ * TIN Validation
+ * Validates Tax Identification Numbers (TINs) from the US, EU member states and the United Kingdom.
*
+ * EU-UK:
+ * National TIN validity is calculated using public algorithms as made available by DG TAXUD.
+ *
+ * See `https://ec.europa.eu/taxation_customs/tin/specs/FS-TIN%20Algorithms-Public.docx` for more information.
+ *
+ * US:
* An Employer Identification Number (EIN), also known as a Federal Tax Identification Number,
* is used to identify a business entity.
*
@@ -14,6 +23,289 @@ import assertString from './util/assertString';
* for more information.
*/
+// Locale functions
+
+/*
+ * bg-BG validation function
+ * (Edinen graždanski nomer (EGN/ЕГН), persons only)
+ * Checks if birth date (first six digits) is valid and calculates check (last) digit
+ */
+function bgBgCheck(tin) {
+ // Extract full year, normalize month and check birth date validity
+ let century_year = tin.slice(0, 2);
+ let month = parseInt(tin.slice(2, 4), 10);
+ if (month > 40) {
+ month -= 40;
+ century_year = `20${century_year}`;
+ } else if (month > 20) {
+ month -= 20;
+ century_year = `18${century_year}`;
+ } else {
+ century_year = `19${century_year}`;
+ }
+ if (month < 10) { month = `0${month}`; }
+ const date = `${century_year}/${month}/${tin.slice(4, 6)}`;
+ if (!isDate(date, 'YYYY/MM/DD')) { return false; }
+
+ // split digits into an array for further processing
+ const digits = tin.split('').map(a => parseInt(a, 10));
+
+ // Calculate checksum by multiplying digits with fixed values
+ const multip_lookup = [2, 4, 8, 5, 10, 9, 7, 3, 6];
+ let checksum = 0;
+ for (let i = 0; i < multip_lookup.length; i++) {
+ checksum += digits[i] * multip_lookup[i];
+ }
+ checksum = checksum % 11 === 10 ? 0 : checksum % 11;
+ return checksum === digits[9];
+}
+
+/*
+ * cs-CZ validation function
+ * (Rodné číslo (RČ), persons only)
+ * Checks if birth date (first six digits) is valid and divisibility by 11
+ * Material not in DG TAXUD document sourced from:
+ * -`https://lorenc.info/3MA381/overeni-spravnosti-rodneho-cisla.htm`
+ * -`https://www.mvcr.cz/clanek/rady-a-sluzby-dokumenty-rodne-cislo.aspx`
+ */
+function csCzCheck(tin) {
+ tin = tin.replace(/\W/, '');
+
+ // Extract full year from TIN length
+ let full_year = parseInt(tin.slice(0, 2), 10);
+ if (tin.length === 10) {
+ if (full_year < 54) {
+ full_year = `20${full_year}`;
+ } else {
+ full_year = `19${full_year}`;
+ }
+ } else {
+ if (tin.slice(6) === '000') { return false; } // Three-zero serial not assigned before 1954
+ if (full_year < 54) {
+ full_year = `19${full_year}`;
+ } else {
+ return false; // No 18XX years seen in any of the resources
+ }
+ }
+ // Add missing zero if needed
+ if (full_year.length === 3) {
+ full_year = [full_year.slice(0, 2), '0', full_year.slice(2)].join('');
+ }
+
+ // Extract month from TIN and normalize
+ let month = parseInt(tin.slice(2, 4), 10);
+ if (month > 50) {
+ month -= 50;
+ }
+ if (month > 20) {
+ // Month-plus-twenty was only introduced in 2004
+ if (parseInt(full_year, 10) < 2004) { return false; }
+ month -= 20;
+ }
+ if (month < 10) { month = `0${month}`; }
+
+ // Check date validity
+ const date = `${full_year}/${month}/${tin.slice(4, 6)}`;
+ if (!isDate(date, 'YYYY/MM/DD')) { return false; }
+
+ // Verify divisibility by 11
+ if (tin.length === 10) {
+ if (parseInt(tin, 10) % 11 !== 0) {
+ // Some numbers up to and including 1985 are still valid if
+ // check (last) digit equals 0 and modulo of first 9 digits equals 10
+ const checkdigit = parseInt(tin.slice(0, 9), 10) % 11;
+ if (parseInt(full_year, 10) < 1986 && checkdigit === 10) {
+ if (parseInt(tin.slice(9), 10) !== 0) { return false; }
+ } else {
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+/*
+ * de-AT validation function
+ * (Abgabenkontonummer, persons/entities)
+ * Verify TIN validity by calling luhnCheck()
+ */
+function deAtCheck(tin) {
+ return algorithms.luhnCheck(tin);
+}
+
+/*
+ * de-DE validation function
+ * (Steueridentifikationsnummer (Steuer-IdNr.), persons only)
+ * Tests for single duplicate/triplicate value, then calculates ISO 7064 check (last) digit
+ * Partial implementation of spec (same result with both algorithms always)
+ */
+function deDeCheck(tin) {
+ // Split digits into an array for further processing
+ const digits = tin.split('').map(a => parseInt(a, 10));
+
+ // Fill array with strings of number positions
+ let occurences = [];
+ for (let i = 0; i < digits.length - 1; i++) {
+ occurences.push('');
+ for (let j = 0; j < digits.length - 1; j++) {
+ if (digits[i] === digits[j]) {
+ occurences[i] += j;
+ }
+ }
+ }
+
+ // Remove digits with one occurence and test for only one duplicate/triplicate
+ occurences = occurences.filter(a => a.length > 1);
+ if (occurences.length !== 2 && occurences.length !== 3) { return false; }
+
+ // In case of triplicate value only two digits are allowed next to each other
+ if (occurences[0].length === 3) {
+ const trip_locations = occurences[0].split('').map(a => parseInt(a, 10));
+ let recurrent = 0; // Amount of neighbour occurences
+ for (let i = 0; i < trip_locations.length - 1; i++) {
+ if (trip_locations[i] + 1 === trip_locations[i + 1]) {
+ recurrent += 1;
+ }
+ }
+ if (recurrent === 2) {
+ return false;
+ }
+ }
+ return algorithms.iso7064Check(tin);
+}
+
+/*
+ * dk-DK validation function
+ * (CPR-nummer (personnummer), persons only)
+ * Checks if birth date (first six digits) is valid and assigned to century (seventh) digit,
+ * and calculates check (last) digit
+ */
+function dkDkCheck(tin) {
+ tin = tin.replace(/\W/, '');
+
+ // Extract year, check if valid for given century digit and add century
+ let year = parseInt(tin.slice(4, 6), 10);
+ const century_digit = tin.slice(6, 7);
+ switch (century_digit) {
+ case '0':
+ case '1':
+ case '2':
+ case '3':
+ year = `19${year}`;
+ break;
+ case '4':
+ case '9':
+ if (year < 37) {
+ year = `20${year}`;
+ } else {
+ year = `19${year}`;
+ }
+ break;
+ default:
+ if (year < 37) {
+ year = `20${year}`;
+ } else if (year > 58) {
+ year = `18${year}`;
+ } else {
+ return false;
+ }
+ break;
+ }
+ // Add missing zero if needed
+ if (year.length === 3) {
+ year = [year.slice(0, 2), '0', year.slice(2)].join('');
+ }
+ // Check date validity
+ const date = `${year}/${tin.slice(2, 4)}/${tin.slice(0, 2)}`;
+ if (!isDate(date, 'YYYY/MM/DD')) { return false; }
+
+ // Split digits into an array for further processing
+ const digits = tin.split('').map(a => parseInt(a, 10));
+ let checksum = 0;
+ let weight = 4;
+ // Multiply by weight and add to checksum
+ for (let i = 0; i < 9; i++) {
+ checksum += digits[i] * weight;
+ weight -= 1;
+ if (weight === 1) {
+ weight = 7;
+ }
+ }
+ checksum %= 11;
+ if (checksum === 1) { return false; }
+ return checksum === 0 ? digits[9] === 0 : digits[9] === 11 - checksum;
+}
+
+/*
+ * el-CY validation function
+ * (Arithmos Forologikou Mitroou (AFM/ΑΦΜ), persons only)
+ * Verify TIN validity by calculating ASCII value of check (last) character
+ */
+function elCyCheck(tin) {
+ // split digits into an array for further processing
+ const digits = tin.slice(0, 8).split('').map(a => parseInt(a, 10));
+
+ let checksum = 0;
+ // add digits in even places
+ for (let i = 1; i < digits.length; i += 2) {
+ checksum += digits[i];
+ }
+
+ // add digits in odd places
+ for (let i = 0; i < digits.length; i += 2) {
+ if (digits[i] < 2) {
+ checksum += 1 - digits[i];
+ } else {
+ checksum += (2 * (digits[i] - 2)) + 5;
+ if (digits[i] > 4) {
+ checksum += 2;
+ }
+ }
+ }
+ return String.fromCharCode((checksum % 26) + 65) === tin.charAt(8);
+}
+
+/*
+ * el-GR validation function
+ * (Arithmos Forologikou Mitroou (AFM/ΑΦΜ), persons/entities)
+ * Verify TIN validity by calculating check (last) digit
+ * Algorithm not in DG TAXUD document- sourced from:
+ * - `http://epixeirisi.gr/%CE%9A%CE%A1%CE%99%CE%A3%CE%99%CE%9C%CE%91-%CE%98%CE%95%CE%9C%CE%91%CE%A4%CE%91-%CE%A6%CE%9F%CE%A1%CE%9F%CE%9B%CE%9F%CE%93%CE%99%CE%91%CE%A3-%CE%9A%CE%91%CE%99-%CE%9B%CE%9F%CE%93%CE%99%CE%A3%CE%A4%CE%99%CE%9A%CE%97%CE%A3/23791/%CE%91%CF%81%CE%B9%CE%B8%CE%BC%CF%8C%CF%82-%CE%A6%CE%BF%CF%81%CE%BF%CE%BB%CE%BF%CE%B3%CE%B9%CE%BA%CE%BF%CF%8D-%CE%9C%CE%B7%CF%84%CF%81%CF%8E%CE%BF%CF%85`
+ */
+function elGrCheck(tin) {
+ // split digits into an array for further processing
+ const digits = tin.split('').map(a => parseInt(a, 10));
+
+ let checksum = 0;
+ for (let i = 0; i < 8; i++) {
+ checksum += digits[i] * (2 ** (8 - i));
+ }
+ return checksum % 11 === digits[8];
+}
+
+/*
+ * en-GB validation function (should go here if needed)
+ * (National Insurance Number (NINO) or Unique Taxpayer Reference (UTR),
+ * persons/entities respectively)
+ */
+
+/*
+ * en-IE validation function
+ * (Personal Public Service Number (PPS No), persons only)
+ * Verify TIN validity by calculating check (second to last) character
+ */
+function enIeCheck(tin) {
+ let checksum = algorithms.reverseMultiplyAndSum(tin.split('').slice(0, 7).map(a => parseInt(a, 10)), 8);
+ if (tin.length === 9 && tin[8] !== 'W') {
+ checksum += (tin[8].charCodeAt(0) - 64) * 9;
+ }
+
+ checksum %= 23;
+ if (checksum === 0) {
+ return tin[7].toUpperCase() === 'W';
+ }
+ return tin[7].toUpperCase() === String.fromCharCode(64 + checksum);
+}
// Valid US IRS campus prefixes
const enUsCampusPrefix = {
@@ -54,20 +346,757 @@ function enUsCheck(tin) {
return enUsGetPrefixes().indexOf(tin.substr(0, 2)) !== -1;
}
-// tax id regex formats for various locales
+/*
+ * es-ES validation function
+ * (Documento Nacional de Identidad (DNI)
+ * or Número de Identificación de Extranjero (NIE), persons only)
+ * Verify TIN validity by calculating check (last) character
+ */
+function esEsCheck(tin) {
+ // Split characters into an array for further processing
+ let chars = tin.toUpperCase().split('');
+
+ // Replace initial letter if needed
+ if (isNaN(parseInt(chars[0], 10)) && chars.length > 1) {
+ let lead_replace = 0;
+ switch (chars[0]) {
+ case 'Y':
+ lead_replace = 1;
+ break;
+ case 'Z':
+ lead_replace = 2;
+ break;
+ default:
+ }
+ chars.splice(0, 1, lead_replace);
+ // Fill with zeros if smaller than proper
+ } else {
+ while (chars.length < 9) {
+ chars.unshift(0);
+ }
+ }
+
+ // Calculate checksum and check according to lookup
+ const lookup = ['T', 'R', 'W', 'A', 'G', 'M', 'Y', 'F', 'P', 'D', 'X', 'B', 'N', 'J', 'Z', 'S', 'Q', 'V', 'H', 'L', 'C', 'K', 'E'];
+ chars = chars.join('');
+ let checksum = (parseInt(chars.slice(0, 8), 10) % 23);
+ return chars[8] === lookup[checksum];
+}
+
+/*
+ * et-EE validation function
+ * (Isikukood (IK), persons only)
+ * Checks if birth date (century digit and six following) is valid and calculates check (last) digit
+ * Material not in DG TAXUD document sourced from:
+ * - `https://www.oecd.org/tax/automatic-exchange/crs-implementation-and-assistance/tax-identification-numbers/Estonia-TIN.pdf`
+ */
+function etEeCheck(tin) {
+ // Extract year and add century
+ let full_year = tin.slice(1, 3);
+ const century_digit = tin.slice(0, 1);
+ switch (century_digit) {
+ case '1':
+ case '2':
+ full_year = `18${full_year}`;
+ break;
+ case '3':
+ case '4':
+ full_year = `19${full_year}`;
+ break;
+ default:
+ full_year = `20${full_year}`;
+ break;
+ }
+ // Check date validity
+ const date = `${full_year}/${tin.slice(3, 5)}/${tin.slice(5, 7)}`;
+ if (!isDate(date, 'YYYY/MM/DD')) { return false; }
+
+ // Split digits into an array for further processing
+ const digits = tin.split('').map(a => parseInt(a, 10));
+ let checksum = 0;
+ let weight = 1;
+ // Multiply by weight and add to checksum
+ for (let i = 0; i < 10; i++) {
+ checksum += digits[i] * weight;
+ weight += 1;
+ if (weight === 10) {
+ weight = 1;
+ }
+ }
+ // Do again if modulo 11 of checksum is 10
+ if (checksum % 11 === 10) {
+ checksum = 0;
+ weight = 3;
+ for (let i = 0; i < 10; i++) {
+ checksum += digits[i] * weight;
+ weight += 1;
+ if (weight === 10) {
+ weight = 1;
+ }
+ }
+ if (checksum % 11 === 10) { return digits[10] === 0; }
+ }
+
+ return checksum % 11 === digits[10];
+}
+
+/*
+ * fi-FI validation function
+ * (Henkilötunnus (HETU), persons only)
+ * Checks if birth date (first six digits plus century symbol) is valid
+ * and calculates check (last) digit
+ */
+function fiFiCheck(tin) {
+ // Extract year and add century
+ let full_year = tin.slice(4, 6);
+ const century_symbol = tin.slice(6, 7);
+ switch (century_symbol) {
+ case '+':
+ full_year = `18${full_year}`;
+ break;
+ case '-':
+ full_year = `19${full_year}`;
+ break;
+ default:
+ full_year = `20${full_year}`;
+ break;
+ }
+ // Check date validity
+ const date = `${full_year}/${tin.slice(2, 4)}/${tin.slice(0, 2)}`;
+ if (!isDate(date, 'YYYY/MM/DD')) { return false; }
+
+ // Calculate check character
+ let checksum = parseInt((tin.slice(0, 6) + tin.slice(7, 10)), 10) % 31;
+ if (checksum < 10) { return checksum === parseInt(tin.slice(10), 10); }
+
+ checksum -= 10;
+ const letters_lookup = ['A', 'B', 'C', 'D', 'E', 'F', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y'];
+ return letters_lookup[checksum] === tin.slice(10);
+}
+
+/*
+ * fr/nl-BE validation function
+ * (Numéro national (N.N.), persons only)
+ * Checks if birth date (first six digits) is valid and calculates check (last two) digits
+ */
+function frBeCheck(tin) {
+ // Zero month/day value is acceptable
+ if (tin.slice(2, 4) !== '00' || tin.slice(4, 6) !== '00') {
+ // Extract date from first six digits of TIN
+ const date = `${tin.slice(0, 2)}/${tin.slice(2, 4)}/${tin.slice(4, 6)}`;
+ if (!isDate(date, 'YY/MM/DD')) { return false; }
+ }
+
+ let checksum = 97 - (parseInt(tin.slice(0, 9), 10) % 97);
+ const checkdigits = parseInt(tin.slice(9, 11), 10);
+ if (checksum !== checkdigits) {
+ checksum = 97 - (parseInt(`2${tin.slice(0, 9)}`, 10) % 97);
+ if (checksum !== checkdigits) {
+ return false;
+ }
+ }
+ return true;
+}
+
+/*
+ * fr-FR validation function
+ * (Numéro fiscal de référence (numéro SPI), persons only)
+ * Verify TIN validity by calculating check (last three) digits
+ */
+function frFrCheck(tin) {
+ tin = tin.replace(/\s/g, '');
+ const checksum = parseInt(tin.slice(0, 10), 10) % 511;
+ const checkdigits = parseInt(tin.slice(10, 13), 10);
+ return checksum === checkdigits;
+}
+
+/*
+ * fr/lb-LU validation function
+ * (numéro d’identification personnelle, persons only)
+ * Verify birth date validity and run Luhn and Verhoeff checks
+ */
+function frLuCheck(tin) {
+ // Extract date and check validity
+ const date = `${tin.slice(0, 4)}/${tin.slice(4, 6)}/${tin.slice(6, 8)}`;
+ if (!isDate(date, 'YYYY/MM/DD')) { return false; }
+
+ // Run Luhn check
+ if (!algorithms.luhnCheck(tin.slice(0, 12))) { return false; }
+ // Remove Luhn check digit and run Verhoeff check
+ return algorithms.verhoeffCheck(`${tin.slice(0, 11)}${tin[12]}`);
+}
+
+/*
+ * hr-HR validation function
+ * (Osobni identifikacijski broj (OIB), persons/entities)
+ * Verify TIN validity by calling iso7064Check(digits)
+ */
+function hrHrCheck(tin) {
+ return algorithms.iso7064Check(tin);
+}
+
+/*
+ * hu-HU validation function
+ * (Adóazonosító jel, persons only)
+ * Verify TIN validity by calculating check (last) digit
+ */
+function huHuCheck(tin) {
+ // split digits into an array for further processing
+ const digits = tin.split('').map(a => parseInt(a, 10));
+
+ let checksum = 8;
+ for (let i = 1; i < 9; i++) {
+ checksum += digits[i] * (i + 1);
+ }
+ return checksum % 11 === digits[9];
+}
+
+/*
+ * lt-LT validation function (should go here if needed)
+ * (Asmens kodas, persons/entities respectively)
+ * Current validation check is alias of etEeCheck- same format applies
+ */
+
+/*
+ * it-IT first/last name validity check
+ * Accepts it-IT TIN-encoded names as a three-element character array and checks their validity
+ * Due to lack of clarity between resources ("Are only Italian consonants used?
+ * What happens if a person has X in their name?" etc.) only two test conditions
+ * have been implemented:
+ * Vowels may only be followed by other vowels or an X character
+ * and X characters after vowels may only be followed by other X characters.
+ */
+function itItNameCheck(name) {
+ // true at the first occurence of a vowel
+ let vowelflag = false;
+
+ // true at the first occurence of an X AFTER vowel
+ // (to properly handle last names with X as consonant)
+ let xflag = false;
+
+ for (let i = 0; i < 3; i++) {
+ if (!vowelflag && /[AEIOU]/.test(name[i])) {
+ vowelflag = true;
+ } else if (!xflag && vowelflag && (name[i] === 'X')) {
+ xflag = true;
+ } else if (i > 0) {
+ if (vowelflag && !xflag) {
+ if (!/[AEIOU]/.test(name[i])) { return false; }
+ }
+ if (xflag) {
+ if (!/X/.test(name[i])) { return false; }
+ }
+ }
+ }
+ return true;
+}
+
+/*
+ * it-IT validation function
+ * (Codice fiscale (TIN-IT), persons only)
+ * Verify name, birth date and codice catastale validity
+ * and calculate check character.
+ * Material not in DG-TAXUD document sourced from:
+ * `https://en.wikipedia.org/wiki/Italian_fiscal_code`
+ */
+function itItCheck(tin) {
+ // Capitalize and split characters into an array for further processing
+ const chars = tin.toUpperCase().split('');
+
+ // Check first and last name validity calling itItNameCheck()
+ if (!itItNameCheck(chars.slice(0, 3))) { return false; }
+ if (!itItNameCheck(chars.slice(3, 6))) { return false; }
+
+ // Convert letters in number spaces back to numbers if any
+ const number_locations = [6, 7, 9, 10, 12, 13, 14];
+ const number_replace = {
+ L: '0',
+ M: '1',
+ N: '2',
+ P: '3',
+ Q: '4',
+ R: '5',
+ S: '6',
+ T: '7',
+ U: '8',
+ V: '9',
+ };
+ for (const i of number_locations) {
+ if (chars[i] in number_replace) {
+ chars.splice(i, 1, number_replace[chars[i]]);
+ }
+ }
+
+ // Extract month and day, and check date validity
+ const month_replace = {
+ A: '01',
+ B: '02',
+ C: '03',
+ D: '04',
+ E: '05',
+ H: '06',
+ L: '07',
+ M: '08',
+ P: '09',
+ R: '10',
+ S: '11',
+ T: '12',
+ };
+ let month = month_replace[chars[8]];
+
+ let day = parseInt(chars[9] + chars[10], 10);
+ if (day > 40) { day -= 40; }
+ if (day < 10) { day = `0${day}`; }
+
+ const date = `${chars[6]}${chars[7]}/${month}/${day}`;
+ if (!isDate(date, 'YY/MM/DD')) { return false; }
+
+ // Calculate check character by adding up even and odd characters as numbers
+ let checksum = 0;
+ for (let i = 1; i < chars.length - 1; i += 2) {
+ let char_to_int = parseInt(chars[i], 10);
+ if (isNaN(char_to_int)) {
+ char_to_int = chars[i].charCodeAt(0) - 65;
+ }
+ checksum += char_to_int;
+ }
+
+ const odd_convert = { // Maps of characters at odd places
+ A: 1,
+ B: 0,
+ C: 5,
+ D: 7,
+ E: 9,
+ F: 13,
+ G: 15,
+ H: 17,
+ I: 19,
+ J: 21,
+ K: 2,
+ L: 4,
+ M: 18,
+ N: 20,
+ O: 11,
+ P: 3,
+ Q: 6,
+ R: 8,
+ S: 12,
+ T: 14,
+ U: 16,
+ V: 10,
+ W: 22,
+ X: 25,
+ Y: 24,
+ Z: 23,
+ 0: 1,
+ 1: 0,
+ };
+ for (let i = 0; i < chars.length - 1; i += 2) {
+ let char_to_int = 0;
+ if (chars[i] in odd_convert) {
+ char_to_int = odd_convert[chars[i]];
+ } else {
+ let multiplier = parseInt(chars[i], 10);
+ char_to_int = (2 * multiplier) + 1;
+ if (multiplier > 4) {
+ char_to_int += 2;
+ }
+ }
+ checksum += char_to_int;
+ }
+
+ if (String.fromCharCode(65 + (checksum % 26)) !== chars[15]) { return false; }
+ return true;
+}
+
+/*
+ * lv-LV validation function
+ * (Personas kods (PK), persons only)
+ * Check validity of birth date and calculate check (last) digit
+ * Support only for old format numbers (not starting with '32', issued before 2017/07/01)
+ * Material not in DG TAXUD document sourced from:
+ * `https://boot.ritakafija.lv/forums/index.php?/topic/88314-personas-koda-algoritms-%C4%8Deksumma/`
+ */
+function lvLvCheck(tin) {
+ tin = tin.replace(/\W/, '');
+ // Extract date from TIN
+ const day = tin.slice(0, 2);
+ if (day !== '32') { // No date/checksum check if new format
+ const month = tin.slice(2, 4);
+ if (month !== '00') { // No date check if unknown month
+ let full_year = tin.slice(4, 6);
+ switch (tin[6]) {
+ case '0':
+ full_year = `18${full_year}`;
+ break;
+ case '1':
+ full_year = `19${full_year}`;
+ break;
+ default:
+ full_year = `20${full_year}`;
+ break;
+ }
+ // Check date validity
+ const date = `${full_year}/${tin.slice(2, 4)}/${day}`;
+ if (!isDate(date, 'YYYY/MM/DD')) { return false; }
+ }
+
+ // Calculate check digit
+ let checksum = 1101;
+ const multip_lookup = [1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
+ for (let i = 0; i < tin.length - 1; i++) {
+ checksum -= parseInt(tin[i], 10) * multip_lookup[i];
+ }
+ return (parseInt(tin[10], 10) === checksum % 11);
+ }
+ return true;
+}
+
+/*
+ * mt-MT validation function
+ * (Identity Card Number or Unique Taxpayer Reference, persons/entities)
+ * Verify Identity Card Number structure (no other tests found)
+ */
+function mtMtCheck(tin) {
+ if (tin.length !== 9) { // No tests for UTR
+ let chars = tin.toUpperCase().split('');
+ // Fill with zeros if smaller than proper
+ while (chars.length < 8) {
+ chars.unshift(0);
+ }
+ // Validate format according to last character
+ switch (tin[7]) {
+ case 'A':
+ case 'P':
+ if (parseInt(chars[6], 10) === 0) { return false; }
+ break;
+ default: {
+ const first_part = parseInt(chars.join('').slice(0, 5), 10);
+ if (first_part > 32000) { return false; }
+ const second_part = parseInt(chars.join('').slice(5, 7), 10);
+ if (first_part === second_part) { return false; }
+ }
+ }
+ }
+ return true;
+}
+
+/*
+ * nl-NL validation function
+ * (Burgerservicenummer (BSN) or Rechtspersonen Samenwerkingsverbanden Informatie Nummer (RSIN),
+ * persons/entities respectively)
+ * Verify TIN validity by calculating check (last) digit (variant of MOD 11)
+ */
+function nlNlCheck(tin) {
+ return algorithms.reverseMultiplyAndSum(tin.split('').slice(0, 8).map(a => parseInt(a, 10)), 9) % 11 === parseInt(tin[8], 10);
+}
+
+/*
+ * pl-PL validation function
+ * (Powszechny Elektroniczny System Ewidencji Ludności (PESEL)
+ * or Numer identyfikacji podatkowej (NIP), persons/entities)
+ * Verify TIN validity by validating birth date (PESEL) and calculating check (last) digit
+ */
+function plPlCheck(tin) {
+ // NIP
+ if (tin.length === 10) {
+ // Calculate last digit by multiplying with lookup
+ const lookup = [6, 5, 7, 2, 3, 4, 5, 6, 7];
+ let checksum = 0;
+ for (let i = 0; i < lookup.length; i++) {
+ checksum += parseInt(tin[i], 10) * lookup[i];
+ }
+ checksum %= 11;
+ if (checksum === 10) { return false; }
+ return (checksum === parseInt(tin[9], 10));
+ }
+
+ // PESEL
+ // Extract full year using month
+ let full_year = tin.slice(0, 2);
+ let month = parseInt(tin.slice(2, 4), 10);
+ if (month > 80) {
+ full_year = `18${full_year}`;
+ month -= 80;
+ } else if (month > 60) {
+ full_year = `22${full_year}`;
+ month -= 60;
+ } else if (month > 40) {
+ full_year = `21${full_year}`;
+ month -= 40;
+ } else if (month > 20) {
+ full_year = `20${full_year}`;
+ month -= 20;
+ } else {
+ full_year = `19${full_year}`;
+ }
+ // Add leading zero to month if needed
+ if (month < 10) { month = `0${month}`; }
+ // Check date validity
+ const date = `${full_year}/${month}/${tin.slice(4, 6)}`;
+ if (!isDate(date, 'YYYY/MM/DD')) { return false; }
+
+ // Calculate last digit by mulitplying with odd one-digit numbers except 5
+ let checksum = 0;
+ let multiplier = 1;
+ for (let i = 0; i < tin.length - 1; i++) {
+ checksum += (parseInt(tin[i], 10) * multiplier) % 10;
+ multiplier += 2;
+ if (multiplier > 10) {
+ multiplier = 1;
+ } else if (multiplier === 5) {
+ multiplier += 2;
+ }
+ }
+ checksum = 10 - (checksum % 10);
+ return checksum === parseInt(tin[10], 10);
+}
+
+/*
+ * pt-PT validation function
+ * (Número de identificação fiscal (NIF), persons/entities)
+ * Verify TIN validity by calculating check (last) digit (variant of MOD 11)
+ */
+function ptPtCheck(tin) {
+ let checksum = 11 - (algorithms.reverseMultiplyAndSum(tin.split('').slice(0, 8).map(a => parseInt(a, 10)), 9) % 11);
+ if (checksum > 9) { return parseInt(tin[8], 10) === 0; }
+ return checksum === parseInt(tin[8], 10);
+}
+
+/*
+ * ro-RO validation function
+ * (Cod Numeric Personal (CNP) or Cod de înregistrare fiscală (CIF),
+ * persons only)
+ * Verify CNP validity by calculating check (last) digit (test not found for CIF)
+ * Material not in DG TAXUD document sourced from:
+ * `https://en.wikipedia.org/wiki/National_identification_number#Romania`
+ */
+function roRoCheck(tin) {
+ if (tin.slice(0, 4) !== '9000') { // No test found for this format
+ // Extract full year using century digit if possible
+ let full_year = tin.slice(1, 3);
+ switch (tin[0]) {
+ case '1':
+ case '2':
+ full_year = `19${full_year}`;
+ break;
+ case '3':
+ case '4':
+ full_year = `18${full_year}`;
+ break;
+ case '5':
+ case '6':
+ full_year = `20${full_year}`;
+ break;
+ default:
+ }
+
+ // Check date validity
+ const date = `${full_year}/${tin.slice(3, 5)}/${tin.slice(5, 7)}`;
+ if (date.length === 8) {
+ if (!isDate(date, 'YY/MM/DD')) { return false; }
+ } else if (!isDate(date, 'YYYY/MM/DD')) { return false; }
+
+ // Calculate check digit
+ const digits = tin.split('').map(a => parseInt(a, 10));
+ const multipliers = [2, 7, 9, 1, 4, 6, 3, 5, 8, 2, 7, 9];
+ let checksum = 0;
+ for (let i = 0; i < multipliers.length; i++) {
+ checksum += digits[i] * multipliers[i];
+ }
+ if (checksum % 11 === 10) { return digits[12] === 1; }
+ return digits[12] === checksum % 11;
+ }
+ return true;
+}
+
+/*
+ * sk-SK validation function
+ * (Rodné číslo (RČ) or bezvýznamové identifikačné číslo (BIČ), persons only)
+ * Checks validity of pre-1954 birth numbers (rodné číslo) only
+ * Due to the introduction of the pseudo-random BIČ it is not possible to test
+ * post-1954 birth numbers without knowing whether they are BIČ or RČ beforehand
+ */
+function skSkCheck(tin) {
+ if (tin.length === 9) {
+ tin = tin.replace(/\W/, '');
+ if (tin.slice(6) === '000') { return false; } // Three-zero serial not assigned before 1954
+
+ // Extract full year from TIN length
+ let full_year = parseInt(tin.slice(0, 2), 10);
+ if (full_year > 53) { return false; }
+ if (full_year < 10) {
+ full_year = `190${full_year}`;
+ } else {
+ full_year = `19${full_year}`;
+ }
+
+ // Extract month from TIN and normalize
+ let month = parseInt(tin.slice(2, 4), 10);
+ if (month > 50) {
+ month -= 50;
+ }
+ if (month < 10) { month = `0${month}`; }
+
+ // Check date validity
+ const date = `${full_year}/${month}/${tin.slice(4, 6)}`;
+ if (!isDate(date, 'YYYY/MM/DD')) { return false; }
+ }
+ return true;
+}
+
+/*
+ * sl-SI validation function
+ * (Davčna številka, persons/entities)
+ * Verify TIN validity by calculating check (last) digit (variant of MOD 11)
+ */
+function slSiCheck(tin) {
+ let checksum = 11 - (algorithms.reverseMultiplyAndSum(tin.split('').slice(0, 7).map(a => parseInt(a, 10)), 8) % 11);
+ if (checksum === 10) { return parseInt(tin[7], 10) === 0; }
+ return checksum === parseInt(tin[7], 10);
+}
+
+/*
+ * sv-SE validation function
+ * (Personnummer or samordningsnummer, persons only)
+ * Checks validity of birth date and calls luhnCheck() to validate check (last) digit
+ */
+function svSeCheck(tin) {
+ // Make copy of TIN and normalize to two-digit year form
+ let tin_copy = tin.slice(0);
+ if (tin.length > 11) {
+ tin_copy = tin_copy.slice(2);
+ }
+
+ // Extract date of birth
+ let full_year = '';
+ const month = tin_copy.slice(2, 4);
+ let day = parseInt(tin_copy.slice(4, 6), 10);
+ if (tin.length > 11) {
+ full_year = tin.slice(0, 4);
+ } else {
+ full_year = tin.slice(0, 2);
+ if (tin.length === 11 && day < 60) {
+ // Extract full year from centenarian symbol
+ // Should work just fine until year 10000 or so
+ let current_year = new Date().getFullYear().toString();
+ const current_century = parseInt(current_year.slice(0, 2), 10);
+ current_year = parseInt(current_year, 10);
+ if (tin[6] === '-') {
+ if (parseInt(`${current_century}${full_year}`, 10) > current_year) {
+ full_year = `${current_century - 1}${full_year}`;
+ } else {
+ full_year = `${current_century}${full_year}`;
+ }
+ } else {
+ full_year = `${current_century - 1}${full_year}`;
+ if (current_year - parseInt(full_year, 10) < 100) { return false; }
+ }
+ }
+ }
+
+ // Normalize day and check date validity
+ if (day > 60) { day -= 60; }
+ if (day < 10) { day = `0${day}`; }
+ const date = `${full_year}/${month}/${day}`;
+ if (date.length === 8) {
+ if (!isDate(date, 'YY/MM/DD')) { return false; }
+ } else if (!isDate(date, 'YYYY/MM/DD')) { return false; }
+
+ return algorithms.luhnCheck(tin.replace(/\W/, ''));
+}
+
+// Locale lookup objects
+
+/*
+ * Tax id regex formats for various locales
+ *
+ * Where not explicitly specified in DG-TAXUD document both
+ * uppercase and lowercase letters are acceptable.
+ */
const taxIdFormat = {
+ 'bg-BG': /^\d{10}$/,
+ 'cs-CZ': /^\d{6}\/{0,1}\d{3,4}$/,
+ 'de-AT': /^\d{9}$/,
+ 'de-DE': /^[1-9]\d{10}$/,
+ 'dk-DK': /^\d{6}-{0,1}\d{4}$/,
+ 'el-CY': /^[09]\d{7}[A-Z]$/,
+ 'el-GR': /^([0-4]|[7-9])\d{8}$/,
+ 'en-GB': /^\d{10}$|^(?!GB|NK|TN|ZZ)(?![DFIQUV])[A-Z](?![DFIQUVO])[A-Z]\d{6}[ABCD ]$/i,
+ 'en-IE': /^\d{7}[A-W][A-IW]{0,1}$/i,
'en-US': /^\d{2}[- ]{0,1}\d{7}$/,
+ 'es-ES': /^(\d{0,8}|[XYZKLM]\d{7})[A-HJ-NP-TV-Z]$/i,
+ 'et-EE': /^[1-6]\d{6}(00[1-9]|0[1-9][0-9]|[1-6][0-9]{2}|70[0-9]|710)\d$/,
+ 'fi-FI': /^\d{6}[-+A]\d{3}[0-9A-FHJ-NPR-Y]$/i,
+ 'fr-BE': /^\d{11}$/,
+ 'fr-FR': /^[0-3]\d{12}$|^[0-3]\d\s\d{2}(\s\d{3}){3}$/, // Conforms both to official spec and provided example
+ 'fr-LU': /^\d{13}$/,
+ 'hr-HR': /^\d{11}$/,
+ 'hu-HU': /^8\d{9}$/,
+ 'it-IT': /^[A-Z]{6}[L-NP-V0-9]{2}[A-EHLMPRST][L-NP-V0-9]{2}[A-ILMZ][L-NP-V0-9]{3}[A-Z]$/i,
+ 'lv-LV': /^\d{6}-{0,1}\d{5}$/, // Conforms both to DG TAXUD spec and original research
+ 'mt-MT': /^\d{3,7}[APMGLHBZ]$|^([1-8])\1\d{7}$/i,
+ 'nl-NL': /^\d{9}$/,
+ 'pl-PL': /^\d{10,11}$/,
+ 'pt-PT': /^\d{9}$/,
+ 'ro-RO': /^\d{13}$/,
+ 'sk-SK': /^\d{6}\/{0,1}\d{3,4}$/,
+ 'sl-SI': /^[1-9]\d{7}$/,
+ 'sv-SE': /^(\d{6}[-+]{0,1}\d{4}|(18|19|20)\d{6}[-+]{0,1}\d{4})$/,
};
-
+// taxIdFormat locale aliases
+taxIdFormat['lb-LU'] = taxIdFormat['fr-LU'];
+taxIdFormat['lt-LT'] = taxIdFormat['et-EE'];
+taxIdFormat['nl-BE'] = taxIdFormat['fr-BE'];
// Algorithmic tax id check functions for various locales
const taxIdCheck = {
+ 'bg-BG': bgBgCheck,
+ 'cs-CZ': csCzCheck,
+ 'de-AT': deAtCheck,
+ 'de-DE': deDeCheck,
+ 'dk-DK': dkDkCheck,
+ 'el-CY': elCyCheck,
+ 'el-GR': elGrCheck,
+ 'en-IE': enIeCheck,
'en-US': enUsCheck,
+ 'es-ES': esEsCheck,
+ 'et-EE': etEeCheck,
+ 'fi-FI': fiFiCheck,
+ 'fr-BE': frBeCheck,
+ 'fr-FR': frFrCheck,
+ 'fr-LU': frLuCheck,
+ 'hr-HR': hrHrCheck,
+ 'hu-HU': huHuCheck,
+ 'it-IT': itItCheck,
+ 'lv-LV': lvLvCheck,
+ 'mt-MT': mtMtCheck,
+ 'nl-NL': nlNlCheck,
+ 'pl-PL': plPlCheck,
+ 'pt-PT': ptPtCheck,
+ 'ro-RO': roRoCheck,
+ 'sk-SK': skSkCheck,
+ 'sl-SI': slSiCheck,
+ 'sv-SE': svSeCheck,
};
+// taxIdCheck locale aliases
+taxIdCheck['lb-LU'] = taxIdCheck['fr-LU'];
+taxIdCheck['lt-LT'] = taxIdCheck['et-EE'];
+taxIdCheck['nl-BE'] = taxIdCheck['fr-BE'];
+
+// Regexes for locales where characters should be omitted before checking format
+const allsymbols = /[-\\\/!@#$%\^&\*\(\)\+\=\[\]]+/g;
+const sanitizeRegexes = {
+ 'de-AT': allsymbols,
+ 'de-DE': /[\/\\]/g,
+ 'fr-BE': allsymbols,
+};
+// sanitizeRegexes locale aliases
+sanitizeRegexes['nl-BE'] = sanitizeRegexes['fr-BE'];
/*
* Validator function
@@ -77,13 +1106,19 @@ const taxIdCheck = {
*/
export default function isTaxID(str, locale = 'en-US') {
assertString(str);
+ // Copy TIN to avoid replacement if sanitized
+ let strcopy = str.slice(0);
+
if (locale in taxIdFormat) {
- if (!taxIdFormat[locale].test(str)) {
+ if (locale in sanitizeRegexes) {
+ strcopy = strcopy.replace(sanitizeRegexes[locale], '');
+ }
+ if (!taxIdFormat[locale].test(strcopy)) {
return false;
}
if (locale in taxIdCheck) {
- return taxIdCheck[locale](str);
+ return taxIdCheck[locale](strcopy);
}
// Fallthrough; not all locales have algorithmic checks
return true;
diff --git a/src/lib/util/algorithms.js b/src/lib/util/algorithms.js
new file mode 100644
index 000000000..4ac1f2784
--- /dev/null
+++ b/src/lib/util/algorithms.js
@@ -0,0 +1,97 @@
+/**
+ * Algorithmic validation functions
+ * May be used as is or implemented in the workflow of other validators.
+ */
+
+/*
+ * ISO 7064 validation function
+ * Called with a string of numbers (incl. check digit)
+ * to validate according to ISO 7064 (MOD 11, 10).
+ */
+export function iso7064Check(str) {
+ let checkvalue = 10;
+ for (let i = 0; i < str.length - 1; i++) {
+ checkvalue = (parseInt(str[i], 10) + checkvalue) % 10 === 0 ? (10 * 2) % 11 :
+ (((parseInt(str[i], 10) + checkvalue) % 10) * 2) % 11;
+ }
+ checkvalue = checkvalue === 1 ? 0 : 11 - checkvalue;
+ return checkvalue === parseInt(str[10], 10);
+}
+
+/*
+ * Luhn (mod 10) validation function
+ * Called with a string of numbers (incl. check digit)
+ * to validate according to the Luhn algorithm.
+ */
+export function luhnCheck(str) {
+ let checksum = 0;
+ let second = false;
+ for (let i = str.length - 1; i >= 0; i--) {
+ if (second) {
+ const product = parseInt(str[i], 10) * 2;
+ if (product > 9) {
+ // sum digits of product and add to checksum
+ checksum += product.toString().split('').map(a => parseInt(a, 10)).reduce((a, b) => a + b, 0);
+ } else {
+ checksum += product;
+ }
+ } else {
+ checksum += parseInt(str[i], 10);
+ }
+ second = !second;
+ }
+ return checksum % 10 === 0;
+}
+
+/*
+ * Reverse TIN multiplication and summation helper function
+ * Called with an array of single-digit integers and a base multiplier
+ * to calculate the sum of the digits multiplied in reverse.
+ * Normally used in variations of MOD 11 algorithmic checks.
+ */
+export function reverseMultiplyAndSum(digits, base) {
+ let total = 0;
+ for (let i = 0; i < digits.length; i++) {
+ total += digits[i] * (base - i);
+ }
+ return total;
+}
+
+/*
+ * Verhoeff validation helper function
+ * Called with a string of numbers
+ * to validate according to the Verhoeff algorithm.
+ */
+export function verhoeffCheck(str) {
+ const d_table = [
+ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
+ [1, 2, 3, 4, 0, 6, 7, 8, 9, 5],
+ [2, 3, 4, 0, 1, 7, 8, 9, 5, 6],
+ [3, 4, 0, 1, 2, 8, 9, 5, 6, 7],
+ [4, 0, 1, 2, 3, 9, 5, 6, 7, 8],
+ [5, 9, 8, 7, 6, 0, 4, 3, 2, 1],
+ [6, 5, 9, 8, 7, 1, 0, 4, 3, 2],
+ [7, 6, 5, 9, 8, 2, 1, 0, 4, 3],
+ [8, 7, 6, 5, 9, 3, 2, 1, 0, 4],
+ [9, 8, 7, 6, 5, 4, 3, 2, 1, 0],
+ ];
+
+ const p_table = [
+ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
+ [1, 5, 7, 6, 2, 8, 3, 0, 9, 4],
+ [5, 8, 0, 3, 7, 9, 6, 1, 4, 2],
+ [8, 9, 1, 6, 0, 4, 3, 5, 2, 7],
+ [9, 4, 5, 3, 1, 2, 6, 8, 7, 0],
+ [4, 2, 8, 6, 5, 7, 3, 9, 0, 1],
+ [2, 7, 9, 3, 8, 0, 6, 4, 1, 5],
+ [7, 0, 4, 6, 9, 1, 3, 2, 5, 8],
+ ];
+
+ // Copy (to prevent replacement) and reverse
+ const str_copy = str.split('').reverse().join('');
+ let checksum = 0;
+ for (let i = 0; i < str_copy.length; i++) {
+ checksum = d_table[checksum][p_table[i % 8][parseInt(str_copy[i], 10)]];
+ }
+ return checksum === 0;
+}
diff --git a/test/validators.js b/test/validators.js
index dedd0366a..3bb515d4b 100644
--- a/test/validators.js
+++ b/test/validators.js
@@ -9296,10 +9296,164 @@ describe('Validators', () => {
});
});
-
+ // EU-UK valid numbers sourced from https://ec.europa.eu/taxation_customs/tin/specs/FS-TIN%20Algorithms-Public.docx or constructed by @tplessas.
it('should validate taxID', () => {
test({
validator: 'isTaxID',
+ args: ['bg-BG'],
+ valid: [
+ '7501010010',
+ '0101010012',
+ '0111010010',
+ '7521010014',
+ '7541010019'],
+ invalid: [
+ '750101001',
+ '75010100101',
+ '75-01010/01 0',
+ '7521320010',
+ '7501010019'],
+ });
+ test({
+ validator: 'isTaxID',
+ args: ['cs-CZ'],
+ valid: [
+ '530121999',
+ '530121/999',
+ '530121/9990',
+ '5301219990',
+ '1602295134',
+ '5451219994',
+ '0424175466',
+ '0532175468',
+ '7159079940'],
+ invalid: [
+ '53-0121 999',
+ '530121000',
+ '960121999',
+ '0124175466',
+ '0472301754',
+ '1975116400',
+ '7159079945'],
+ });
+ test({
+ validator: 'isTaxID',
+ args: ['de-AT'],
+ valid: [
+ '931736581',
+ '93-173/6581',
+ '93--173/6581'],
+ invalid: [
+ '999999999',
+ '93 173 6581',
+ '93-173/65811',
+ '93-173/658'],
+ });
+ test({
+ validator: 'isTaxID',
+ args: ['de-DE'],
+ valid: [
+ '26954371827',
+ '86095742719',
+ '65929970489',
+ '79608434120',
+ '659/299/7048/9'],
+ invalid: [
+ '26954371828',
+ '86095752719',
+ '8609575271',
+ '860957527190',
+ '65299970489',
+ '65999970489',
+ '6592997048-9'],
+ });
+ test({
+ validator: 'isTaxID',
+ args: ['dk-DK'],
+ valid: [
+ '010111-1113',
+ '0101110117',
+ '2110084008',
+ '2110489008',
+ '2110595002',
+ '2110197007',
+ '0101110117',
+ '0101110230'],
+ invalid: [
+ '010111/1113',
+ '010111111',
+ '01011111133',
+ '2110485008',
+ '2902034000',
+ '0101110630'],
+ });
+ test({
+ validator: 'isTaxID',
+ args: ['el-CY'],
+ valid: [
+ '00123123T',
+ '99652156X'],
+ invalid: [
+ '99652156A',
+ '00124123T',
+ '00123123',
+ '001123123T',
+ '00 12-3123/T'],
+ });
+ test({
+ validator: 'isTaxID',
+ args: ['el-GR'],
+ valid: [
+ '758426713',
+ '054100004'],
+ invalid: [
+ '054100005',
+ '05410000',
+ '0541000055',
+ '05 4100005',
+ '05-410/0005',
+ '658426713',
+ '558426713'],
+ });
+ test({
+ validator: 'isTaxID',
+ args: ['en-GB'],
+ valid: [
+ '1234567890',
+ 'AA123456A',
+ 'AA123456 '],
+ invalid: [
+ 'GB123456A',
+ '123456789',
+ '12345678901',
+ 'NK123456A',
+ 'TN123456A',
+ 'ZZ123456A',
+ 'GB123456Z',
+ 'DM123456A',
+ 'AO123456A',
+ 'GB-123456A',
+ 'GB 123456 A',
+ 'GB123456 '],
+ });
+ test({
+ validator: 'isTaxID',
+ args: ['en-IE'],
+ valid: [
+ '1234567T',
+ '1234567TW',
+ '1234577W',
+ '1234577WW',
+ '1234577IA'],
+ invalid: [
+ '1234567',
+ '1234577WWW',
+ '1234577A',
+ '1234577JA'],
+ });
+ test({
+ validator: 'isTaxID',
+ args: ['en-US'],
valid: [
'01-1234567',
'01 1234567',
@@ -9319,6 +9473,297 @@ describe('Validators', () => {
'28-1234567',
'96-1234567'],
});
+ test({
+ validator: 'isTaxID',
+ args: ['es-ES'],
+ valid: [
+ '00054237A',
+ '54237A',
+ 'X1234567L',
+ 'Z1234567R',
+ 'M2812345C',
+ 'Y2812345B'],
+ invalid: [
+ 'M2812345CR',
+ 'A2812345C',
+ '0/005 423-7A',
+ '00054237U'],
+ });
+ test({
+ validator: 'isTaxID',
+ args: ['et-EE'],
+ valid: [
+ '10001010080',
+ '46304280206',
+ '37102250382',
+ '32708101201'],
+ invalid: [
+ '46304280205',
+ '61002293333',
+ '4-6304 28/0206',
+ '4630428020',
+ '463042802066'],
+ });
+ test({
+ validator: 'isTaxID',
+ args: ['fi-FI'],
+ valid: [
+ '131052-308T',
+ '131002+308W',
+ '131019A3089'],
+ invalid: [
+ '131052308T',
+ '131052-308TT',
+ '131052S308T',
+ '13 1052-308/T',
+ '290219A1111'],
+ });
+ test({
+ validator: 'isTaxID',
+ args: ['fr-BE'],
+ valid: [
+ '00012511119'],
+ });
+ test({
+ validator: 'isTaxID',
+ args: ['fr-FR'],
+ valid: [
+ '30 23 217 600 053',
+ '3023217600053'],
+ invalid: [
+ '30 2 3 217 600 053',
+ '3 023217-600/053',
+ '3023217600052',
+ '3023217500053',
+ '30232176000534',
+ '302321760005'],
+ });
+ test({
+ validator: 'isTaxID',
+ args: ['nl-BE'],
+ valid: [
+ '00012511148',
+ '00/0125-11148',
+ '00000011115'],
+ invalid: [
+ '00 01 2511148',
+ '01022911148',
+ '00013211148',
+ '0001251114',
+ '000125111480',
+ '00012511149'],
+ });
+ test({
+ validator: 'isTaxID',
+ args: ['fr-LU'],
+ valid: [
+ '1893120105732'],
+ invalid: [
+ '189312010573',
+ '18931201057322',
+ '1893 12-01057/32',
+ '1893120105742',
+ '1893120105733'],
+ });
+ test({
+ validator: 'isTaxID',
+ args: ['lb-LU'],
+ invalid: [
+ '2016023005732'],
+ });
+ test({
+ validator: 'isTaxID',
+ args: ['hr-HR'],
+ valid: [
+ '94577403194'],
+ invalid: [
+ '94 57-7403/194',
+ '9457740319',
+ '945774031945',
+ '94577403197',
+ '94587403194'],
+ });
+ test({
+ validator: 'isTaxID',
+ args: ['hu-HU'],
+ valid: [
+ '8071592153'],
+ invalid: [
+ '80 71-592/153',
+ '80715921534',
+ '807159215',
+ '8071592152',
+ '8071582153'],
+ });
+ test({
+ validator: 'isTaxID',
+ args: ['lt-LT'],
+ valid: [
+ '33309240064'],
+ });
+ test({
+ validator: 'isTaxID',
+ args: ['it-IT'],
+ valid: [
+ 'DMLPRY77D15H501F',
+ 'AXXFAXTTD41H501D'],
+ invalid: [
+ 'DML PRY/77D15H501-F',
+ 'DMLPRY77D15H501',
+ 'DMLPRY77D15H501FF',
+ 'AAPPRY77D15H501F',
+ 'DMLAXA77D15H501F',
+ 'AXXFAX90A01Z001F',
+ 'DMLPRY77B29H501F',
+ 'AXXFAX3TD41H501E'],
+ });
+ test({
+ validator: 'isTaxID',
+ args: ['lv-LV'],
+ valid: [
+ '01011012344',
+ '32579461005',
+ '01019902341',
+ '325794-61005'],
+ invalid: [
+ '010110123444',
+ '0101101234',
+ '01001612345',
+ '290217-22343'],
+ });
+ test({
+ validator: 'isTaxID',
+ args: ['mt-MT'],
+ valid: [
+ '1234567A',
+ '882345608',
+ '34581M',
+ '199Z'],
+ invalid: [
+ '812345608',
+ '88234560',
+ '8823456088',
+ '11234567A',
+ '12/34-567 A',
+ '88 23-456/08',
+ '1234560A',
+ '0000000M',
+ '3200100G'],
+ });
+ test({
+ validator: 'isTaxID',
+ args: ['nl-NL'],
+ valid: [
+ '174559434'],
+ invalid: [
+ '17455943',
+ '1745594344',
+ '17 455-94/34'],
+ });
+ test({
+ validator: 'isTaxID',
+ args: ['pl-PL'],
+ valid: [
+ '2234567895',
+ '02070803628',
+ '02870803622',
+ '02670803626',
+ '01510813623'],
+ invalid: [
+ '020708036285',
+ '223456789',
+ '22 345-678/95',
+ '02 070-8036/28',
+ '2234567855',
+ '02223013623'],
+ });
+ test({
+ validator: 'isTaxID',
+ args: ['pt-PT'],
+ valid: [
+ '299999998',
+ '299992020'],
+ invalid: [
+ '2999999988',
+ '29999999',
+ '29 999-999/8'],
+ });
+ test({
+ validator: 'isTaxID',
+ args: ['ro-RO'],
+ valid: [
+ '8001011234563',
+ '9000123456789',
+ '1001011234560',
+ '3001011234564',
+ '5001011234568'],
+ invalid: [
+ '5001011234569',
+ '500 1011-234/568',
+ '500101123456',
+ '50010112345688',
+ '5001011504568',
+ '8000230234563',
+ '6000230234563'],
+ });
+ test({
+ validator: 'isTaxID',
+ args: ['sk-SK'],
+ valid: [
+ '530121999',
+ '536221/999',
+ '031121999',
+ '520229999',
+ '1234567890'],
+ invalid: [
+ '53012199999',
+ '990101999',
+ '530121000',
+ '53012199',
+ '53-0121 999',
+ '535229999'],
+ });
+ test({
+ validator: 'isTaxID',
+ args: ['sl-SI'],
+ valid: [
+ '15012557',
+ '15012590'],
+ invalid: [
+ '150125577',
+ '1501255',
+ '15 01-255/7'],
+ });
+ test({
+ validator: 'isTaxID',
+ args: ['sv-SE'],
+ valid: [
+ '640823-3234',
+ '640883-3231',
+ '6408833231',
+ '19640823-3233',
+ '196408233233',
+ '19640883-3230',
+ '200228+5266',
+ '20180101-5581'],
+ invalid: [
+ '640823+3234',
+ '160230-3231',
+ '160260-3231',
+ '160260-323',
+ '160260323',
+ '640823+323',
+ '640823323',
+ '640823+32344',
+ '64082332344',
+ '19640823-32333',
+ '1964082332333'],
+ });
+ test({
+ validator: 'isTaxID',
+ valid: [
+ '01-1234567'],
+ });
test({
validator: 'isTaxID',
args: ['is-NOT'],