Repository: validatorjs/validator.js
Pull request #1595 (2021-02-07 23:14:42)

feat: IR passport and identityCard, respect .gitignore files
- `IR` for **isPassportNumber**
- `IR` for **isIdentityCard**
- respect **.gitignore**

## Checklist

- [x] PR contains only changes related; no stray files, etc.
- [x] README updated (where applicable)
- [x] Tests written (where applicable)


codecov[bot]: # [Codecov](https://codecov.io/gh/validatorjs/validator.js/pull/1595?src=pr&el=h1) Report
> Merging [#1595](https://codecov.io/gh/validatorjs/validator.js/pull/1595?src=pr&el=desc) (0238023) into [master](https://codecov.io/gh/validatorjs/validator.js/commit/1b85829bd6bdf660b2989cdd094bcd80f7d3b975?el=desc) (1b85829) will **not change** coverage.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/validatorjs/validator.js/pull/1595/graphs/tree.svg?width=650&height=150&src=pr&token=YzR3kbN1m2)](https://codecov.io/gh/validatorjs/validator.js/pull/1595?src=pr&el=tree)

```diff
@@            Coverage Diff            @@
##            master     #1595   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files          100       101    +1     
  Lines         1796      1819   +23     
=========================================
+ Hits          1796      1819   +23     
```


| [Impacted Files](https://codecov.io/gh/validatorjs/validator.js/pull/1595?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/lib/isPassportNumber.js](https://codecov.io/gh/validatorjs/validator.js/pull/1595/diff?src=pr&el=tree#diff-c3JjL2xpYi9pc1Bhc3Nwb3J0TnVtYmVyLmpz) | `100.00% <ø> (ø)` | |
| [src/index.js](https://codecov.io/gh/validatorjs/validator.js/pull/1595/diff?src=pr&el=tree#diff-c3JjL2luZGV4Lmpz) | `100.00% <100.00%> (ø)` | |
| [src/lib/isIdentityCard.js](https://codecov.io/gh/validatorjs/validator.js/pull/1595/diff?src=pr&el=tree#diff-c3JjL2xpYi9pc0lkZW50aXR5Q2FyZC5qcw==) | `100.00% <100.00%> (ø)` | |
| [src/lib/isLuhn.js](https://codecov.io/gh/validatorjs/validator.js/pull/1595/diff?src=pr&el=tree#diff-c3JjL2xpYi9pc0x1aG4uanM=) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/validatorjs/validator.js/pull/1595?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/validatorjs/validator.js/pull/1595?src=pr&el=footer). Last update [1b85829...d377f8c](https://codecov.io/gh/validatorjs/validator.js/pull/1595?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

mhf-ir: @tux-tn As i Said previously isLuhn one of the most important validation that you can see in [npm many version](https://www.npmjs.com/search?q=keywords:luhn) (about 50 package) of that is implemented.

Forget about isCreditCard, it's Independent validation for Luhn algorithm that developers need it already. Dependency of isCreditCard is out of this PR scope.

isCreditCard might be depend on Luhn or not, but could be different issue and PR.

mhf-ir: After all I can remove isLuhn and send another issue and PR. Is that OK? @tux-tn 
tux-tn: > After all I can remove isLuhn and send another issue and PR. Is that OK? @tux-tn

it's a good compromise, it will allow us to land directly IR Passport/IdentityCard validator and open discussion concerning the new `isLuhn` validator (which may take some time)
tux-tn: @mhf-ir feel free to open a new PR concerning `isLuhn` in order to continue the discussion 

Patch:
diff --git a/README.md b/README.md
index d86ed5d..4b2ed2d 100644
--- a/README.md
+++ b/README.md
@@ -115,12 +115,12 @@ Validator                               | Description
 **isHexColor(str)**                     | check if the string is a hexadecimal color.
 **isHSL(str)**                          | check if the string is an HSL (hue, saturation, lightness, optional alpha) color based on [CSS Colors Level 4 specification](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value).<br/><br/>Comma-separated format supported. Space-separated format supported with the exception of a few edge cases (ex: `hsl(200grad+.1%62%/1)`).
 **isIBAN(str)**                         | check if a string is a IBAN (International Bank Account Number).
-**isIdentityCard(str [, locale])**      | check if the string is a valid identity card code.<br/><br/>`locale` is one of `['ES', 'IN', 'IT', 'NO', 'zh-TW', 'he-IL', 'ar-LY', 'ar-TN', 'zh-CN']` OR `'any'`. If 'any' is used, function will check if any of the locals match.<br/><br/>Defaults to 'any'.
+**isIdentityCard(str [, locale])**      | check if the string is a valid identity card code.<br/><br/>`locale` is one of `['ES', 'IN', 'IT', 'IR', 'NO', 'zh-TW', 'he-IL', 'ar-LY', 'ar-TN', 'zh-CN']` OR `'any'`. If 'any' is used, function will check if any of the locals match.<br/><br/>Defaults to 'any'.
 **isIMEI(str [, options]))**                         | check if the string is a valid IMEI number. Imei should be of format `###############` or `##-######-######-#`.<br/><br/>`options` is an object which can contain the keys `allow_hyphens`. Defaults to first format . If allow_hyphens is set to true, the validator will validate the second format.
 **isIn(str, values)**                   | check if the string is in a array of allowed values.
 **isInt(str [, options])**              | check if the string is an integer.<br/><br/>`options` is an object which can contain the keys `min` and/or `max` to check the integer is within boundaries (e.g. `{ min: 10, max: 99 }`). `options` can also contain the key `allow_leading_zeroes`, which when set to false will disallow integer values with leading zeroes (e.g. `{ allow_leading_zeroes: false }`). Finally, `options` can contain the keys `gt` and/or `lt` which will enforce integers being greater than or less than, respectively, the value provided (e.g. `{gt: 1, lt: 4}` for a number between 1 and 4).
 **isIP(str [, version])**               | check if the string is an IP (version 4 or 6).
-**isIPRange(str)**                      | check if the string is an IP Range(version 4 only).
+**isIPRange(str [, version])**          | check if the string is an IP Range (version 4 or 6).
 **isISBN(str [, version])**             | check if the string is an ISBN (version 10 or 13).
 **isISIN(str)**                         | check if the string is an [ISIN][ISIN] (stock/security identifier).
 **isISO8601(str)**                      | check if the string is a valid [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) date. <br/>`options` is an object which defaults to `{ strict: false, strictSeparator: false }`. If `strict` is true, date strings with invalid dates like `2009-02-29` will be invalid. If `strictSeparator` is true, date strings with date and time separated by anything other than a T will be invalid.
@@ -144,7 +144,7 @@ Validator                               | Description
 **isMultibyte(str)**                    | check if the string contains one or more multibyte chars.
 **isNumeric(str [, options])**                      | check if the string contains only numbers.<br/><br/>`options` is an object which defaults to `{no_symbols: false}` it also has locale as an option. If `no_symbols` is true, the validator will reject numeric strings that feature a symbol (e.g. `+`, `-`, or `.`).<br/><br/>`locale` determine the decimal separator and is one of `['ar', 'ar-AE', 'ar-BH', 'ar-DZ', 'ar-EG', 'ar-IQ', 'ar-JO', 'ar-KW', 'ar-LB', 'ar-LY', 'ar-MA', 'ar-QA', 'ar-QM', 'ar-SA', 'ar-SD', 'ar-SY', 'ar-TN', 'ar-YE', 'bg-BG', 'cs-CZ', 'da-DK', 'de-DE', 'en-AU', 'en-GB', 'en-HK', 'en-IN', 'en-NZ', 'en-US', 'en-ZA', 'en-ZM', 'es-ES', 'fr-FR', 'fr-CA', 'hu-HU', 'it-IT', 'nb-NO', 'nl-NL', 'nn-NO', 'pl-PL', 'pt-BR', 'pt-PT', 'ru-RU', 'sl-SI', 'sr-RS', 'sr-RS@latin', 'sv-SE', 'tr-TR', 'uk-UA']`.
 **isOctal(str)**                        | check if the string is a valid octal number.
-**isPassportNumber(str, countryCode)**    | check if the string is a valid passport number.<br/><br/>(countryCode is one of `[ 'AM', 'AR', 'AT', 'AU', 'BE', 'BG', 'BY', 'BR', 'CA', 'CH', 'CN', 'CY', 'CZ', 'DE', 'DK', 'DZ', 'EE', 'ES', 'FI', 'FR', 'GB', 'GR', 'HR', 'HU', 'IE' 'IN', 'IS', 'IT', 'JP', 'KR', 'LT', 'LU', 'LV', 'LY', 'MT', 'MY', 'NL', 'PO', 'PT', 'RO', 'RU', 'SE', 'SL', 'SK', 'TR', 'UA', 'US' ]`.
+**isPassportNumber(str, countryCode)**    | check if the string is a valid passport number.<br/><br/>(countryCode is one of `[ 'AM', 'AR', 'AT', 'AU', 'BE', 'BG', 'BY', 'BR', 'CA', 'CH', 'CN', 'CY', 'CZ', 'DE', 'DK', 'DZ', 'EE', 'ES', 'FI', 'FR', 'GB', 'GR', 'HR', 'HU', 'IE' 'IN', 'IR', 'IS', 'IT', 'JP', 'KR', 'LT', 'LU', 'LV', 'LY', 'MT', 'MY', 'NL', 'PO', 'PT', 'RO', 'RU', 'SE', 'SL', 'SK', 'TR', 'UA', 'US' ]`.
 **isPort(str)**                         | check if the string is a valid port number.
 **isPostalCode(str, locale)**           | check if the string is a postal code,<br/><br/>(locale is one of `[ 'AD', 'AT', 'AU', 'AZ', 'BE', 'BG', 'BR', 'BY', 'CA', 'CH', 'CN', 'CZ', 'DE', 'DK', 'DO', 'DZ', 'EE', 'ES', 'FI', 'FR', 'GB', 'GR', 'HR', 'HT', 'HU', 'ID', 'IE' 'IL', 'IN', 'IR', 'IS', 'IT', 'JP', 'KE', 'LI', 'LT', 'LU', 'LV', 'MT', 'MX', 'MY', 'NL', 'NO', 'NP', 'NZ', 'PL', 'PR', 'PT', 'RO', 'RU', 'SA', 'SE', 'SG', 'SI', 'TH', 'TN', 'TW', 'UA', 'US', 'ZA', 'ZM' ]` OR 'any'. If 'any' is used, function will check if any of the locals match. Locale list is `validator.isPostalCodeLocales`.).
 **isRFC3339(str)**                      | check if the string is a valid [RFC 3339](https://tools.ietf.org/html/rfc3339) date.
diff --git a/src/lib/isIPRange.js b/src/lib/isIPRange.js
index ffb4e1a..5bb6916 100644
--- a/src/lib/isIPRange.js
+++ b/src/lib/isIPRange.js
@@ -1,9 +1,11 @@
 import assertString from './util/assertString';
 import isIP from './isIP';
 
-const subnetMaybe = /^\d{1,2}$/;
+const subnetMaybe = /^\d{1,3}$/;
+const v4Subnet = 32;
+const v6Subnet = 128;
 
-export default function isIPRange(str) {
+export default function isIPRange(str, version = '') {
   assertString(str);
   const parts = str.split('/');
 
@@ -21,5 +23,25 @@ export default function isIPRange(str) {
     return false;
   }
 
-  return isIP(parts[0], 4) && parts[1] <= 32 && parts[1] >= 0;
+  const isValidIP = isIP(parts[0], version);
+  if (!isValidIP) {
+    return false;
+  }
+
+  // Define valid subnet according to IP's version
+  let expectedSubnet = null;
+  switch (String(version)) {
+    case '4':
+      expectedSubnet = v4Subnet;
+      break;
+
+    case '6':
+      expectedSubnet = v6Subnet;
+      break;
+
+    default:
+      expectedSubnet = isIP(parts[0], '6') ? v6Subnet : v4Subnet;
+  }
+
+  return parts[1] <= expectedSubnet && parts[1] >= 0;
 }
diff --git a/src/lib/isIdentityCard.js b/src/lib/isIdentityCard.js
index c24054b..872ff19 100644
--- a/src/lib/isIdentityCard.js
+++ b/src/lib/isIdentityCard.js
@@ -75,6 +75,25 @@ const validators = {
 
     return c === 0;
   },
+  IR: (str) => {
+    if (!str.match(/^\d{10}$/)) return false;
+    str = (`0000${str}`).substr(str.length - 6);
+
+    if (parseInt(str.substr(3, 6), 10) === 0) return false;
+
+    const lastNumber = parseInt(str.substr(9, 1), 10);
+    let sum = 0;
+
+    for (let i = 0; i < 9; i++) {
+      sum += parseInt(str.substr(i, 1), 10) * (10 - i);
+    }
+
+    sum %= 11;
+
+    return (
+      (sum < 2 && lastNumber === sum) || (sum >= 2 && lastNumber === 11 - sum)
+    );
+  },
   IT: function IT(str) {
     if (str.length !== 9) return false;
     if (str === 'CA00000AA') return false; // https://it.wikipedia.org/wiki/Carta_d%27identit%C3%A0_elettronica_italiana
diff --git a/src/lib/isMobilePhone.js b/src/lib/isMobilePhone.js
index 207dae7..ed1f6d2 100644
--- a/src/lib/isMobilePhone.js
+++ b/src/lib/isMobilePhone.js
@@ -46,7 +46,7 @@ const phones = {
   'en-PK': /^((\+92)|(0092))-{0,1}\d{3}-{0,1}\d{7}$|^\d{11}$|^\d{4}-\d{7}$/,
   'en-PH': /^(09|\+639)\d{9}$/,
   'en-RW': /^(\+?250|0)?[7]\d{8}$/,
-  'en-SG': /^(\+65)?[689]\d{7}$/,
+  'en-SG': /^(\+65)?[3689]\d{7}$/,
   'en-SL': /^(?:0|94|\+94)?(7(0|1|2|5|6|7|8)( |-)?\d)\d{6}$/,
   'en-TZ': /^(\+?255|0)?[67]\d{8}$/,
   'en-UG': /^(\+?256|0)?[7]\d{8}$/,
diff --git a/src/lib/isPassportNumber.js b/src/lib/isPassportNumber.js
index 3572abd..ceda6ff 100644
--- a/src/lib/isPassportNumber.js
+++ b/src/lib/isPassportNumber.js
@@ -33,6 +33,7 @@ const passportRegexByCountryCode = {
   HU: /^[A-Z]{2}(\d{6}|\d{7})$/, // HUNGARY
   IE: /^[A-Z0-9]{2}\d{7}$/, // IRELAND
   IN: /^[A-Z]{1}-?\d{7}$/, // INDIA
+  IR: /^[A-Z]\d{8}$/, // IRAN
   IS: /^(A)\d{7}$/, // ICELAND
   IT: /^[A-Z0-9]{2}\d{7}$/, // ITALY
   JP: /^[A-Z]{2}\d{7}$/, // JAPAN


Test patch:
diff --git a/test/validators.js b/test/validators.js
index 4a4180a..171d1a8 100644
--- a/test/validators.js
+++ b/test/validators.js
@@ -872,18 +872,88 @@ describe('Validators', () => {
         '127.0.0.1/24',
         '0.0.0.0/0',
         '255.255.255.0/32',
+        '::/0',
+        '::/128',
+        '2001::/128',
+        '2001:800::/128',
+        '::ffff:127.0.0.1/128',
       ],
       invalid: [
+        'abc',
         '127.200.230.1/35',
         '127.200.230.1/-1',
         '1.1.1.1/011',
-        '::1/64',
         '1.1.1/24.1',
         '1.1.1.1/01',
         '1.1.1.1/1.1',
         '1.1.1.1/1.',
         '1.1.1.1/1/1',
         '1.1.1.1',
+        '::1',
+        '::1/164',
+        '2001::/240',
+        '2001::/-1',
+        '2001::/001',
+        '2001::/24.1',
+        '2001:db8:0000:1:1:1:1:1',
+        '::ffff:127.0.0.1',
+      ],
+    });
+    test({
+      validator: 'isIPRange',
+      args: [4],
+      valid: [
+        '127.0.0.1/1',
+        '0.0.0.0/1',
+        '255.255.255.255/1',
+        '1.2.3.4/1',
+        '255.0.0.1/1',
+        '0.0.1.1/1',
+      ],
+      invalid: [
+        'abc',
+        '::1',
+        '2001:db8:0000:1:1:1:1:1',
+        '::ffff:127.0.0.1',
+        '137.132.10.01',
+        '0.256.0.256',
+        '255.256.255.256',
+      ],
+    });
+    test({
+      validator: 'isIPRange',
+      args: [6],
+      valid: [
+        '::1/1',
+        '2001:db8:0000:1:1:1:1:1/1',
+        '::ffff:127.0.0.1/1',
+      ],
+      invalid: [
+        'abc',
+        '127.0.0.1',
+        '0.0.0.0',
+        '255.255.255.255',
+        '1.2.3.4',
+        '::ffff:287.0.0.1',
+        '::ffff:287.0.0.1/254',
+        '%',
+        'fe80::1234%',
+        'fe80::1234%1%3%4',
+        'fe80%fe80%',
+      ],
+    });
+    test({
+      validator: 'isIPRange',
+      args: [10],
+      valid: [],
+      invalid: [
+        'abc',
+        '127.0.0.1/1',
+        '0.0.0.0/1',
+        '255.255.255.255/1',
+        '1.2.3.4/1',
+        '::1/1',
+        '2001:db8:0000:1:1:1:1:1/1',
       ],
     });
   });
@@ -2553,6 +2623,21 @@ describe('Validators', () => {
       ],
     });
 
+    test({
+      validator: 'isPassportNumber',
+      args: ['IR'],
+      valid: [
+        'J97634522',
+        'A01234567',
+        'Z11977831',
+      ],
+      invalid: [
+        'A0123456',
+        'A0123456Z',
+        '012345678',
+      ],
+    });
+
     test({
       validator: 'isPassportNumber',
       args: ['IS'],
@@ -4475,6 +4560,31 @@ describe('Validators', () => {
           'X1234567L',
         ],
       },
+      {
+        locale: 'IR',
+        valid: [
+          '0499370899',
+          '0790419904',
+          '0084575948',
+          '0963695398',
+          '0684159414',
+          '0067749828',
+          '0650451252',
+          '1583250689',
+          '4032152314',
+          '0076229645',
+          '4271467685',
+          '0200203241',
+        ],
+        invalid: [
+          '1260293040',
+          '0000000001',
+          '1999999999',
+          '9999999991',
+          'AAAAAAAAAA',
+          '0684159415',
+        ],
+      },
       {
         locale: 'IT',
         valid: [
@@ -6263,6 +6373,7 @@ describe('Validators', () => {
       {
         locale: 'en-SG',
         valid: [
+          '32891278',
           '87654321',
           '98765432',
           '+6587654321',
@@ -6270,6 +6381,7 @@ describe('Validators', () => {
           '+6565241234',
         ],
         invalid: [
+          '332891231',
           '987654321',
           '876543219',
           '8765432',