Skip to content

[feature] added rule for currency validation #178

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Aug 26, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 39 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.

Expand Down
2 changes: 2 additions & 0 deletions lib/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}.",
Expand Down
27 changes: 27 additions & 0 deletions lib/rules/currency.js
Original file line number Diff line number Diff line change
@@ -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;
`
};
};
5 changes: 3 additions & 2 deletions lib/validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down Expand Up @@ -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}];
Expand Down
65 changes: 65 additions & 0 deletions test/rules/currency.spec.js
Original file line number Diff line number Diff line change
@@ -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"}]);
});
});