Repository: icebob/fastest-validator
Pull request #178 (2020-07-17 22:01:14)

[feature] added rule for currency validation
 Regarding [this](https://github.com/icebob/fastest-validator/issues/110) feature request.


coveralls: 
[![Coverage Status](https://coveralls.io/builds/32155949/badge)](https://coveralls.io/builds/32155949)

Coverage increased (+0.003%) to 99.787% when pulling **cfbd71535a924fce7dfceb2e0e99c0c79f1492cf on ishan-srivastava:master** into **55d3e68c73974db94edebeba243bdbf7f11b2956 on icebob:master**.

icebob: @ishan-srivastava thank you for your job on this PR.
I see this rule can cover the familiar currencies like, $, €, £ but not all.
In my country, the format of currency is: `1 234,56Ft`
As you see, the thousand separator is space, the decimal separator is a comma and the sign at the end and two characters. Fully different, I know :)

![image](https://user-images.githubusercontent.com/306521/87848199-1b079c80-c8de-11ea-95d7-d9ac5e32ce71.png)

Since you are using regex for testing, I think it would be hard to cover all types. The easier way can be that if we can set different checker regexp in the rule params. What do you think @ishan-srivastava @erfanium ?

ishan-srivastava: Sure @icebob , that makes sense.
For what I can see is, that the validation logic can be changed by these params.
1. Currency ~~symbol~~ **string** [ having options to make it optional, and case insensitive.]
2. The currency is in prefix or postfix [default to prefix]
3. String to allow between the Currency and digits in the format of 
     a.) {**character**, minimum occurrences allowed, maximum occurrences allowed (_leave blank for infinity_) }
     b.) false if no String is allowed between the currency and digits.
4. Character for decimal [deafults to a '.'].
5. Character for thousand separator [deafults to a ',']

What we can also do is to extend the regex for the end-user to provide (in the schema), and that would override all the inbuilt validations the currency validator does, and would simply check the provided value against the regex provided by the user.

What do you think @icebob @erfanium ?
icebob: Yeah, and please support negative currencies as well. I didn't see tests for negative values.
erfanium: > What do you think @ishan-srivastava @erfanium ?

This is a good idea, ‌But I don't think we need to support all currencies, We can first support some important currencies and And wait for user issues/PRs 🙂
ishan-srivastava: @erfanium sure.
I think that in order to support more currencies, we would need to add more and more logic on top of existing logic, which won't be scalable.
Like you said : Adding a param that takes in a custom currency validator name, eg currency_rial / currency_hungarian_forint, sounds good to me.
The current logic would support all the currencies that have 
a.) ',' as the thousand separator.
b.) '.' as decimal separator.
c.) Currency symbol is a single character, prefixed before the digits

Should I go ahead and add the changes I suggested, or just simply support -ve values and update the PR?
icebob: As @erfanium said, we don't need to support all currencies. So as you described, just add some property like thousand separator and decimal separator and a custom regexp pattern. So if I want to check Hungarian forint with this rule, I will define a custom regexp and set it to the rule.
ishan-srivastava: > Just add some property like thousand separator and decimal separator and a custom regexp pattern. 

Sure, I'll add that in this week, day job has turned v hectic :p


Patch:
diff --git a/README.md b/README.md
index 6ad064b..f7ff3be 100644
--- a/README.md
+++ b/README.md
@@ -70,33 +70,34 @@ $ npm run bench
     - [Properties](#properties-1)
   - [`class`](#class)
     - [Properties](#properties-2)
-  - [`date`](#date)
+  - [`currency`](#currency)
     - [Properties](#properties-3)
-  - [`email`](#email)
+  - [`date`](#date)
     - [Properties](#properties-4)
-  - [`enum`](#enum)
+  - [`email`](#email)
     - [Properties](#properties-5)
-  - [`equal`](#equal)
+  - [`enum`](#enum)
     - [Properties](#properties-6)
-  - [`forbidden`](#forbidden)
+  - [`equal`](#equal)
     - [Properties](#properties-7)
+  - [`forbidden`](#forbidden)
+    - [Properties](#properties-8)
   - [`function`](#function)
   - [`luhn`](#luhn)
   - [`mac`](#mac)
   - [`multi`](#multi)
   - [`number`](#number)
-    - [Properties](#properties-8)
-  - [`object`](#object)
     - [Properties](#properties-9)
-  - [`string`](#string)
+  - [`object`](#object)
     - [Properties](#properties-10)
-  - [`tuple`](#tuple)
+  - [`string`](#string)
     - [Properties](#properties-11)
-  - [`url`](#url)
+  - [`tuple`](#tuple)
     - [Properties](#properties-12)
-  - [`uuid`](#uuid)
+  - [`url`](#url)
     - [Properties](#properties-13)
   - [`objectID`](#objectID)
+  - [`uuid`](#uuid)
     - [Properties](#properties-14)
 - [Custom validator](#custom-validator)
   - [Custom validation for built-in rules](#custom-validation-for-built-in-rules)
@@ -537,6 +538,33 @@ Property | Default  | Description
 -------- | -------- | -----------
 `instanceOf` | `null` | Checked Class.
 
+## `currency`
+This is a `Currency` validator to check if the value is a valid currency string.
+
+```js
+const schema = {
+    money_amount: { type: "currency", currencySymbol: '$' }
+}
+
+v.validate({ money_amount: '$12.99'}, schema); // Valid
+v.validate({ money_amount: '$0.99'}, schema); // Valid
+v.validate({ money_amount: '$12,345.99'}, schema); // Valid
+v.validate({ money_amount: '$123,456.99'}, schema); // Valid
+
+v.validate({ money_amount: '$1234,567.99'}, schema); // Fail
+v.validate({ money_amount: '$1,23,456.99'}, schema); // Fail
+v.validate({ money_amount: '$12,34.5.99' }, schema); // Fail
+```
+
+### Properties
+Property | Default  | Description
+-------- | -------- | -----------
+`currencySymbol` | `null` | The currency symbol expected in string (as prefix).
+`symbolOptional` | `false` | Toggle to make the symbol optional in string, although, if present it would only allow the currencySymbol.
+`thousandSeparator` | `,` | Thousand place separator character.
+`decimalSeparator` | `.` | Decimal place character.
+`customRegex` | `null` | Custom regular expression, to validate currency strings (For eg:  /[0-9]*/g).
+
 ## `date`
 This is a `Date` validator.
 
diff --git a/lib/messages.js b/lib/messages.js
index 039175d..afee092 100644
--- a/lib/messages.js
+++ b/lib/messages.js
@@ -42,6 +42,8 @@ module.exports = {
 
 	boolean: "The '{field}' field must be a boolean.",
 
+	currency: "The '{field}' must be a valid currency format",
+
 	date: "The '{field}' field must be a Date.",
 	dateMin: "The '{field}' field must be greater than or equal to {expected}.",
 	dateMax: "The '{field}' field must be less than or equal to {expected}.",
diff --git a/lib/rules/currency.js b/lib/rules/currency.js
new file mode 100644
index 0000000..86ba795
--- /dev/null
+++ b/lib/rules/currency.js
@@ -0,0 +1,27 @@
+"use strict";
+const CURRENCY_REGEX = `(?=.*\\d)^(-?~1|~1-?)(([0-9]\\d{0,2}(~2\\d{3})*)|0)?(\\~3\\d{1,2})?$`;
+/**	Signature: function(value, field, parent, errors, context)
+ */
+
+module.exports = function ({schema, messages}, path, context) {
+	const currencySymbol = schema.currencySymbol || null;
+	const thousandSeparator = schema.thousandSeparator || ',';
+	const decimalSeparator = schema.decimalSeparator || '.';
+	const customRegex = schema.customRegex;
+	let isCurrencySymbolMandatory = !schema.symbolOptional;
+	let finalRegex = CURRENCY_REGEX.replace(/~1/g, currencySymbol ? (`\\${currencySymbol}${(isCurrencySymbolMandatory ? '' : '?')}`) : '')
+		.replace('~2', thousandSeparator)
+		.replace('~3', decimalSeparator);
+	return {
+		source: `
+			if (!value.match(${customRegex || new RegExp(finalRegex)})){
+			return ${this.makeError({
+			type: "currency",
+			actual: "value",
+			messages
+		})}
+			}
+			return value;
+		`
+	};
+};
diff --git a/lib/validator.js b/lib/validator.js
index 241998a..254b678 100644
--- a/lib/validator.js
+++ b/lib/validator.js
@@ -14,6 +14,7 @@ function loadRules() {
 		boolean: require("./rules/boolean"),
 		class: require("./rules/class"),
 		custom: require("./rules/custom"),
+		currency: require("./rules/currency"),
 		date: require("./rules/date"),
 		email: require("./rules/email"),
 		enum: require("./rules/enum"),
@@ -392,12 +393,12 @@ class Validator {
 		const ruleVName = "rule" + ruleIndex;
 		const fnCustomErrorsVName = "fnCustomErrors" + ruleIndex;
 		if (typeof schema[fnName] == "function") {
-			if (context.customs[ruleIndex]) { 
+			if (context.customs[ruleIndex]) {
 				context.customs[ruleIndex].messages = messages;
 				context.customs[ruleIndex].schema = schema;
 			}
 			else context.customs[ruleIndex] = { messages, schema };
-                  
+
 			if (this.opts.useNewCustomCheckerFunction) {
 				return `
                const ${ruleVName} = context.customs[${ruleIndex}];


Test patch:
diff --git a/test/rules/currency.spec.js b/test/rules/currency.spec.js
new file mode 100644
index 0000000..57aa2e8
--- /dev/null
+++ b/test/rules/currency.spec.js
@@ -0,0 +1,65 @@
+"use strict";
+
+const Validator = require("../../lib/validator");
+const v = new Validator();
+
+describe("Test rule: currency", () => {
+	it("should have decimal optional, and correctly placed if present", () => {
+		const check = v.compile({$$root: true, type: "currency", 'currencySymbol': '$', 'symbolOptional': true});
+		expect(check("$12.2")).toEqual(true);
+		expect(check("$12,222.2")).toEqual(true);
+		expect(check("$12,222")).toEqual(true);
+		expect(check("$12,222.0")).toEqual(true);
+		expect(check("$1.22.00")).toEqual([{"actual": "$1.22.00", "field": undefined, "message": "The '' must be a valid currency format", "type": "currency"}]);
+	});
+
+	it("should check thousand separator placement is correct", () => {
+		const check = v.compile({$$root: true, type: "currency", 'currencySymbol': '$', 'symbolOptional': true});
+		expect(check("$12.2")).toEqual(true);
+		expect(check("$12,222.2")).toEqual(true);
+		expect(check("$122,222.2")).toEqual(true);
+		expect(check("$1234,222.2")).toEqual([{"actual": "$1234,222.2", "field": undefined, "message": "The '' must be a valid currency format", "type": "currency"}]);
+		expect(check("$1,2,222")).toEqual( [{"actual": "$1,2,222", "field": undefined, "message": "The '' must be a valid currency format", "type": "currency"}]);
+	});
+
+	it("should not allow any currency symbol , if not supplied in schema", () => {
+		let check = v.compile({$$root: true, type: "currency"});
+		expect(check("12.2")).toEqual(true);
+		expect(check("$12.2")).toEqual( [{"actual": "$12.2", "field": undefined, "message": "The '' must be a valid currency format", "type": "currency"}]);
+	});
+
+	it("should not allow any other currency symbol, other than supplied in schema", () => {
+		let check = v.compile({$$root: true, type: "currency", 'currencySymbol': '$', 'symbolOptional': false});
+		expect(check("$12.2")).toEqual(true);
+		expect(check("#12.2")).toEqual([{"actual": "#12.2", "field": undefined, "message": "The '' must be a valid currency format", "type": "currency"}]);
+	});
+
+	it("should keep currency symbol optional, if symbolOptional is true in schema", () => {
+		let check = v.compile({$$root: true, type: "currency", 'currencySymbol': '$', 'symbolOptional': true});
+		expect(check("$12.2")).toEqual(true);
+		expect(check("12.2")).toEqual(true);
+		expect(check("#12.2")).toEqual([{"actual": "#12.2", "field": undefined, "message": "The '' must be a valid currency format", "type": "currency"}]
+		);
+	});
+
+	it("should allow negative currencies", () => {
+		let check = v.compile({$$root: true, type: "currency", 'currencySymbol': '$', 'symbolOptional': true});
+		expect(check("-12.2")).toEqual(true);
+		expect(check("$-12.2")).toEqual(true);
+		expect(check("-$12.2")).toEqual(true);
+		expect(check("-$-12.2")).toEqual([{"actual": "-$-12.2", "field": undefined, "message": "The '' must be a valid currency format", "type": "currency"}]);
+	});
+
+	it("should work correctly with supplied thousand and decimal separator", () => {
+		let check = v.compile({$$root: true, type: "currency", 'currencySymbol': '$', 'symbolOptional': true, 'thousandSeparator':'.', 'decimalSeparator':','});
+		expect(check("$12,2")).toEqual(true);
+		expect(check("$12.222")).toEqual(true);
+		expect(check("$12.222,2")).toEqual(true);
+		expect(check("$12,222.2")).toEqual([{"actual": "$12,222.2", "field": undefined, "message": "The '' must be a valid currency format", "type": "currency"}]);
+	});
+	it("should work correctly with supplied regex pattern", () => {
+		let check = v.compile({$$root: true, type: "currency", 'customRegex': /123/g});
+		expect(check("123")).toEqual(true);
+		expect(check("134")).toEqual([{"actual": "134", "field": undefined, "message": "The '' must be a valid currency format", "type": "currency"}]);
+	});
+});