diff --git a/README.md b/README.md index cc9a97882..f6830d832 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ Validator | Description **isBtcAddress(str)** | check if the string is a valid BTC address. **isByteLength(str [, options])** | check if the string's length (in UTF-8 bytes) falls in a range.

`options` is an object which defaults to `{ min: 0, max: undefined }`. **isCreditCard(str [, options])** | check if the string is a credit card number.

`options` is an optional object that can be supplied with the following key(s): `provider` is an optional key whose value should be a string, and defines the company issuing the credit card. Valid values include `['amex', 'dinersclub', 'discover', 'jcb', 'mastercard', 'unionpay', 'visa']` or blank will check for any provider. -**isCurrency(str [, options])** | check if the string is a valid currency amount.

`options` is an object which defaults to `{ symbol: '$', require_symbol: false, allow_space_after_symbol: false, symbol_after_digits: false, allow_negatives: true, parens_for_negatives: false, negative_sign_before_digits: false, negative_sign_after_digits: false, allow_negative_sign_placeholder: false, thousands_separator: ',', decimal_separator: '.', allow_decimal: true, require_decimal: false, digits_after_decimal: [2], allow_space_after_digits: false }`.
**Note:** The array `digits_after_decimal` is filled with the exact number of digits allowed not a range, for example a range 1 to 3 will be given as [1, 2, 3]. +**isCurrency(str [, options])** | check if the string is a valid currency amount.

`options` is an object which defaults to `{ symbol: '$', require_symbol: false, allow_space_after_symbol: false, symbol_after_digits: false, allow_negatives: true, parens_for_negatives: false, negative_sign_before_digits: false, negative_sign_after_digits: false, allow_negative_sign_placeholder: false, thousands_separator: ',', decimal_separator: '.', allow_decimal: true, require_decimal: false, digits_after_decimal: [2], allow_space_after_digits: false, thousands_separator_mode: 'allowed' }`.
**Note:** The array `digits_after_decimal` is filled with the exact number of digits allowed not a range, for example a range 1 to 3 will be given as [1, 2, 3].
**Note:** `thousands_separator_mode` accepts `'required'` (amounts >= 1000 must have separators), `'allowed'` (default, separators optional), or `'forbidden'` (separators not allowed). **isDataURI(str)** | check if the string is a [data uri format][Data URI Format]. **isDate(str [, options])** | check if the string is a valid date. e.g. [`2002-07-15`, new Date()].

`options` is an object which can contain the keys `format`, `strictMode` and/or `delimiters`.

`format` is a string and defaults to `YYYY/MM/DD`.

`strictMode` is a boolean and defaults to `false`. If `strictMode` is set to true, the validator will reject strings different from `format`.

`delimiters` is an array of allowed date delimiters and defaults to `['/', '-']`. **isDecimal(str [, options])** | check if the string represents a decimal number, such as 0.1, .3, 1.1, 1.00003, 4.0, etc.

`options` is an object which defaults to `{force_decimal: false, decimal_digits: '1,', locale: 'en-US'}`.

`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', 'el-GR', 'en-AU', 'en-GB', 'en-HK', 'en-IN', 'en-NZ', 'en-US', 'en-ZA', 'en-ZM', 'eo', 'es-ES', 'fa', 'fa-AF', 'fa-IR', 'fr-FR', 'fr-CA', 'hu-HU', 'id-ID', 'it-IT', 'ku-IQ', 'nb-NO', 'nl-NL', 'nn-NO', 'pl-PL', 'pl-Pl', 'pt-BR', 'pt-PT', 'ru-RU', 'sl-SI', 'sr-RS', 'sr-RS@latin', 'sv-SE', 'tr-TR', 'uk-UA', 'vi-VN']`.
**Note:** `decimal_digits` is given as a range like '1,3', a specific value like '3' or min like '1,'. diff --git a/src/lib/isCurrency.js b/src/lib/isCurrency.js index 54b1f7c7c..1ffc38df0 100755 --- a/src/lib/isCurrency.js +++ b/src/lib/isCurrency.js @@ -1,6 +1,28 @@ import merge from './util/merge'; import assertString from './util/assertString'; +/** + * Returns valid whole dollar amount patterns based on thousands_separator_mode. + * @param {string} mode - 'required', 'allowed', or 'forbidden' + * @param {string} withoutSep - regex pattern for amounts without separators + * @param {string} withSep - regex pattern for amounts with separators + * @returns {string[]} array of valid regex patterns + */ +function getValidWholeAmounts(mode, withoutSep, withSep) { + switch (mode) { + case 'required': + // For amounts >= 1000, require separators. Amounts 0-999 don't need them. + return ['0', '[1-9]\\d{0,2}', withSep]; + case 'forbidden': + // Never allow thousands separators + return ['0', withoutSep]; + case 'allowed': + default: + // Allow both with and without separators (current default behavior) + return ['0', withoutSep, withSep]; + } +} + function currencyRegex(options) { let decimal_digits = `\\d{${options.digits_after_decimal[0]}}`; options.digits_after_decimal.forEach((digit, index) => { if (index !== 0) decimal_digits = `${decimal_digits}|\\d{${digit}}`; }); @@ -10,8 +32,11 @@ function currencyRegex(options) { negative = '-?', whole_dollar_amount_without_sep = '[1-9]\\d*', whole_dollar_amount_with_sep = `[1-9]\\d{0,2}(\\${options.thousands_separator}\\d{3})*`, - valid_whole_dollar_amounts = [ - '0', whole_dollar_amount_without_sep, whole_dollar_amount_with_sep], + valid_whole_dollar_amounts = getValidWholeAmounts( + options.thousands_separator_mode, + whole_dollar_amount_without_sep, + whole_dollar_amount_with_sep + ), whole_dollar_amount = `(${valid_whole_dollar_amounts.join('|')})?`, decimal_amount = `(\\${options.decimal_separator}(${decimal_digits}))${options.require_decimal ? '' : '?'}`; let pattern = whole_dollar_amount + (options.allow_decimal || options.require_decimal ? decimal_amount : ''); @@ -70,6 +95,7 @@ const default_currency_options = { require_decimal: false, digits_after_decimal: [2], allow_space_after_digits: false, + thousands_separator_mode: 'allowed', }; export default function isCurrency(str, options) { diff --git a/test/validators.test.js b/test/validators.test.js index 60ffa9c81..fede927b8 100644 --- a/test/validators.test.js +++ b/test/validators.test.js @@ -12069,6 +12069,114 @@ describe('Validators', () => { '$R 1.400,00', ], }); + + // thousands_separator_mode: 'required' (issue #912) + test({ + validator: 'isCurrency', + args: [ + { + thousands_separator_mode: 'required', + }, + ], + valid: [ + '$10,123.45', + '-$10,123.45', + '$1,234.56', + '1,234.56', + '$1,234,567.89', + '1,234,567.89', + '$100.00', + '100.00', + '$10.00', + '10.00', + '$1.00', + '1.00', + '$0.50', + '0.50', + '.50', + '$.50', + '0', + '$0', + '999', + '$999', + '999.99', + '$999.99', + ], + invalid: [ + '1234.56', + '$1234.56', + '1234', + '$1234', + '10000', + '$10000', + '1234567.89', + '$1234567.89', + '10000.00', + '$10000.00', + ], + }); + + // thousands_separator_mode: 'required' with European format + test({ + validator: 'isCurrency', + args: [ + { + thousands_separator_mode: 'required', + thousands_separator: '.', + decimal_separator: ',', + symbol: '€', + symbol_after_digits: true, + }, + ], + valid: [ + '10.123,45€', + '1.234,56€', + '1.234.567,89€', + '100,00€', + '10,00€', + '1,00€', + '0,50€', + '999€', + '999,99€', + ], + invalid: [ + '1234,56€', + '1234€', + '10000€', + '1234567,89€', + ], + }); + + // thousands_separator_mode: 'forbidden' + test({ + validator: 'isCurrency', + args: [ + { + thousands_separator_mode: 'forbidden', + }, + ], + valid: [ + '$1234.56', + '1234.56', + '$1234567.89', + '1234567.89', + '$100.00', + '100.00', + '$0.50', + '0.50', + '0', + '$0', + '999', + '$999', + ], + invalid: [ + '$1,234.56', + '1,234.56', + '$1,234,567.89', + '1,234,567.89', + '$10,123.45', + ], + }); }); it('should validate Ethereum addresses', () => {