From 71f95eccbb6778932fbcf4aa3280e82fa27d14b5 Mon Sep 17 00:00:00 2001 From: abhu85 <60182103+abhu85@users.noreply.github.com> Date: Tue, 24 Feb 2026 11:43:57 +0000 Subject: [PATCH 1/2] feat(isCurrency): add require_thousands_separator option Add new option `require_thousands_separator` to enforce thousands separators in currency values. When enabled, amounts of 1000 or greater must include thousands separators (e.g., "1,234" is valid but "1234" is invalid). This is useful for validating financial data where comma separators must always be present. Fixes #912 Co-Authored-By: Claude Opus 4.6 --- README.md | 2 +- src/lib/isCurrency.js | 6 ++-- test/validators.test.js | 77 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index cc9a97882..ccc141fed 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, require_thousands_separator: 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]. **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..3b4eeac97 100755 --- a/src/lib/isCurrency.js +++ b/src/lib/isCurrency.js @@ -10,8 +10,9 @@ 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 = options.require_thousands_separator + ? ['0', '[1-9]\\d{0,2}', whole_dollar_amount_with_sep] + : ['0', 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 +71,7 @@ const default_currency_options = { require_decimal: false, digits_after_decimal: [2], allow_space_after_digits: false, + require_thousands_separator: false, }; export default function isCurrency(str, options) { diff --git a/test/validators.test.js b/test/validators.test.js index 60ffa9c81..189c363f1 100644 --- a/test/validators.test.js +++ b/test/validators.test.js @@ -12069,6 +12069,83 @@ describe('Validators', () => { '$R 1.400,00', ], }); + + // require_thousands_separator option (issue #912) + test({ + validator: 'isCurrency', + args: [ + { + require_thousands_separator: true, + }, + ], + 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', + ], + }); + + // require_thousands_separator with different separator + test({ + validator: 'isCurrency', + args: [ + { + require_thousands_separator: true, + 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€', + ], + }); }); it('should validate Ethereum addresses', () => { From f54a1b198a4445741a8abad8863c0401f2e2a363 Mon Sep 17 00:00:00 2001 From: abhu85 Date: Tue, 3 Mar 2026 18:31:51 +0000 Subject: [PATCH 2/2] refactor(isCurrency): replace boolean with three-state thousands_separator_mode Replace `require_thousands_separator: boolean` with `thousands_separator_mode` that accepts three values: 'required', 'allowed', 'forbidden'. This follows the API design suggested by @WikiRik in issue #912 to consolidate allow_* and require_* patterns into a single option with explicit states. Changes: - thousands_separator_mode: 'required' - amounts >= 1000 must have separators - thousands_separator_mode: 'allowed' - separators optional (default, backwards compatible) - thousands_separator_mode: 'forbidden' - separators not allowed Fixes #912 Co-Authored-By: Claude Opus 4.6 --- README.md | 2 +- src/lib/isCurrency.js | 32 ++++++++++++++++++++++++++++---- test/validators.test.js | 39 +++++++++++++++++++++++++++++++++++---- 3 files changed, 64 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index ccc141fed..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, require_thousands_separator: 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 3b4eeac97..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,9 +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 = options.require_thousands_separator - ? ['0', '[1-9]\\d{0,2}', whole_dollar_amount_with_sep] - : ['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 : ''); @@ -71,7 +95,7 @@ const default_currency_options = { require_decimal: false, digits_after_decimal: [2], allow_space_after_digits: false, - require_thousands_separator: false, + thousands_separator_mode: 'allowed', }; export default function isCurrency(str, options) { diff --git a/test/validators.test.js b/test/validators.test.js index 189c363f1..fede927b8 100644 --- a/test/validators.test.js +++ b/test/validators.test.js @@ -12070,12 +12070,12 @@ describe('Validators', () => { ], }); - // require_thousands_separator option (issue #912) + // thousands_separator_mode: 'required' (issue #912) test({ validator: 'isCurrency', args: [ { - require_thousands_separator: true, + thousands_separator_mode: 'required', }, ], valid: [ @@ -12116,12 +12116,12 @@ describe('Validators', () => { ], }); - // require_thousands_separator with different separator + // thousands_separator_mode: 'required' with European format test({ validator: 'isCurrency', args: [ { - require_thousands_separator: true, + thousands_separator_mode: 'required', thousands_separator: '.', decimal_separator: ',', symbol: '€', @@ -12146,6 +12146,37 @@ describe('Validators', () => { '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', () => {