Repository: validatorjs/validator.js
Pull request #2526 (2025-02-07 15:43:11)

fix(isPassportNumber): improve `CA` locale
# Update isPassportNumber to cover both new and old Canadian passport number formats

- Regex now covers both of the following patterns:
  -  Old pattern of: 2 letters, followed by 6 digits
  - New pattern of: 1 letter, 6 digits, 2 letters

- Modify existing CA isPassportNumber regular expression to cover new format and old format.
- Update test cases to cover new format, and add additional invalid passport number cases

Old format: `/^[A-Z]{2}\d{6}$`
New format: `^[A-Z]\d{6}[A-Z]{2}$/`
Combined into a single regular expression: `/^[A-Z]{2}\d{6}$|^[A-Z]\d{6}[A-Z]{2}$/`

### Sources
-  https://www.canada.ca/en/immigration-refugees-citizenship/services/canadian-passports/new-passport-features.html
- https://en.wikipedia.org/wiki/Canadian_passport#Data_page
- https://www.canada.ca/en/immigration-refugees-citizenship/news/2023/05/canada-unveils-new-passport-design-with-state-of-the-art-security-features.html
- https://techcommunity.microsoft.com/discussions/azurepurview/feature-request-dlp-add-new-passport-numbering-format-for-canada/4214263

closes: https://github.com/validatorjs/validator.js/issues/2525 

## Checklist
- [x] PR contains only changes related; no stray files, etc.
- [x] README updated (where applicable)
- [x] Tests written (where applicable)
- [x] References provided in PR (where applicable)



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 581908f..236fe7c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,57 @@
+# 13.15.0
+
+### New Features / Validators
+
+- [#2399](https://github.com/validatorjs/validator.js/pull/2399) `isISO31661Numeric` @RobinvanderVliet
+- [#2294](https://github.com/validatorjs/validator.js/pull/2294) `isULID` @arafatkn
+- [#2215](https://github.com/validatorjs/validator.js/pull/2215) `isISO15924` @xDivisionByZerox
+
+### Fixes, New Locales and Enhancements
+
+- `isMobilePhone`
+  - [#2395](https://github.com/validatorjs/validator.js/pull/2395) add `es-GT` locale @ignaciosuarezquilis
+  - [#1971](https://github.com/validatorjs/validator.js/pull/1971) improve `en-GB` locale @ihmpavel
+  - [#2359](https://github.com/validatorjs/validator.js/pull/2359) improve `uk-UA` locale @arttiger
+  - [#2350](https://github.com/validatorjs/validator.js/pull/2350) improve `ky-KG` locale @sadraliev
+  - [#2482](https://github.com/validatorjs/validator.js/pull/2482) improve `en-ZM` locale @sonikishan
+  - [#2362](https://github.com/validatorjs/validator.js/pull/2362) improve `en-GH` locale @NanaAb-116
+  - [#2500](https://github.com/validatorjs/validator.js/pull/2500) add `mk-MK` locale @eshward95 
+  - [#2534](https://github.com/validatorjs/validator.js/pull/2534) improve `sq-AL` locale @nichoola
+- [#2406](https://github.com/validatorjs/validator.js/pull/2406) `isBtcAddress` support all address formats and testnets @madoke
+- [#2339](https://github.com/validatorjs/validator.js/pull/2339) `isIBAN` improve `VG` regex @ST-DDT
+- [#2332](https://github.com/validatorjs/validator.js/pull/2332) `isISO4217` update currency codes @cbodtorf
+- [#2291](https://github.com/validatorjs/validator.js/pull/2291) `isIdentityCard` add `PK` locale @Daniyal-Qureshi
+- [#2414](https://github.com/validatorjs/validator.js/pull/2414) `isEmail` fix blacklist_chars @keshavlingala
+- [#2416](https://github.com/validatorjs/validator.js/pull/2416) `isInt`/`isFloat` handle undefined and null values @Daniyal-Qureshi
+- [#2415](https://github.com/validatorjs/validator.js/pull/2415) `isPostalCode` add `CO` locale @jorgevrgs
+- [#2404](https://github.com/validatorjs/validator.js/pull/2404) `isPassportNumber` export `passportNumberLocales` @derekparnell
+- [#2029](https://github.com/validatorjs/validator.js/pull/2029) `isRgbColor` add `allowSpaces` option @a-h-i
+- [#2421](https://github.com/validatorjs/validator.js/pull/2421) `isUUID` require valid variant field and require RFC9562 UUID in version `all` @broofa
+- [#2439](https://github.com/validatorjs/validator.js/pull/2439) `isURL` add `max_allowed_length` option @pinkiesky
+- [#2437](https://github.com/validatorjs/validator.js/pull/2437) `isEmail` reject starting with double quotes @code0emperor
+- [#2333](https://github.com/validatorjs/validator.js/pull/2333) `isLicensePlate` add `en-SG` locale @Sabarinathan07
+- [#2441](https://github.com/validatorjs/validator.js/pull/2441) `normalizeEmail` add `yandex_convert_yandexru` option @AayushGH
+- [#2443](https://github.com/validatorjs/validator.js/pull/2443) `isDate` return false instead of Error in certain cases @pano9000
+- [#2474](https://github.com/validatorjs/validator.js/pull/2474) `isLength` add `discreteLengths` option @Suven-p
+- [#2481](https://github.com/validatorjs/validator.js/pull/2481) `isDate` disallow mismatching length in `strictMode` @sonikishan
+- [#2492](https://github.com/validatorjs/validator.js/pull/2492) `isISO6346` set check digit to 0 if remainder is 10 @joelcuy
+- [#2493](https://github.com/validatorjs/validator.js/pull/2493) `isPostalCode` improve `BR` locale @ticmaisdev
+- [#2494](https://github.com/validatorjs/validator.js/pull/2494) `isEmail` allow regexp in `host_whitelist` and `host_blacklist` @weikangchia
+- [#2518](https://github.com/validatorjs/validator.js/pull/2518) `isIBAN` improve `IE`/`PS` regex @Tarasz57
+- **Doc fixes and others:**
+  - [#2402](https://github.com/validatorjs/validator.js/pull/2402) @BibhushanKarki
+  - [#2394](https://github.com/validatorjs/validator.js/pull/2394) @RobinvanderVliet
+  - [#1732](https://github.com/validatorjs/validator.js/pull/1732) @alguerocode
+  - [#2413](https://github.com/validatorjs/validator.js/pull/2413) @rubiin
+  - [#2408](https://github.com/validatorjs/validator.js/pull/2408) @profnandaa
+  - [#2411](https://github.com/validatorjs/validator.js/pull/2411) @rubiin
+  - [#2325](https://github.com/validatorjs/validator.js/pull/2325) @ovarn
+  - [#2418](https://github.com/validatorjs/validator.js/pull/2418) @ihmpavel
+  - [#2323](https://github.com/validatorjs/validator.js/pull/2323) @ovarn
+  - [#2423](https://github.com/validatorjs/validator.js/pull/2423) @rubiin
+  - [#2409](https://github.com/validatorjs/validator.js/pull/2409) @profnandaa
+  - [#2442](https://github.com/validatorjs/validator.js/pull/2442) @pano9000
+
 # 13.12.0
 
 ### New Features / Validators
diff --git a/README.md b/README.md
index a3ad3bd..209b6cf 100644
--- a/README.md
+++ b/README.md
@@ -123,13 +123,14 @@ Validator                               | Description
 **isIMEI(str [, options]))**                         | check if the string is a valid [IMEI number][IMEI]. 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 an 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).
+**isIP(str [, options])**               | check if the string is an IP address (version 4 or 6).<br/><br/>`options` is an object that defaults to `{ version: '' }`.<br/><br/>**Options:**<br/>`version`: defines which IP version to compare to. Accepted values: `4`, `6`, `'4'`, `'6'`.
 **isIPRange(str [, version])**          | check if the string is an IP Range (version 4 or 6).
 **isISBN(str [, options])**             | check if the string is an [ISBN][ISBN].<br/><br/>`options` is an object that has no default.<br/>**Options:**<br/>`version`: ISBN version to compare to. Accepted values are '10' and '13'. If none provided, both will be tested.
 **isISIN(str)**                         | check if the string is an [ISIN][ISIN] (stock/security identifier).
 **isISO6346(str)**                      | check if the string is a valid [ISO 6346](https://en.wikipedia.org/wiki/ISO_6346) shipping container identification.
 **isISO6391(str)**                      | check if the string is a valid [ISO 639-1][ISO 639-1] language code.
 **isISO8601(str [, options])**          | check if the string is a valid [ISO 8601][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.
+**isISO15924(str)**               | check if the string is a valid [ISO 15924][ISO 15924] officially assigned script code.
 **isISO31661Alpha2(str)**               | check if the string is a valid [ISO 3166-1 alpha-2][ISO 3166-1 alpha-2] officially assigned country code.
 **isISO31661Alpha3(str)**               | check if the string is a valid [ISO 3166-1 alpha-3][ISO 3166-1 alpha-3] officially assigned country code.
 **isISO31661Numeric(str)**              | check if the string is a valid [ISO 3166-1 numeric][ISO 3166-1 numeric] officially assigned country code.
@@ -252,6 +253,7 @@ This project is licensed under the [MIT](LICENSE). See the [LICENSE](LICENSE) fi
 [ISIN]: https://en.wikipedia.org/wiki/International_Securities_Identification_Number
 [ISO 639-1]: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
 [ISO 8601]: https://en.wikipedia.org/wiki/ISO_8601
+[ISO 15924]: https://en.wikipedia.org/wiki/ISO_15924
 [ISO 3166-1 alpha-2]: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
 [ISO 3166-1 alpha-3]: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-3
 [ISO 3166-1 numeric]: https://en.wikipedia.org/wiki/ISO_3166-1_numeric
diff --git a/package.json b/package.json
index 854657f..49ac0f8 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "validator",
   "description": "String validation and sanitization",
-  "version": "13.12.0",
+  "version": "13.15.0",
   "sideEffects": false,
   "homepage": "https://github.com/validatorjs/validator.js",
   "files": [
diff --git a/src/index.js b/src/index.js
index 1bc65a8..c476745 100644
--- a/src/index.js
+++ b/src/index.js
@@ -94,6 +94,7 @@ import { isISO6346, isFreightContainerID } from './lib/isISO6346';
 import isISO6391 from './lib/isISO6391';
 import isISO8601 from './lib/isISO8601';
 import isRFC3339 from './lib/isRFC3339';
+import isISO15924 from './lib/isISO15924';
 import isISO31661Alpha2 from './lib/isISO31661Alpha2';
 import isISO31661Alpha3 from './lib/isISO31661Alpha3';
 import isISO31661Numeric from './lib/isISO31661Numeric';
@@ -129,7 +130,7 @@ import isStrongPassword from './lib/isStrongPassword';
 
 import isVAT from './lib/isVAT';
 
-const version = '13.12.0';
+const version = '13.15.0';
 
 const validator = {
   version,
@@ -211,6 +212,7 @@ const validator = {
   isFreightContainerID,
   isISO6391,
   isISO8601,
+  isISO15924,
   isRFC3339,
   isISO31661Alpha2,
   isISO31661Alpha3,
diff --git a/src/lib/isIP.js b/src/lib/isIP.js
index 40ca19a..dca52d2 100644
--- a/src/lib/isIP.js
+++ b/src/lib/isIP.js
@@ -44,17 +44,24 @@ const IPv6AddressRegExp = new RegExp('^(' +
   `(?::((?::${IPv6SegmentFormat}){0,5}:${IPv4AddressFormat}|(?::${IPv6SegmentFormat}){1,7}|:))` +
   ')(%[0-9a-zA-Z-.:]{1,})?$');
 
-export default function isIP(str, version = '') {
-  assertString(str);
-  version = String(version);
+export default function isIP(ipAddress, options = {}) {
+  assertString(ipAddress);
+
+  // accessing 'arguments' for backwards compatibility: isIP(ipAddress [, version])
+  // eslint-disable-next-line prefer-rest-params
+  const version = (typeof options === 'object' ? options.version : arguments[1]) || '';
+
   if (!version) {
-    return isIP(str, 4) || isIP(str, 6);
+    return isIP(ipAddress, { version: 4 }) || isIP(ipAddress, { version: 6 });
   }
-  if (version === '4') {
-    return IPv4AddressRegExp.test(str);
+
+  if (version.toString() === '4') {
+    return IPv4AddressRegExp.test(ipAddress);
   }
-  if (version === '6') {
-    return IPv6AddressRegExp.test(str);
+
+  if (version.toString() === '6') {
+    return IPv6AddressRegExp.test(ipAddress);
   }
+
   return false;
 }
diff --git a/src/lib/isISO15924.js b/src/lib/isISO15924.js
new file mode 100644
index 0000000..30f9e4a
--- /dev/null
+++ b/src/lib/isISO15924.js
@@ -0,0 +1,37 @@
+import assertString from './util/assertString';
+
+// from https://www.unicode.org/iso15924/iso15924-codes.html
+const validISO15924Codes = new Set([
+  'Adlm', 'Afak', 'Aghb', 'Ahom', 'Arab', 'Aran', 'Armi', 'Armn', 'Avst',
+  'Bali', 'Bamu', 'Bass', 'Batk', 'Beng', 'Bhks', 'Blis', 'Bopo', 'Brah', 'Brai', 'Bugi', 'Buhd',
+  'Cakm', 'Cans', 'Cari', 'Cham', 'Cher', 'Chis', 'Chrs', 'Cirt', 'Copt', 'Cpmn', 'Cprt', 'Cyrl', 'Cyrs',
+  'Deva', 'Diak', 'Dogr', 'Dsrt', 'Dupl',
+  'Egyd', 'Egyh', 'Egyp', 'Elba', 'Elym', 'Ethi',
+  'Gara', 'Geok', 'Geor', 'Glag', 'Gong', 'Gonm', 'Goth', 'Gran', 'Grek', 'Gujr', 'Gukh', 'Guru',
+  'Hanb', 'Hang', 'Hani', 'Hano', 'Hans', 'Hant', 'Hatr', 'Hebr', 'Hira', 'Hluw', 'Hmng', 'Hmnp', 'Hrkt', 'Hung',
+  'Inds', 'Ital',
+  'Jamo', 'Java', 'Jpan', 'Jurc',
+  'Kali', 'Kana', 'Kawi', 'Khar', 'Khmr', 'Khoj', 'Kitl', 'Kits', 'Knda', 'Kore', 'Kpel', 'Krai', 'Kthi',
+  'Lana', 'Laoo', 'Latf', 'Latg', 'Latn', 'Leke', 'Lepc', 'Limb', 'Lina', 'Linb', 'Lisu', 'Loma', 'Lyci', 'Lydi',
+  'Mahj', 'Maka', 'Mand', 'Mani', 'Marc', 'Maya', 'Medf', 'Mend', 'Merc', 'Mero', 'Mlym', 'Modi', 'Mong', 'Moon', 'Mroo', 'Mtei', 'Mult', 'Mymr',
+  'Nagm', 'Nand', 'Narb', 'Nbat', 'Newa', 'Nkdb', 'Nkgb', 'Nkoo', 'Nshu',
+  'Ogam', 'Olck', 'Onao', 'Orkh', 'Orya', 'Osge', 'Osma', 'Ougr',
+  'Palm', 'Pauc', 'Pcun', 'Pelm', 'Perm', 'Phag', 'Phli', 'Phlp', 'Phlv', 'Phnx', 'Plrd', 'Piqd', 'Prti', 'Psin',
+  'Qaaa', 'Qaab', 'Qaac', 'Qaad', 'Qaae', 'Qaaf', 'Qaag', 'Qaah', 'Qaai', 'Qaaj', 'Qaak', 'Qaal', 'Qaam', 'Qaan', 'Qaao', 'Qaap', 'Qaaq', 'Qaar', 'Qaas', 'Qaat', 'Qaau', 'Qaav', 'Qaaw', 'Qaax', 'Qaay', 'Qaaz', 'Qaba', 'Qabb', 'Qabc', 'Qabd', 'Qabe', 'Qabf', 'Qabg', 'Qabh', 'Qabi', 'Qabj', 'Qabk', 'Qabl', 'Qabm', 'Qabn', 'Qabo', 'Qabp', 'Qabq', 'Qabr', 'Qabs', 'Qabt', 'Qabu', 'Qabv', 'Qabw', 'Qabx',
+  'Ranj', 'Rjng', 'Rohg', 'Roro', 'Runr',
+  'Samr', 'Sara', 'Sarb', 'Saur', 'Sgnw', 'Shaw', 'Shrd', 'Shui', 'Sidd', 'Sidt', 'Sind', 'Sinh', 'Sogd', 'Sogo', 'Sora', 'Soyo', 'Sund', 'Sunu', 'Sylo', 'Syrc', 'Syre', 'Syrj', 'Syrn',
+  'Tagb', 'Takr', 'Tale', 'Talu', 'Taml', 'Tang', 'Tavt', 'Tayo', 'Telu', 'Teng', 'Tfng', 'Tglg', 'Thaa', 'Thai', 'Tibt', 'Tirh', 'Tnsa', 'Todr', 'Tols', 'Toto', 'Tutg',
+  'Ugar',
+  'Vaii', 'Visp', 'Vith',
+  'Wara', 'Wcho', 'Wole',
+  'Xpeo', 'Xsux',
+  'Yezi', 'Yiii',
+  'Zanb', 'Zinh', 'Zmth', 'Zsye', 'Zsym', 'Zxxx', 'Zyyy', 'Zzzz',
+]);
+
+export default function isISO15924(str) {
+  assertString(str);
+  return validISO15924Codes.has(str);
+}
+
+export const ScriptCodes = validISO15924Codes;
diff --git a/src/lib/isMobilePhone.js b/src/lib/isMobilePhone.js
index 4779797..b45bd64 100644
--- a/src/lib/isMobilePhone.js
+++ b/src/lib/isMobilePhone.js
@@ -144,7 +144,7 @@ const phones = {
   'sl-SI': /^(\+386\s?|0)(\d{1}\s?\d{3}\s?\d{2}\s?\d{2}|\d{2}\s?\d{3}\s?\d{3})$/,
   'sk-SK': /^(\+?421)? ?[1-9][0-9]{2} ?[0-9]{3} ?[0-9]{3}$/,
   'so-SO': /^(\+?252|0)((6[0-9])\d{7}|(7[1-9])\d{7})$/,
-  'sq-AL': /^(\+355|0)6[789]\d{6}$/,
+  'sq-AL': /^(\+355|0)6[2-9]\d{7}$/,
   'sr-RS': /^(\+3816|06)[- \d]{5,9}$/,
   'sv-SE': /^(\+?46|0)[\s\-]?7[\s\-]?[02369]([\s\-]?\d){7}$/,
   'tg-TJ': /^(\+?992)?[5][5]\d{7}$/,
diff --git a/src/lib/isPassportNumber.js b/src/lib/isPassportNumber.js
index 92282ac..33481e8 100644
--- a/src/lib/isPassportNumber.js
+++ b/src/lib/isPassportNumber.js
@@ -16,7 +16,7 @@ const passportRegexByCountryCode = {
   BG: /^\d{9}$/, // BULGARIA
   BR: /^[A-Z]{2}\d{6}$/, // BRAZIL
   BY: /^[A-Z]{2}\d{7}$/, // BELARUS
-  CA: /^[A-Z]{2}\d{6}$/, // CANADA
+  CA: /^[A-Z]{2}\d{6}$|^[A-Z]\d{6}[A-Z]{2}$/, // CANADA
   CH: /^[A-Z]\d{7}$/, // SWITZERLAND
   CN: /^G\d{8}$|^E(?![IO])[A-Z0-9]\d{7}$/, // CHINA [G=Ordinary, E=Electronic] followed by 8-digits, or E followed by any UPPERCASE letter (except I and O) followed by 7 digits
   CY: /^[A-Z](\d{6}|\d{8})$/, // CYPRUS


Test patch:
diff --git a/test/validators.test.js b/test/validators.test.js
index 8f3c4ab..d51672d 100644
--- a/test/validators.test.js
+++ b/test/validators.test.js
@@ -1047,136 +1047,6 @@ describe('Validators', () => {
     });
   });
 
-  it('should validate IP addresses', () => {
-    test({
-      validator: 'isIP',
-      valid: [
-        '127.0.0.1',
-        '0.0.0.0',
-        '255.255.255.255',
-        '1.2.3.4',
-        '::1',
-        '2001:db8:0000:1:1:1:1:1',
-        '2001:db8:3:4::192.0.2.33',
-        '2001:41d0:2:a141::1',
-        '::ffff:127.0.0.1',
-        '::0000',
-        '0000::',
-        '1::',
-        '1111:1:1:1:1:1:1:1',
-        'fe80::a6db:30ff:fe98:e946',
-        '::',
-        '::8',
-        '::ffff:127.0.0.1',
-        '::ffff:255.255.255.255',
-        '::ffff:0:255.255.255.255',
-        '::2:3:4:5:6:7:8',
-        '::255.255.255.255',
-        '0:0:0:0:0:ffff:127.0.0.1',
-        '1:2:3:4:5:6:7::',
-        '1:2:3:4:5:6::8',
-        '1::7:8',
-        '1:2:3:4:5::7:8',
-        '1:2:3:4:5::8',
-        '1::6:7:8',
-        '1:2:3:4::6:7:8',
-        '1:2:3:4::8',
-        '1::5:6:7:8',
-        '1:2:3::5:6:7:8',
-        '1:2:3::8',
-        '1::4:5:6:7:8',
-        '1:2::4:5:6:7:8',
-        '1:2::8',
-        '1::3:4:5:6:7:8',
-        '1::8',
-        'fe80::7:8%eth0',
-        'fe80::7:8%1',
-        '64:ff9b::192.0.2.33',
-        '0:0:0:0:0:0:10.0.0.1',
-      ],
-      invalid: [
-        'abc',
-        '256.0.0.0',
-        '0.0.0.256',
-        '26.0.0.256',
-        '0200.200.200.200',
-        '200.0200.200.200',
-        '200.200.0200.200',
-        '200.200.200.0200',
-        '::banana',
-        'banana::',
-        '::1banana',
-        '::1::',
-        '1:',
-        ':1',
-        ':1:1:1::2',
-        '1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1',
-        '::11111',
-        '11111:1:1:1:1:1:1:1',
-        '2001:db8:0000:1:1:1:1::1',
-        '0:0:0:0:0:0:ffff:127.0.0.1',
-        '0:0:0:0:ffff:127.0.0.1',
-      ],
-    });
-    test({
-      validator: 'isIP',
-      args: [4],
-      valid: [
-        '127.0.0.1',
-        '0.0.0.0',
-        '255.255.255.255',
-        '1.2.3.4',
-        '255.0.0.1',
-        '0.0.1.1',
-      ],
-      invalid: [
-        '::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: 'isIP',
-      args: [6],
-      valid: [
-        '::1',
-        '2001:db8:0000:1:1:1:1:1',
-        '::ffff:127.0.0.1',
-        'fe80::1234%1',
-        'ff08::9abc%10',
-        'ff08::9abc%interface10',
-        'ff02::5678%pvc1.3',
-      ],
-      invalid: [
-        '127.0.0.1',
-        '0.0.0.0',
-        '255.255.255.255',
-        '1.2.3.4',
-        '::ffff:287.0.0.1',
-        '%',
-        'fe80::1234%',
-        'fe80::1234%1%3%4',
-        'fe80%fe80%',
-      ],
-    });
-    test({
-      validator: 'isIP',
-      args: [10],
-      valid: [],
-      invalid: [
-        '127.0.0.1',
-        '0.0.0.0',
-        '255.255.255.255',
-        '1.2.3.4',
-        '::1',
-        '2001:db8:0000:1:1:1:1:1',
-      ],
-    });
-  });
-
   it('should validate isIPRange', () => {
     test({
       validator: 'isIPRange',
@@ -1371,6 +1241,7 @@ describe('Validators', () => {
       ],
     });
   });
+
   it('should validate alpha strings', () => {
     test({
       validator: 'isAlpha',
@@ -3085,9 +2956,17 @@ describe('Validators', () => {
       valid: [
         'GA302922',
         'ZE000509',
+        'A123456AB',
+        'Z556378HG',
       ],
       invalid: [
         'AB0123456',
+        'AZ556378H',
+        '556378HCX',
+        '556378432',
+        '5563784',
+        '#B12345FD',
+        'A43F12354',
       ],
     });
 
@@ -10341,15 +10220,27 @@ describe('Validators', () => {
       {
         locale: 'sq-AL',
         valid: [
-          '067123456',
-          '+35567123456',
+          '0621234567',
+          '0661234567',
+          '0671234567',
+          '0681234567',
+          '0691234567',
+          '+355621234567',
+          '+355651234567',
+          '+355661234567',
+          '+355671234567',
+          '+355681234567',
+          '+355691234567',
         ],
         invalid: [
           '67123456',
           '06712345',
+          '067123456',
           '06712345678',
-          '065123456',
-          '057123456',
+          '0571234567',
+          '+3556712345',
+          '+35565123456',
+          '+35157123456',
           'NotANumber',
         ],
       },
@@ -12131,6 +12022,28 @@ describe('Validators', () => {
     });
   });
 
+  it('should validate ISO 15924 script codes', () => {
+    test({
+      validator: 'isISO15924',
+      valid: [
+        'Adlm',
+        'Bass',
+        'Copt',
+        'Dsrt',
+        'Egyd',
+        'Latn',
+        'Zzzz',
+      ],
+      invalid: [
+        '',
+        'arab',
+        'zzzz',
+        'Qaby',
+        'Lati',
+      ],
+    });
+  });
+
   it('should validate RFC 3339 dates', () => {
     test({
       validator: 'isRFC3339',
diff --git a/test/validators/isIP.test.js b/test/validators/isIP.test.js
new file mode 100644
index 0000000..9b01d02
--- /dev/null
+++ b/test/validators/isIP.test.js
@@ -0,0 +1,302 @@
+import test from '../testFunctions';
+
+describe('isIP', () => {
+  it('should validate IP addresses', () => {
+    test({
+      validator: 'isIP',
+      valid: [
+        '127.0.0.1',
+        '0.0.0.0',
+        '255.255.255.255',
+        '1.2.3.4',
+        '::1',
+        '2001:db8:0000:1:1:1:1:1',
+        '2001:db8:3:4::192.0.2.33',
+        '2001:41d0:2:a141::1',
+        '::ffff:127.0.0.1',
+        '::0000',
+        '0000::',
+        '1::',
+        '1111:1:1:1:1:1:1:1',
+        'fe80::a6db:30ff:fe98:e946',
+        '::',
+        '::8',
+        '::ffff:127.0.0.1',
+        '::ffff:255.255.255.255',
+        '::ffff:0:255.255.255.255',
+        '::2:3:4:5:6:7:8',
+        '::255.255.255.255',
+        '0:0:0:0:0:ffff:127.0.0.1',
+        '1:2:3:4:5:6:7::',
+        '1:2:3:4:5:6::8',
+        '1::7:8',
+        '1:2:3:4:5::7:8',
+        '1:2:3:4:5::8',
+        '1::6:7:8',
+        '1:2:3:4::6:7:8',
+        '1:2:3:4::8',
+        '1::5:6:7:8',
+        '1:2:3::5:6:7:8',
+        '1:2:3::8',
+        '1::4:5:6:7:8',
+        '1:2::4:5:6:7:8',
+        '1:2::8',
+        '1::3:4:5:6:7:8',
+        '1::8',
+        'fe80::7:8%eth0',
+        'fe80::7:8%1',
+        '64:ff9b::192.0.2.33',
+        '0:0:0:0:0:0:10.0.0.1',
+      ],
+      invalid: [
+        'abc',
+        '256.0.0.0',
+        '0.0.0.256',
+        '26.0.0.256',
+        '0200.200.200.200',
+        '200.0200.200.200',
+        '200.200.0200.200',
+        '200.200.200.0200',
+        '::banana',
+        'banana::',
+        '::1banana',
+        '::1::',
+        '1:',
+        ':1',
+        ':1:1:1::2',
+        '1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1',
+        '::11111',
+        '11111:1:1:1:1:1:1:1',
+        '2001:db8:0000:1:1:1:1::1',
+        '0:0:0:0:0:0:ffff:127.0.0.1',
+        '0:0:0:0:ffff:127.0.0.1',
+      ],
+    });
+
+    test({
+      validator: 'isIP',
+      args: [{ version: 'invalid version' }],
+      valid: [],
+      invalid: [
+        '127.0.0.1',
+        '0.0.0.0',
+        '255.255.255.255',
+        '1.2.3.4',
+      ],
+    });
+
+    test({
+      validator: 'isIP',
+      args: [{ version: null }],
+      valid: [
+        '127.0.0.1',
+        '0.0.0.0',
+        '255.255.255.255',
+        '1.2.3.4',
+      ],
+    });
+
+    test({
+      validator: 'isIP',
+      args: [{ version: undefined }],
+      valid: [
+        '127.0.0.1',
+        '0.0.0.0',
+        '255.255.255.255',
+        '1.2.3.4',
+      ],
+    });
+
+    test({
+      validator: 'isIP',
+      args: [{ version: 4 }],
+      valid: [
+        '127.0.0.1',
+        '0.0.0.0',
+        '255.255.255.255',
+        '1.2.3.4',
+        '255.0.0.1',
+        '0.0.1.1',
+      ],
+      invalid: [
+        '::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: 'isIP',
+      args: [{ version: 6 }],
+      valid: [
+        '::1',
+        '2001:db8:0000:1:1:1:1:1',
+        '::ffff:127.0.0.1',
+        'fe80::1234%1',
+        'ff08::9abc%10',
+        'ff08::9abc%interface10',
+        'ff02::5678%pvc1.3',
+      ],
+      invalid: [
+        '127.0.0.1',
+        '0.0.0.0',
+        '255.255.255.255',
+        '1.2.3.4',
+        '::ffff:287.0.0.1',
+        '%',
+        'fe80::1234%',
+        'fe80::1234%1%3%4',
+        'fe80%fe80%',
+      ],
+    });
+
+    test({
+      validator: 'isIP',
+      args: [{ version: 10 }],
+      valid: [],
+      invalid: [
+        '127.0.0.1',
+        '0.0.0.0',
+        '255.255.255.255',
+        '1.2.3.4',
+        '::1',
+        '2001:db8:0000:1:1:1:1:1',
+      ],
+    });
+  });
+
+  describe('legacy syntax', () => {
+    it('should validate IP addresses', () => {
+      test({
+        validator: 'isIP',
+        valid: [
+          '127.0.0.1',
+          '0.0.0.0',
+          '255.255.255.255',
+          '1.2.3.4',
+          '::1',
+          '2001:db8:0000:1:1:1:1:1',
+          '2001:db8:3:4::192.0.2.33',
+          '2001:41d0:2:a141::1',
+          '::ffff:127.0.0.1',
+          '::0000',
+          '0000::',
+          '1::',
+          '1111:1:1:1:1:1:1:1',
+          'fe80::a6db:30ff:fe98:e946',
+          '::',
+          '::8',
+          '::ffff:127.0.0.1',
+          '::ffff:255.255.255.255',
+          '::ffff:0:255.255.255.255',
+          '::2:3:4:5:6:7:8',
+          '::255.255.255.255',
+          '0:0:0:0:0:ffff:127.0.0.1',
+          '1:2:3:4:5:6:7::',
+          '1:2:3:4:5:6::8',
+          '1::7:8',
+          '1:2:3:4:5::7:8',
+          '1:2:3:4:5::8',
+          '1::6:7:8',
+          '1:2:3:4::6:7:8',
+          '1:2:3:4::8',
+          '1::5:6:7:8',
+          '1:2:3::5:6:7:8',
+          '1:2:3::8',
+          '1::4:5:6:7:8',
+          '1:2::4:5:6:7:8',
+          '1:2::8',
+          '1::3:4:5:6:7:8',
+          '1::8',
+          'fe80::7:8%eth0',
+          'fe80::7:8%1',
+          '64:ff9b::192.0.2.33',
+          '0:0:0:0:0:0:10.0.0.1',
+        ],
+        invalid: [
+          'abc',
+          '256.0.0.0',
+          '0.0.0.256',
+          '26.0.0.256',
+          '0200.200.200.200',
+          '200.0200.200.200',
+          '200.200.0200.200',
+          '200.200.200.0200',
+          '::banana',
+          'banana::',
+          '::1banana',
+          '::1::',
+          '1:',
+          ':1',
+          ':1:1:1::2',
+          '1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1',
+          '::11111',
+          '11111:1:1:1:1:1:1:1',
+          '2001:db8:0000:1:1:1:1::1',
+          '0:0:0:0:0:0:ffff:127.0.0.1',
+          '0:0:0:0:ffff:127.0.0.1',
+        ],
+      });
+      test({
+        validator: 'isIP',
+        args: [4],
+        valid: [
+          '127.0.0.1',
+          '0.0.0.0',
+          '255.255.255.255',
+          '1.2.3.4',
+          '255.0.0.1',
+          '0.0.1.1',
+        ],
+        invalid: [
+          '::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: 'isIP',
+        args: [6],
+        valid: [
+          '::1',
+          '2001:db8:0000:1:1:1:1:1',
+          '::ffff:127.0.0.1',
+          'fe80::1234%1',
+          'ff08::9abc%10',
+          'ff08::9abc%interface10',
+          'ff02::5678%pvc1.3',
+        ],
+        invalid: [
+          '127.0.0.1',
+          '0.0.0.0',
+          '255.255.255.255',
+          '1.2.3.4',
+          '::ffff:287.0.0.1',
+          '%',
+          'fe80::1234%',
+          'fe80::1234%1%3%4',
+          'fe80%fe80%',
+        ],
+      });
+      test({
+        validator: 'isIP',
+        args: [10],
+        valid: [],
+        invalid: [
+          '127.0.0.1',
+          '0.0.0.0',
+          '255.255.255.255',
+          '1.2.3.4',
+          '::1',
+          '2001:db8:0000:1:1:1:1:1',
+        ],
+      });
+    });
+  });
+});