Repository: validatorjs/validator.js
Pull request #2521 (2025-01-22 18:38:17)

feat(isTime): add `withOptionalSeconds` option
<!--
Add a descriptive title textbox above, e.g.
feat(validatorName): brief title of what has been done
-->
The `withOptionalSeconds` option is useful when you want to validate a time and want to allow with or without seconds. 

<!--- briefly describe what you have done in this PR --->

<!--- provide some (credible) references showing the structure of the data to be validated, if applicable --->

## Checklist

- [x] PR contains only changes related; no stray files, etc.
- [x] README updated (where applicable)
- [x] Tests written (where applicable)
- [ ] 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 791fb53..a64ca45 100644
--- a/README.md
+++ b/README.md
@@ -93,8 +93,8 @@ Validator                               | Description
 **isAscii(str)**                        | check if the string contains ASCII chars only.
 **isBase32(str [, options])**           | check if the string is base32 encoded. `options` is optional and defaults to `{ crockford: false }`.<br/> When `crockford` is true it tests the given base32 encoded string using [Crockford's base32 alternative][Crockford Base32].
 **isBase58(str)**                       | check if the string is base58 encoded.
-**isBase64(str [, options])**          | check if the string is base64 encoded. `options` is optional and defaults to `{ urlSafe: false }`<br/> when `urlSafe` is true it tests the given base64 encoded string is [url safe][Base64 URL Safe].
-**isBefore(str [, date])**              | check if the string is a date that is before the specified date.
+**isBase64(str [, options])**          | check if the string is base64 encoded. `options` is optional and defaults to `{ urlSafe: false, padding: true }`<br/> when `urlSafe` is true default value for `padding` is false and it tests the given base64 encoded string is [url safe][Base64 URL Safe].
+**isBefore(str [, options])**              | check if the string is a date that is before the specified date.<br/><br/>`options` is an object that defaults to `{ comparisonDate: Date().toString() }`.<br/><br/>**Options:**<br/>`comparisonDate`: Date to compare to. Defaults to `Date().toString()` (now).
 **isBIC(str)**                          | check if the string is a BIC (Bank Identification Code) or SWIFT code.
 **isBoolean(str [, options])**          | check if the string is a boolean.<br/>`options` is an object which defaults to `{ loose: false }`. If `loose` is set to false, the validator will strictly match ['true', 'false', '0', '1']. If `loose` is set to true, the validator will also match 'yes', 'no', and will match a valid boolean string of any case. (e.g.: ['true', 'True', 'TRUE']).
 **isBtcAddress(str)**            | check if the string is a valid BTC address.
@@ -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.
@@ -149,7 +150,7 @@ Validator                               | Description
 **isMailtoURI(str, [, options])**                      | check if the string is a [Mailto URI format][Mailto URI Format].<br/><br/>`options` is an object of validating emails inside the URI (check `isEmail`s options for details).
 **isMD5(str)**                          | check if the string is a MD5 hash.<br/><br/>Please note that you can also use the `isHash(str, 'md5')` function. Keep in mind that MD5 has some collision weaknesses compared to other algorithms (e.g., SHA).
 **isMimeType(str)**                     | check if the string matches to a valid [MIME type][MIME Type] format.
-**isMobilePhone(str [, locale [, options]])**          | check if the string is a mobile phone number,<br/><br/>`locale` is either an array of locales (e.g. `['sk-SK', 'sr-RS']`) OR one of `['am-Am', 'ar-AE', 'ar-BH', 'ar-DZ', 'ar-EG', 'ar-EH', 'ar-IQ', 'ar-JO', 'ar-KW', 'ar-PS', 'ar-SA', 'ar-SD', 'ar-SY', 'ar-TN', 'ar-YE', 'az-AZ', 'az-LB', 'az-LY', 'be-BY', 'bg-BG', 'bn-BD', 'bs-BA', 'ca-AD', 'cs-CZ', 'da-DK', 'de-AT', 'de-CH', 'de-DE', 'de-LU', 'dv-MV', 'dz-BT', 'el-CY', 'el-GR', 'en-AG', 'en-AI', 'en-AU', 'en-BM', 'en-BS', 'en-BW', 'en-CA', 'en-GB', 'en-GG', 'en-GH', 'en-GY', 'en-HK', 'en-IE', 'en-IN', 'en-JM', 'en-KE', 'en-KI', 'en-KN', 'en-LS', 'en-MO', 'en-MT', 'en-MU', 'en-MW', 'en-NG', 'en-NZ', 'en-PG', 'en-PH', 'en-PK', 'en-RW', 'en-SG', 'en-SL', 'en-SS', 'en-TZ', 'en-UG', 'en-US', 'en-ZA', 'en-ZM', 'en-ZW', 'es-AR', 'es-BO', 'es-CL', 'es-CO', 'es-CR', 'es-CU', 'es-DO', 'es-EC', 'es-ES', 'es-GT','es-HN', 'es-MX', 'es-NI', 'es-PA', 'es-PE', 'es-PY', 'es-SV', 'es-UY', 'es-VE', 'et-EE', 'fa-AF', 'fa-IR', 'fi-FI', 'fj-FJ', 'fo-FO', 'fr-BE', 'fr-BF', 'fr-BJ', 'fr-CD', 'fr-CF', 'fr-FR', 'fr-GF', 'fr-GP', 'fr-MQ', 'fr-PF', 'fr-RE', 'fr-WF', 'ga-IE', 'he-IL', 'hu-HU', 'id-ID', 'ir-IR', 'it-IT', 'it-SM', 'ja-JP', 'ka-GE', 'kk-KZ', 'kl-GL', 'ko-KR', 'ky-KG', 'lt-LT', 'mg-MG', 'mn-MN', 'ms-MY', 'my-MM', 'mz-MZ', 'nb-NO', 'ne-NP', 'nl-AW', 'nl-BE', 'nl-NL', 'nn-NO', 'pl-PL', 'pt-AO', 'pt-BR', 'pt-PT', 'ro-Md', 'ro-RO', 'ru-RU', 'si-LK', 'sk-SK', 'sl-SI', 'so-SO', 'sq-AL', 'sr-RS', 'sv-SE', 'tg-TJ', 'th-TH', 'tk-TM', 'tr-TR', 'uk-UA', 'uz-UZ', 'vi-VN', 'zh-CN', 'zh-HK', 'zh-MO', 'zh-TW']` OR defaults to `'any'`. If 'any' or a falsey value is used, function will check if any of the locales match).<br/><br/>`options` is an optional object that can be supplied with the following keys: `strictMode`, if this is set to `true`, the mobile phone number must be supplied with the country code and therefore must start with `+`. Locale list is `validator.isMobilePhoneLocales`.
+**isMobilePhone(str [, locale [, options]])**          | check if the string is a mobile phone number,<br/><br/>`locale` is either an array of locales (e.g. `['sk-SK', 'sr-RS']`) OR one of `['am-Am', 'ar-AE', 'ar-BH', 'ar-DZ', 'ar-EG', 'ar-EH', 'ar-IQ', 'ar-JO', 'ar-KW', 'ar-PS', 'ar-SA', 'ar-SD', 'ar-SY', 'ar-TN', 'ar-YE', 'az-AZ', 'az-LB', 'az-LY', 'be-BY', 'bg-BG', 'bn-BD', 'bs-BA', 'ca-AD', 'cs-CZ', 'da-DK', 'de-AT', 'de-CH', 'de-DE', 'de-LU', 'dv-MV', 'dz-BT', 'el-CY', 'el-GR', 'en-AG', 'en-AI', 'en-AU', 'en-BM', 'en-BS', 'en-BW', 'en-CA', 'en-GB', 'en-GG', 'en-GH', 'en-GY', 'en-HK', 'en-IE', 'en-IN', 'en-JM', 'en-KE', 'en-KI', 'en-KN', 'en-LS', 'en-MO', 'en-MT', 'en-MU', 'en-MW', 'en-NG', 'en-NZ', 'en-PG', 'en-PH', 'en-PK', 'en-RW', 'en-SG', 'en-SL', 'en-SS', 'en-TZ', 'en-UG', 'en-US', 'en-ZA', 'en-ZM', 'en-ZW', 'es-AR', 'es-BO', 'es-CL', 'es-CO', 'es-CR', 'es-CU', 'es-DO', 'es-EC', 'es-ES', 'es-GT','es-HN', 'es-MX', 'es-NI', 'es-PA', 'es-PE', 'es-PY', 'es-SV', 'es-UY', 'es-VE', 'et-EE', 'fa-AF', 'fa-IR', 'fi-FI', 'fj-FJ', 'fo-FO', 'fr-BE', 'fr-BF', 'fr-BJ', 'fr-CD', 'fr-CF', 'fr-FR', 'fr-GF', 'fr-GP', 'fr-MQ', 'fr-PF', 'fr-RE', 'fr-WF', 'ga-IE', 'he-IL', 'hu-HU', 'id-ID', 'ir-IR', 'it-IT', 'it-SM', 'ja-JP', 'ka-GE', 'kk-KZ', 'kl-GL', 'ko-KR', 'ky-KG', 'lt-LT', 'mg-MG', 'mn-MN', 'mk-MK', 'ms-MY', 'my-MM', 'mz-MZ', 'nb-NO', 'ne-NP', 'nl-AW', 'nl-BE', 'nl-NL', 'nn-NO', 'pl-PL', 'pt-AO', 'pt-BR', 'pt-PT', 'ro-Md', 'ro-RO', 'ru-RU', 'si-LK', 'sk-SK', 'sl-SI', 'so-SO', 'sq-AL', 'sr-RS', 'sv-SE', 'tg-TJ', 'th-TH', 'tk-TM', 'tr-TR', 'uk-UA', 'uz-UZ', 'vi-VN', 'zh-CN', 'zh-HK', 'zh-MO', 'zh-TW']` OR defaults to `'any'`. If 'any' or a falsey value is used, function will check if any of the locales match).<br/><br/>`options` is an optional object that can be supplied with the following keys: `strictMode`, if this is set to `true`, the mobile phone number must be supplied with the country code and therefore must start with `+`. Locale list is `validator.isMobilePhoneLocales`.
 **isMongoId(str)**                      | check if the string is a valid hex-encoded representation of a [MongoDB ObjectId][mongoid].
 **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` determines 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', 'eo', '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']`.
@@ -164,7 +165,7 @@ Validator                               | Description
 **isUppercase(str)**                    | check if the string is uppercase.
 **isSlug(str)**                         | check if the string is of type slug.
 **isStrongPassword(str [, options])**   | check if the string can be considered a strong password 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.<br/>Default options: <br/>`{ 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 }`
-**isTime(str [, options])**             | check if the string is a valid time e.g. [`23:01:59`, new Date().toLocaleTimeString()].<br/><br/> `options` is an object which can contain the keys `hourFormat` or `mode`.<br/><br/>`hourFormat` is a key and defaults to `'hour24'`.<br/><br/>`mode` is a key and defaults to `'default'`. <br/><br/>`hourFormat` can contain the values `'hour12'` or `'hour24'`, `'hour24'` will validate hours in 24 format and `'hour12'` will validate hours in 12 format. <br/><br/>`mode` can contain the values `'default'` or `'withSeconds'`, `'default'` will validate `HH:MM` format, `'withSeconds'` will validate the `HH:MM:SS` format.
+**isTime(str [, options])**             | check if the string is a valid time e.g. [`23:01:59`, new Date().toLocaleTimeString()].<br/><br/> `options` is an object which can contain the keys `hourFormat` or `mode`.<br/><br/>`hourFormat` is a key and defaults to `'hour24'`.<br/><br/>`mode` is a key and defaults to `'default'`. <br/><br/>`hourFormat` can contain the values `'hour12'` or `'hour24'`, `'hour24'` will validate hours in 24 format and `'hour12'` will validate hours in 12 format. <br/><br/>`mode` can contain the values `'default', 'withSeconds', withOptionalSeconds`, `'default'` will validate `HH:MM` format, `'withSeconds'` will validate the `HH:MM:SS` format, `'withOptionalSeconds'` will validate `'HH:MM'` and `'HH:MM:SS'` formats.
 **isTaxID(str, locale)**                | check if the string is a valid Tax Identification Number. Default locale is `en-US`.<br/><br/>More info about exact TIN support can be found in `src/lib/isTaxID.js`.<br/><br/>Supported locales: `[ 'bg-BG', 'cs-CZ', 'de-AT', 'de-DE', 'dk-DK', 'el-CY', 'el-GR', 'en-CA', 'en-GB', 'en-IE', 'en-US', 'es-AR', 'es-ES', 'et-EE', 'fi-FI', 'fr-BE', 'fr-CA', '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-BR', 'pt-PT', 'ro-RO', 'sk-SK', 'sl-SI', 'sv-SE', 'uk-UA']`.
 **isURL(str [, options])**              | check if the string is a URL.<br/><br/>`options` is an object which defaults to `{ protocols: ['http','https','ftp'], require_tld: true, require_protocol: false, require_host: true, require_port: false, require_valid_protocol: true, allow_underscores: false, host_whitelist: false, host_blacklist: false, allow_trailing_dot: false, allow_protocol_relative_urls: false, allow_fragments: true, allow_query_components: true, disallow_auth: false, validate_length: true }`.<br/><br/>`require_protocol` - if set to true isURL will return false if protocol is not present in the URL.<br/>`require_valid_protocol` - isURL will check if the URL's protocol is present in the protocols option.<br/>`protocols` - valid protocols can be modified with this option.<br/>`require_host` - if set to false isURL will not check if host is present in the URL.<br/>`require_port` - if set to true isURL will check if port is present in the URL.<br/>`allow_protocol_relative_urls` - if set to true protocol relative URLs will be allowed.<br/>`allow_fragments` - if set to false isURL will return false if fragments are present.<br/>`allow_query_components` - if set to false isURL will return false if query components are present.<br/>`validate_length` - if set to false isURL will skip string length validation. `max_allowed_length` will be ignored if this is set as `false`.<br/>`max_allowed_length` - if set isURL will not allow URLs longer than the specified value (default is 2084 that IE maximum URL length).
 **isULID(str)**                         | check if the string is a [ULID](https://github.com/ulid/spec).
@@ -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/isAfter.js b/src/lib/isAfter.js
index e116e77..149622a 100644
--- a/src/lib/isAfter.js
+++ b/src/lib/isAfter.js
@@ -3,9 +3,10 @@ import toDate from './toDate';
 export default function isAfter(date, options) {
   // For backwards compatibility:
   // isAfter(str [, date]), i.e. `options` could be used as argument for the legacy `date`
-  const comparisonDate = options?.comparisonDate || options || Date().toString();
+  const comparisonDate = (typeof options === 'object' ? options.comparisonDate : options) || Date().toString();
 
   const comparison = toDate(comparisonDate);
   const original = toDate(date);
+
   return !!(original && comparison && original > comparison);
 }
diff --git a/src/lib/isBase64.js b/src/lib/isBase64.js
index 02dead0..7eb3a5b 100644
--- a/src/lib/isBase64.js
+++ b/src/lib/isBase64.js
@@ -1,28 +1,23 @@
 import assertString from './util/assertString';
 import merge from './util/merge';
 
-const notBase64 = /[^A-Z0-9+\/=]/i;
-const urlSafeBase64 = /^[A-Z0-9_\-]*$/i;
-
-const defaultBase64Options = {
-  urlSafe: false,
-};
+const base64WithPadding = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{4})$/;
+const base64WithoutPadding = /^[A-Za-z0-9+/]+$/;
+const base64UrlWithPadding = /^(?:[A-Za-z0-9_-]{4})*(?:[A-Za-z0-9_-]{2}==|[A-Za-z0-9_-]{3}=|[A-Za-z0-9_-]{4})$/;
+const base64UrlWithoutPadding = /^[A-Za-z0-9_-]+$/;
 
 export default function isBase64(str, options) {
   assertString(str);
-  options = merge(options, defaultBase64Options);
-  const len = str.length;
+  options = merge(options, { urlSafe: false, padding: !options?.urlSafe });
 
-  if (options.urlSafe) {
-    return urlSafeBase64.test(str);
-  }
+  if (str === '') return true;
 
-  if (len % 4 !== 0 || notBase64.test(str)) {
-    return false;
+  let regex;
+  if (options.urlSafe) {
+    regex = options.padding ? base64UrlWithPadding : base64UrlWithoutPadding;
+  } else {
+    regex = options.padding ? base64WithPadding : base64WithoutPadding;
   }
 
-  const firstPaddingChar = str.indexOf('=');
-  return firstPaddingChar === -1 ||
-    firstPaddingChar === len - 1 ||
-    (firstPaddingChar === len - 2 && str[len - 1] === '=');
+  return (!options.padding || str.length % 4 === 0) && regex.test(str);
 }
diff --git a/src/lib/isBefore.js b/src/lib/isBefore.js
index 8314f0e..977d0ad 100644
--- a/src/lib/isBefore.js
+++ b/src/lib/isBefore.js
@@ -1,9 +1,12 @@
-import assertString from './util/assertString';
 import toDate from './toDate';
 
-export default function isBefore(str, date = String(new Date())) {
-  assertString(str);
-  const comparison = toDate(date);
-  const original = toDate(str);
+export default function isBefore(date, options) {
+  // For backwards compatibility:
+  // isBefore(str [, date]), i.e. `options` could be used as argument for the legacy `date`
+  const comparisonDate = (typeof options === 'object' ? options.comparisonDate : options) || Date().toString();
+
+  const comparison = toDate(comparisonDate);
+  const original = toDate(date);
+
   return !!(original && comparison && original < comparison);
 }
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 393ee6f..5097698 100644
--- a/src/lib/isMobilePhone.js
+++ b/src/lib/isMobilePhone.js
@@ -33,7 +33,7 @@ const phones = {
   'de-LU': /^(\+352)?((6\d1)\d{6})$/,
   'dv-MV': /^(\+?960)?(7[2-9]|9[1-9])\d{5}$/,
   'el-GR': /^(\+?30|0)?6(8[5-9]|9(?![26])[0-9])\d{7}$/,
-  'el-CY': /^(\+?357?)?(9(9|6)\d{6})$/,
+  'el-CY': /^(\+?357?)?(9(9|7|6|5|4)\d{6})$/,
   'en-AI': /^(\+?1|0)264(?:2(35|92)|4(?:6[1-2]|76|97)|5(?:3[6-9]|8[1-4])|7(?:2(4|9)|72))\d{4}$/,
   'en-AU': /^(\+?61|0)4\d{8}$/,
   'en-AG': /^(?:\+1|1)268(?:464|7(?:1[3-9]|[28]\d|3[0246]|64|7[0-689]))\d{4}$/,
@@ -136,7 +136,7 @@ const phones = {
   'pl-PL': /^(\+?48)? ?([5-8]\d|45) ?\d{3} ?\d{2} ?\d{2}$/,
   'pt-BR': /^((\+?55\ ?[1-9]{2}\ ?)|(\+?55\ ?\([1-9]{2}\)\ ?)|(0[1-9]{2}\ ?)|(\([1-9]{2}\)\ ?)|([1-9]{2}\ ?))((\d{4}\-?\d{4})|(9[1-9]{1}\d{3}\-?\d{4}))$/,
   'pt-PT': /^(\+?351)?9[1236]\d{7}$/,
-  'pt-AO': /^(\+244)\d{9}$/,
+  'pt-AO': /^(\+?244)?9\d{8}$/,
   'ro-MD': /^(\+?373|0)((6(0|1|2|6|7|8|9))|(7(6|7|8|9)))\d{6}$/,
   'ro-RO': /^(\+?40|0)\s?7\d{2}(\/|\s|\.|-)?\d{3}(\s|\.|-)?\d{3}$/,
   'ru-RU': /^(\+?7|8)?9\d{9}$/,
@@ -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}$/,
@@ -160,6 +160,7 @@ const phones = {
   'ar-YE': /^(((\+|00)9677|0?7)[0137]\d{7}|((\+|00)967|0)[1-7]\d{6})$/,
   'ar-EH': /^(\+?212|0)[\s\-]?(5288|5289)[\s\-]?\d{5}$/,
   'fa-AF': /^(\+93|0)?(2{1}[0-8]{1}|[3-5]{1}[0-4]{1})(\d{7})$/,
+  'mk-MK': /^(\+?389|0)?((?:2[2-9]\d{6}|(?:3[1-4]|4[2-8])\d{6}|500\d{5}|5[2-9]\d{6}|7[0-9][2-9]\d{5}|8[1-9]\d{6}|800\d{5}|8009\d{4}))$/,
 };
 /* eslint-enable max-len */
 
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
diff --git a/src/lib/isPostalCode.js b/src/lib/isPostalCode.js
index cadf393..01ae3fc 100644
--- a/src/lib/isPostalCode.js
+++ b/src/lib/isPostalCode.js
@@ -28,7 +28,7 @@ const patterns = {
   EE: fiveDigit,
   ES: /^(5[0-2]{1}|[0-4]{1}\d{1})\d{3}$/,
   FI: fiveDigit,
-  FR: /^\d{2}\s?\d{3}$/,
+  FR: /^(?:(?:0[1-9]|[1-8]\d|9[0-5])\d{3}|97[1-46]\d{2})$/,
   GB: /^(gir\s?0aa|[a-z]{1,2}\d[\da-z]?\s?(\d[a-z]{2})?)$/i,
   GR: /^\d{3}\s?\d{2}$/,
   HR: /^([1-5]\d{4}$)/,
diff --git a/src/lib/isRgbColor.js b/src/lib/isRgbColor.js
index 5267d56..6e28662 100644
--- a/src/lib/isRgbColor.js
+++ b/src/lib/isRgbColor.js
@@ -2,9 +2,9 @@
 import assertString from './util/assertString';
 
 const rgbColor = /^rgb\((([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5]),){2}([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\)$/;
-const rgbaColor = /^rgba\((([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5]),){3}(0?\.\d|1(\.0)?|0(\.0)?)\)$/;
+const rgbaColor = /^rgba\((([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5]),){3}(0?\.\d\d?|1(\.0)?|0(\.0)?)\)$/;
 const rgbColorPercent = /^rgb\((([0-9]%|[1-9][0-9]%|100%),){2}([0-9]%|[1-9][0-9]%|100%)\)$/;
-const rgbaColorPercent = /^rgba\((([0-9]%|[1-9][0-9]%|100%),){3}(0?\.\d|1(\.0)?|0(\.0)?)\)$/;
+const rgbaColorPercent = /^rgba\((([0-9]%|[1-9][0-9]%|100%),){3}(0?\.\d\d?|1(\.0)?|0(\.0)?)\)$/;
 const startsWithRgb = /^rgba?/;
 
 export default function isRgbColor(str, options) {
diff --git a/src/lib/isTime.js b/src/lib/isTime.js
index 076bbfa..3169864 100644
--- a/src/lib/isTime.js
+++ b/src/lib/isTime.js
@@ -9,10 +9,12 @@ const formats = {
   hour24: {
     default: /^([01]?[0-9]|2[0-3]):([0-5][0-9])$/,
     withSeconds: /^([01]?[0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])$/,
+    withOptionalSeconds: /^([01]?[0-9]|2[0-3]):([0-5][0-9])(?::([0-5][0-9]))?$/,
   },
   hour12: {
     default: /^(0?[1-9]|1[0-2]):([0-5][0-9]) (A|P)M$/,
     withSeconds: /^(0?[1-9]|1[0-2]):([0-5][0-9]):([0-5][0-9]) (A|P)M$/,
+    withOptionalSeconds: /^(0?[1-9]|1[0-2]):([0-5][0-9])(?::([0-5][0-9]))? (A|P)M$/,
   },
 };
 


Test patch:
diff --git a/test/validators.test.js b/test/validators.test.js
index d0908ad..5e10e27 100644
--- a/test/validators.test.js
+++ b/test/validators.test.js
@@ -1,9 +1,7 @@
 import assert from 'assert';
 import fs from 'fs';
 import timezone_mock from 'timezone-mock';
-import { format } from 'util';
 import vm from 'vm';
-import validator from '../src/index';
 import test from './testFunctions';
 
 let validator_js = fs.readFileSync(require.resolve('../validator.js')).toString();
@@ -1047,136 +1045,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 +1239,7 @@ describe('Validators', () => {
       ],
     });
   });
+
   it('should validate alpha strings', () => {
     test({
       validator: 'isAlpha',
@@ -3085,9 +2954,17 @@ describe('Validators', () => {
       valid: [
         'GA302922',
         'ZE000509',
+        'A123456AB',
+        'Z556378HG',
       ],
       invalid: [
         'AB0123456',
+        'AZ556378H',
+        '556378HCX',
+        '556378432',
+        '5563784',
+        '#B12345FD',
+        'A43F12354',
       ],
     });
 
@@ -4773,8 +4650,10 @@ describe('Validators', () => {
         'rgba(255,255,255,1)',
         'rgba(255,255,255,.1)',
         'rgba(255,255,255,0.1)',
+        'rgba(255,255,255,.12)',
         'rgb(5%,5%,5%)',
         'rgba(5%,5%,5%,.3)',
+        'rgba(5%,5%,5%,.32)',
       ],
       invalid: [
         'rgb(0,0,0,)',
@@ -4783,11 +4662,12 @@ describe('Validators', () => {
         'rgb()',
         'rgba(0,0,0)',
         'rgba(255,255,255,2)',
-        'rgba(255,255,255,.12)',
+        'rgba(255,255,255,.123)',
         'rgba(255,255,256,0.1)',
         'rgb(4,4,5%)',
         'rgba(5%,5%,5%)',
         'rgba(3,3,3%,.3)',
+        'rgba(5%,5%,5%,.321)',
         'rgb(101%,101%,101%)',
         'rgba(3%,3%,101%,0.3)',
         'rgb(101%,101%,101%) additional invalid string part',
@@ -5773,41 +5653,6 @@ describe('Validators', () => {
     });
   });
 
-  it('should validate dates against an end date', () => {
-    test({
-      validator: 'isBefore',
-      args: ['08/04/2011'],
-      valid: ['2010-07-02', '2010-08-04', new Date(0).toString()],
-      invalid: ['08/04/2011', new Date(2011, 9, 10).toString()],
-    });
-    test({
-      validator: 'isBefore',
-      args: [new Date(2011, 7, 4).toString()],
-      valid: ['2010-07-02', '2010-08-04', new Date(0).toString()],
-      invalid: ['08/04/2011', new Date(2011, 9, 10).toString()],
-    });
-    test({
-      validator: 'isBefore',
-      valid: [
-        '2000-08-04',
-        new Date(0).toString(),
-        new Date(Date.now() - 86400000).toString(),
-      ],
-      invalid: ['2100-07-02', new Date(2217, 10, 10).toString()],
-    });
-    test({
-      validator: 'isBefore',
-      args: ['2011-08-03'],
-      valid: ['1999-12-31'],
-      invalid: ['invalid date'],
-    });
-    test({
-      validator: 'isBefore',
-      args: ['invalid date'],
-      invalid: ['invalid date', '1999-12-31'],
-    });
-  });
-
   it('should validate ABA routing number', () => {
     test({
       validator: 'isAbaRouting',
@@ -7146,76 +6991,6 @@ describe('Validators', () => {
     });
   });
 
-  it('should validate base64 strings', () => {
-    test({
-      validator: 'isBase64',
-      valid: [
-        '',
-        'Zg==',
-        'Zm8=',
-        'Zm9v',
-        'Zm9vYg==',
-        'Zm9vYmE=',
-        'Zm9vYmFy',
-        'TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdC4=',
-        'Vml2YW11cyBmZXJtZW50dW0gc2VtcGVyIHBvcnRhLg==',
-        'U3VzcGVuZGlzc2UgbGVjdHVzIGxlbw==',
-        'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuMPNS1Ufof9EW/M98FNw' +
-        'UAKrwflsqVxaxQjBQnHQmiI7Vac40t8x7pIb8gLGV6wL7sBTJiPovJ0V7y7oc0Ye' +
-        'rhKh0Rm4skP2z/jHwwZICgGzBvA0rH8xlhUiTvcwDCJ0kc+fh35hNt8srZQM4619' +
-        'FTgB66Xmp4EtVyhpQV+t02g6NzK72oZI0vnAvqhpkxLeLiMCyrI416wHm5Tkukhx' +
-        'QmcL2a6hNOyu0ixX/x2kSFXApEnVrJ+/IxGyfyw8kf4N2IZpW5nEP847lpfj0SZZ' +
-        'Fwrd1mnfnDbYohX2zRptLy2ZUn06Qo9pkG5ntvFEPo9bfZeULtjYzIl6K8gJ2uGZ' +
-        'HQIDAQAB',
-      ],
-      invalid: [
-        '12345',
-        'Vml2YW11cyBmZXJtZtesting123',
-        'Zg=',
-        'Z===',
-        'Zm=8',
-        '=m9vYg==',
-        'Zm9vYmFy====',
-      ],
-    });
-
-    test({
-      validator: 'isBase64',
-      args: [{ urlSafe: true }],
-      valid: [
-        '',
-        'bGFkaWVzIGFuZCBnZW50bGVtZW4sIHdlIGFyZSBmbG9hdGluZyBpbiBzcGFjZQ',
-        '1234',
-        'bXVtLW5ldmVyLXByb3Vk',
-        'PDw_Pz8-Pg',
-        'VGhpcyBpcyBhbiBlbmNvZGVkIHN0cmluZw',
-      ],
-      invalid: [
-        ' AA',
-        '\tAA',
-        '\rAA',
-        '\nAA',
-        'This+isa/bad+base64Url==',
-        '0K3RgtC+INC30LDQutC+0LTQuNGA0L7QstCw0L3QvdCw0Y8g0YHRgtGA0L7QutCw',
-      ],
-      error: [
-        null,
-        undefined,
-        {},
-        [],
-        42,
-      ],
-    });
-
-    for (let i = 0, str = '', encoded; i < 1000; i++) {
-      str += String.fromCharCode(Math.random() * 26 | 97); // eslint-disable-line no-bitwise
-      encoded = Buffer.from(str).toString('base64');
-      if (!validator.isBase64(encoded)) {
-        let msg = format('validator.isBase64() failed with "%s"', encoded);
-        throw new Error(msg);
-      }
-    }
-  });
 
   it('should validate hex-encoded MongoDB ObjectId', () => {
     test({
@@ -8719,6 +8494,11 @@ describe('Validators', () => {
           '+3599148725',
           '96537247',
           '3596676533',
+          '+35795123455',
+          '+35797012204',
+          '35799123456',
+          '+35794123456',
+          '+35796123456',
         ],
         invalid: [
           '',
@@ -10341,15 +10121,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',
         ],
       },
@@ -10370,10 +10162,12 @@ describe('Validators', () => {
         locale: 'pt-AO',
         valid: [
           '+244911123432',
-          '+244123091232',
+          '911123432',
+          '244911123432',
         ],
         invalid: [
           '+2449111234321',
+          '+244811123432',
           '31234',
           '31234567',
           '512345',
@@ -10733,7 +10527,34 @@ describe('Validators', () => {
           'This should fail',
           '5021931234567',
           '+50281234567',
-
+        ],
+      },
+      {
+        locale: 'mk-MK',
+        valid: [
+          '+38923234567',
+          '38931234567',
+          '022123456',
+          '22234567',
+          '71234567',
+          '31234567',
+          '+38923091500',
+          '80091234',
+          '81123456',
+          '54123456',
+        ],
+        invalid: [
+          '38912345678',
+          '+389123456789',
+          '21234567',
+          '123456789',
+          '+3891234567',
+          '700012345',
+          '510123456',
+          'This should fail',
+          '+389123456',
+          '389123456',
+          '80912345',
         ],
       },
     ];
@@ -12104,6 +11925,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',
@@ -12559,10 +12402,16 @@ describe('Validators', () => {
         locale: 'FR',
         valid: [
           '75008',
+          '44522',
+          '38499',
+          '39940',
+          '01000',
+        ],
+        invalid: [
           '44 522',
-          '98025',
           '38 499',
-          '39940',
+          '96000',
+          '98025',
         ],
       },
       {
@@ -13800,37 +13649,6 @@ describe('Validators', () => {
     });
   });
 
-  it('should validate base64URL', () => {
-    test({
-      validator: 'isBase64',
-      args: [{ urlSafe: true }],
-      valid: [
-        '',
-        'bGFkaWVzIGFuZCBnZW50bGVtZW4sIHdlIGFyZSBmbG9hdGluZyBpbiBzcGFjZQ',
-        '1234',
-        'bXVtLW5ldmVyLXByb3Vk',
-        'PDw_Pz8-Pg',
-        'VGhpcyBpcyBhbiBlbmNvZGVkIHN0cmluZw',
-      ],
-      invalid: [
-        ' AA',
-        '\tAA',
-        '\rAA',
-        '\nAA',
-        '123=',
-        'This+isa/bad+base64Url==',
-        '0K3RgtC+INC30LDQutC+0LTQuNGA0L7QstCw0L3QvdCw0Y8g0YHRgtGA0L7QutCw',
-      ],
-      error: [
-        null,
-        undefined,
-        {},
-        [],
-        42,
-      ],
-    });
-  });
-
   it('should validate date', () => {
     test({
       validator: 'isDate',
@@ -14170,6 +13988,35 @@ describe('Validators', () => {
         '009:50:01',
       ],
     });
+    test({
+      validator: 'isTime',
+      args: [{ hourFormat: 'hour24', mode: 'withOptionalSeconds' }],
+      valid: [
+        '23:59:59',
+        '00:00:00',
+        '9:50:01',
+        '00:00',
+        '23:59',
+        '9:00',
+      ],
+      invalid: [
+        '',
+        null,
+        undefined,
+        23,
+        '01:00:01 PM',
+        '13:00:',
+        '00',
+        '26',
+        '00;01',
+        '0 :09',
+        '59:59:59',
+        '24:00:00',
+        '00:59:60',
+        '99:99:99',
+        '009:50:01',
+      ],
+    });
     test({
       validator: 'isTime',
       args: [{ hourFormat: 'hour12' }],
@@ -14229,6 +14076,38 @@ describe('Validators', () => {
         '009:50:01',
       ],
     });
+    test({
+      validator: 'isTime',
+      args: [{ hourFormat: 'hour12', mode: 'withOptionalSeconds' }],
+      valid: [
+        '12:59:59 PM',
+        '2:34:45 AM',
+        '7:00:00 AM',
+        '12:59 PM',
+        '12:59 AM',
+        '01:00 PM',
+        '01:00 AM',
+        '7:00 AM',
+      ],
+      invalid: [
+        '',
+        null,
+        undefined,
+        23,
+        '01:00: 1 PM',
+        '13:00:',
+        '00',
+        '26',
+        '00;01',
+        '0 :09',
+        '59:59:59',
+        '24:00:00',
+        '00:59:60',
+        '99:99:99',
+        '9:50:01',
+        '009:50:01',
+      ],
+    });
   });
   it('should be valid license plate', () => {
     test({
diff --git a/test/validators/isAfter.test.js b/test/validators/isAfter.test.js
index d771d91..f0daf8a 100644
--- a/test/validators/isAfter.test.js
+++ b/test/validators/isAfter.test.js
@@ -27,6 +27,21 @@ describe('isAfter', () => {
       args: [{ comparisonDate: 'invalid date' }],
       invalid: ['invalid date', '2015-09-17'],
     });
+    test({
+      validator: 'isAfter',
+      args: [], // will fall back to the current date
+      valid: ['2100-08-04', new Date(Date.now() + 86400000).toString()],
+    });
+    test({
+      validator: 'isAfter',
+      args: [undefined], // will fall back to the current date
+      valid: ['2100-08-04', new Date(Date.now() + 86400000).toString()],
+    });
+    test({
+      validator: 'isAfter',
+      args: [{ comparisonDate: undefined }], // will fall back to the current date
+      valid: ['2100-08-04', new Date(Date.now() + 86400000).toString()],
+    });
   });
 
   describe('(legacy syntax)', () => {
diff --git a/test/validators/isBase64.test.js b/test/validators/isBase64.test.js
new file mode 100644
index 0000000..c007434
--- /dev/null
+++ b/test/validators/isBase64.test.js
@@ -0,0 +1,201 @@
+import { format } from 'util';
+import test from '../testFunctions';
+import validator from '../../src';
+
+describe('isBase64', () => {
+  it('should validate base64 strings with default options', () => {
+    test({
+      validator: 'isBase64',
+      valid: [
+        '',
+        'Zg==',
+        'Zm8=',
+        'Zm9v',
+        'Zm9vYg==',
+        'Zm9vYmE=',
+        'Zm9vYmFy',
+        'TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdC4=',
+        'Vml2YW11cyBmZXJtZW50dW0gc2VtcGVyIHBvcnRhLg==',
+        'U3VzcGVuZGlzc2UgbGVjdHVzIGxlbw==',
+        'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuMPNS1Ufof9EW/M98FNw' +
+        'UAKrwflsqVxaxQjBQnHQmiI7Vac40t8x7pIb8gLGV6wL7sBTJiPovJ0V7y7oc0Ye' +
+        'rhKh0Rm4skP2z/jHwwZICgGzBvA0rH8xlhUiTvcwDCJ0kc+fh35hNt8srZQM4619' +
+        'FTgB66Xmp4EtVyhpQV+t02g6NzK72oZI0vnAvqhpkxLeLiMCyrI416wHm5Tkukhx' +
+        'QmcL2a6hNOyu0ixX/x2kSFXApEnVrJ+/IxGyfyw8kf4N2IZpW5nEP847lpfj0SZZ' +
+        'Fwrd1mnfnDbYohX2zRptLy2ZUn06Qo9pkG5ntvFEPo9bfZeULtjYzIl6K8gJ2uGZ' +
+        'HQIDAQAB',
+      ],
+      invalid: [
+        '12345',
+        'Vml2YW11cyBmZXJtZtesting123',
+        'Zg=',
+        'Z===',
+        'Zm=8',
+        '=m9vYg==',
+        'Zm9vYmFy====',
+      ],
+    });
+
+    test({
+      validator: 'isBase64',
+      args: [{ urlSafe: true }],
+      valid: [
+        '',
+        'bGFkaWVzIGFuZCBnZW50bGVtZW4sIHdlIGFyZSBmbG9hdGluZyBpbiBzcGFjZQ',
+        '1234',
+        'bXVtLW5ldmVyLXByb3Vk',
+        'PDw_Pz8-Pg',
+        'VGhpcyBpcyBhbiBlbmNvZGVkIHN0cmluZw',
+      ],
+      invalid: [
+        ' AA',
+        '\tAA',
+        '\rAA',
+        '\nAA',
+        'This+isa/bad+base64Url==',
+        '0K3RgtC+INC30LDQutC+0LTQuNGA0L7QstCw0L3QvdCw0Y8g0YHRgtGA0L7QutCw',
+      ],
+      error: [
+        null,
+        undefined,
+        {},
+        [],
+        42,
+      ],
+    });
+
+    for (let i = 0, str = '', encoded; i < 1000; i++) {
+      str += String.fromCharCode(Math.random() * 26 | 97); // eslint-disable-line no-bitwise
+      encoded = Buffer.from(str).toString('base64');
+      if (!validator.isBase64(encoded)) {
+        let msg = format('validator.isBase64() failed with "%s"', encoded);
+        throw new Error(msg);
+      }
+    }
+  });
+
+  it('should validate standard Base64 with padding', () => {
+    test({
+      validator: 'isBase64',
+      args: [{ urlSafe: false, padding: true }],
+      valid: [
+        '',
+        'TWFu',
+        'TWE=',
+        'TQ==',
+        'SGVsbG8=',
+        'U29mdHdhcmU=',
+        'YW55IGNhcm5hbCBwbGVhc3VyZS4=',
+      ],
+      invalid: [
+        'TWF',
+        'TWE===',
+        'SGVsbG8@',
+        'SGVsbG8===',
+        'SGVsb G8=',
+        '====',
+      ],
+    });
+  });
+
+  it('should validate standard Base64 without padding', () => {
+    test({
+      validator: 'isBase64',
+      args: [{ urlSafe: false, padding: false }],
+      valid: [
+        '',
+        'TWFu',
+        'TWE',
+        'TQ',
+        'SGVsbG8',
+        'U29mdHdhcmU',
+        'YW55IGNhcm5hbCBwbGVhc3VyZS4',
+      ],
+      invalid: [
+        'TWE=',
+        'TQ===',
+        'SGVsbG8@',
+        'SGVsbG8===',
+        'SGVsb G8',
+        '====',
+      ],
+    });
+  });
+
+  it('should validate Base64url with padding', () => {
+    test({
+      validator: 'isBase64',
+      args: [{ urlSafe: true, padding: true }],
+      valid: [
+        '',
+        'SGVsbG8=',
+        'U29mdHdhcmU=',
+        'YW55IGNhcm5hbCBwbGVhc3VyZS4=',
+        'SGVsbG8-',
+        'SGVsbG8_',
+      ],
+      invalid: [
+        'SGVsbG8===',
+        'SGVsbG8@',
+        'SGVsb G8=',
+        '====',
+      ],
+    });
+  });
+
+  it('should validate Base64url without padding', () => {
+    test({
+      validator: 'isBase64',
+      args: [{ urlSafe: true, padding: false }],
+      valid: [
+        '',
+        'SGVsbG8',
+        'U29mdHdhcmU',
+        'YW55IGNhcm5hbCBwbGVhc3VyZS4',
+        'SGVsbG8-',
+        'SGVsbG8_',
+      ],
+      invalid: [
+        'SGVsbG8=',
+        'SGVsbG8===',
+        'SGVsbG8@',
+        'SGVsb G8',
+        '====',
+      ],
+    });
+  });
+
+  it('should handle mixed cases correctly', () => {
+    test({
+      validator: 'isBase64',
+      args: [{ urlSafe: false, padding: true }],
+      valid: [
+        '',
+        'TWFu',
+        'TWE=',
+        'TQ==',
+      ],
+      invalid: [
+        'TWE',
+        'TQ=',
+        'TQ===',
+      ],
+    });
+
+    test({
+      validator: 'isBase64',
+      args: [{ urlSafe: true, padding: false }],
+      valid: [
+        '',
+        'SGVsbG8',
+        'SGVsbG8-',
+        'SGVsbG8_',
+      ],
+      invalid: [
+        'SGVsbG8=',
+        'SGVsbG8@',
+        'SGVsb G8',
+      ],
+    });
+  });
+});
diff --git a/test/validators/isBefore.test.js b/test/validators/isBefore.test.js
new file mode 100644
index 0000000..298e5b4
--- /dev/null
+++ b/test/validators/isBefore.test.js
@@ -0,0 +1,119 @@
+import { describe } from 'mocha';
+import test from '../testFunctions';
+
+describe('isBefore', () => {
+  describe('should validate dates a given end date', () => {
+    describe('new syntax', () => {
+      test({
+        validator: 'isBefore',
+        args: [{ comparisonDate: '08/04/2011' }],
+        valid: ['2010-07-02', '2010-08-04', new Date(0).toString()],
+        invalid: ['08/04/2011', new Date(2011, 9, 10).toString()],
+      });
+      test({
+        validator: 'isBefore',
+        args: [{ comparisonDate: new Date(2011, 7, 4).toString() }],
+        valid: ['2010-07-02', '2010-08-04', new Date(0).toString()],
+        invalid: ['08/04/2011', new Date(2011, 9, 10).toString()],
+      });
+      test({
+        validator: 'isBefore',
+        args: [{ comparisonDate: '2011-08-03' }],
+        valid: ['1999-12-31'],
+        invalid: ['invalid date'],
+      });
+      test({
+        validator: 'isBefore',
+        args: [{ comparisonDate: 'invalid date' }],
+        invalid: ['invalid date', '1999-12-31'],
+      });
+    });
+
+    describe('legacy syntax', () => {
+      test({
+        validator: 'isBefore',
+        args: ['08/04/2011'],
+        valid: ['2010-07-02', '2010-08-04', new Date(0).toString()],
+        invalid: ['08/04/2011', new Date(2011, 9, 10).toString()],
+      });
+      test({
+        validator: 'isBefore',
+        args: [new Date(2011, 7, 4).toString()],
+        valid: ['2010-07-02', '2010-08-04', new Date(0).toString()],
+        invalid: ['08/04/2011', new Date(2011, 9, 10).toString()],
+      });
+      test({
+        validator: 'isBefore',
+        args: ['2011-08-03'],
+        valid: ['1999-12-31'],
+        invalid: ['invalid date'],
+      });
+      test({
+        validator: 'isBefore',
+        args: ['invalid date'],
+        invalid: ['invalid date', '1999-12-31'],
+      });
+    });
+  });
+
+  describe('should validate dates a default end date', () => {
+    describe('new syntax', () => {
+      test({
+        validator: 'isBefore',
+        valid: [
+          '2000-08-04',
+          new Date(0).toString(),
+          new Date(Date.now() - 86400000).toString(),
+        ],
+        invalid: ['2100-07-02', new Date(2217, 10, 10).toString()],
+      });
+      test({
+        validator: 'isBefore',
+        args: undefined, // will fall back to the current date
+        valid: ['1999-06-07'],
+      });
+      test({
+        validator: 'isBefore',
+        args: [], // will fall back to the current date
+        valid: ['1999-06-07'],
+      });
+      test({
+        validator: 'isBefore',
+        args: [undefined], // will fall back to the current date
+        valid: ['1999-06-07'],
+      });
+      test({
+        validator: 'isBefore',
+        args: [{ comparisonDate: undefined }], // will fall back to the current date
+        valid: ['1999-06-07'],
+      });
+    });
+
+    describe('legacy syntax', () => {
+      test({
+        validator: 'isBefore',
+        valid: [
+          '2000-08-04',
+          new Date(0).toString(),
+          new Date(Date.now() - 86400000).toString(),
+        ],
+        invalid: ['2100-07-02', new Date(2217, 10, 10).toString()],
+      });
+      test({
+        validator: 'isBefore',
+        args: undefined, // will fall back to the current date
+        valid: ['1999-06-07'],
+      });
+      test({
+        validator: 'isBefore',
+        args: [], // will fall back to the current date
+        valid: ['1999-06-07'],
+      });
+      test({
+        validator: 'isBefore',
+        args: [undefined], // will fall back to the current date
+        valid: ['1999-06-07'],
+      });
+    });
+  });
+});
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',
+        ],
+      });
+    });
+  });
+});