diff --git a/README.md b/README.md index 4a395198..d78b82a3 100644 --- a/README.md +++ b/README.md @@ -1671,6 +1671,7 @@ Name | Default text `numberEqual` | The '{field}' field must be equal to {expected}. `numberNotEqual` | The '{field}' field can't be equal to {expected}. `numberInteger` | The '{field}' field must be an integer. +`numberStep` | The '{field}' field must be a multiple of {expected}. `numberPositive` | The '{field}' field must be a positive number. `numberNegative` | The '{field}' field must be a negative number. `array` | The '{field}' field must be an array. diff --git a/index.d.ts b/index.d.ts index 19fbe6f4..f9e9b697 100644 --- a/index.d.ts +++ b/index.d.ts @@ -351,6 +351,10 @@ export interface RuleNumber extends RuleCustom { * @default false */ integer?: boolean; + /** + * The stepping interval for the input value + */ + step?: number; /** * The value must be greater than zero * @default false @@ -731,6 +735,10 @@ export interface BuiltInMessages { * The '{field}' field must be an integer. */ numberInteger?: string; + /** + * The '{field}' field must be a multiple of {expected}. + */ + numberStep?: string; /** * The '{field}' field must be a positive number. */ diff --git a/lib/messages.js b/lib/messages.js index bb2d672a..29321ae6 100644 --- a/lib/messages.js +++ b/lib/messages.js @@ -25,6 +25,7 @@ module.exports = { numberEqual: "The '{field}' field must be equal to {expected}.", numberNotEqual: "The '{field}' field can't be equal to {expected}.", numberInteger: "The '{field}' field must be an integer.", + numberStep: "The '{field}' field must be a multiple of {expected}.", numberPositive: "The '{field}' field must be a positive number.", numberNegative: "The '{field}' field must be a negative number.", diff --git a/lib/rules/number.js b/lib/rules/number.js index 5ac898c0..589371fb 100644 --- a/lib/rules/number.js +++ b/lib/rules/number.js @@ -21,7 +21,7 @@ module.exports = function({ schema, messages }, path, context) { src.push(` if (typeof value !== "number" || isNaN(value) || !isFinite(value)) { - ${this.makeError({ type: "number", actual: "origValue", messages })} + ${this.makeError({ type: "number", actual: "origValue", messages })} return value; } `); @@ -64,16 +64,56 @@ module.exports = function({ schema, messages }, path, context) { if (schema.integer === true) { src.push(` if (value % 1 !== 0) { - ${this.makeError({ type: "numberInteger", actual: "origValue", messages })} + ${this.makeError({ type: "numberInteger", actual: "origValue", messages })} } `); } + // Check step + if (schema.step != null) { + if (schema.step <= 0 || !Number.isFinite(schema.step)) + throw new Error(`Invalid '${schema.type}' schema. The 'step' field must be a positive number.`); + + const errorSrc = this.makeError({ type: "numberStep", expected: schema.step, actual: "origValue", messages }); + + if (Number.isInteger(schema.step)) + src.push(` + if (value % ${schema.step} !== 0) { + ${errorSrc} + } + `) + else { + const stepDecimals = schema.step.toString().split('.')[1].length + const multiplier = Math.pow(10, stepDecimals); + const stepInt = Math.round(schema.step * multiplier); + + src.push(` + if (!Number.isFinite(value)) { + ${errorSrc} + } else { + const valStr = value.toString(); + const valDotIdx = valStr.indexOf('.'); + const valDecimals = valDotIdx !== -1 ? valStr.length - valDotIdx - 1 : 0; + + if (valDecimals > ${stepDecimals}) { + ${errorSrc} + } else { + const valInt = Math.round(value * ${multiplier}); + + if (valInt % ${stepInt} !== 0) { + ${errorSrc} + } + } + } + `); + } + } + // Check positive if (schema.positive === true) { src.push(` if (value <= 0) { - ${this.makeError({ type: "numberPositive", actual: "origValue", messages })} + ${this.makeError({ type: "numberPositive", actual: "origValue", messages })} } `); } @@ -82,7 +122,7 @@ module.exports = function({ schema, messages }, path, context) { if (schema.negative === true) { src.push(` if (value >= 0) { - ${this.makeError({ type: "numberNegative", actual: "origValue", messages })} + ${this.makeError({ type: "numberNegative", actual: "origValue", messages })} } `); } diff --git a/test/messages.spec.js b/test/messages.spec.js index 5f2048d8..3a523253 100644 --- a/test/messages.spec.js +++ b/test/messages.spec.js @@ -22,6 +22,7 @@ describe("Test Messages", () => { expect(msg.numberEqual).toBeDefined(); expect(msg.numberNotEqual).toBeDefined(); expect(msg.numberInteger).toBeDefined(); + expect(msg.numberStep).toBeDefined(); expect(msg.numberPositive).toBeDefined(); expect(msg.numberNegative).toBeDefined(); expect(msg.array).toBeDefined(); diff --git a/test/rules/number.spec.js b/test/rules/number.spec.js index c46a3884..3b020e79 100644 --- a/test/rules/number.spec.js +++ b/test/rules/number.spec.js @@ -82,6 +82,45 @@ describe("Test rule: number", () => { expect(check(20)).toEqual(true); }); + it("check schema's 'step' field type", () => { + const message = "Invalid 'number' schema. The 'step' field must be a positive number."; + + expect(() => v.compile({ $$root: true, type: "number", step: 0 })).toThrow(message); + expect(() => v.compile({ $$root: true, type: "number", step: -1 })).toThrow(message); + expect(() => v.compile({ $$root: true, type: "number", step: NaN })).toThrow(message); + expect(() => v.compile({ $$root: true, type: "number", step: Infinity })).toThrow(message); + }); + + it("check step", () => { + const check = v.compile({ $$root: true, type: "number", step: 10 }); + const message = "The '' field must be a multiple of 10."; + + expect(check(15)).toEqual([{ type: "numberStep", expected: 10, actual: 15, message }]); + expect(check(8.5)).toEqual([{ type: "numberStep", expected: 10, actual: 8.5, message }]); + expect(check(-5.5)).toEqual([{ type: "numberStep", expected: 10, actual: -5.5, message }]); + + expect(check(0)).toEqual(true); + expect(check(20)).toEqual(true); + expect(check(-20)).toEqual(true); + expect(check(100)).toEqual(true); + }); + + it("check step (float)", () => { + const check = v.compile({ $$root: true, type: "number", step: 0.2 }); + const message = "The '' field must be a multiple of 0.2."; + + expect(check(1.1)).toEqual([{ type: "numberStep", expected: 0.2, actual: 1.1, message }]); + expect(check(-5.5)).toEqual([{ type: "numberStep", expected: 0.2, actual: -5.5, message }]); + expect(check(0.20001)).toEqual([{ type: "numberStep", expected: 0.2, actual: 0.20001, message }]); + + expect(check(0)).toEqual(true); + expect(check(2)).toEqual(true); + expect(check(1.4)).toEqual(true); + expect(check(-20.2)).toEqual(true); + + expect(check(1.4000000001)).toEqual([{ type: "numberStep", expected: 0.2, actual: 1.4000000001, message }]); + }); + it("check positive number", () => { const check = v.compile({ $$root: true, type: "number", positive: true }); const message = "The '' field must be a positive number."; diff --git a/test/typescript/messages.spec.ts b/test/typescript/messages.spec.ts index aee94d8e..ef274194 100644 --- a/test/typescript/messages.spec.ts +++ b/test/typescript/messages.spec.ts @@ -22,6 +22,7 @@ describe("TypeScript Definitions", () => { expect(msg.numberEqual).toBeDefined(); expect(msg.numberNotEqual).toBeDefined(); expect(msg.numberInteger).toBeDefined(); + expect(msg.numberStep).toBeDefined(); expect(msg.numberPositive).toBeDefined(); expect(msg.numberNegative).toBeDefined(); expect(msg.array).toBeDefined(); diff --git a/test/typescript/rules/number.spec.ts b/test/typescript/rules/number.spec.ts index 877ee4a2..3bc5d5b0 100644 --- a/test/typescript/rules/number.spec.ts +++ b/test/typescript/rules/number.spec.ts @@ -78,6 +78,44 @@ describe('TypeScript Definitions', () => { expect(check(20)).toEqual(true); }); + it("check schema's 'step' field type", () => { + const message = "Invalid 'number' schema. The 'step' field must be a positive number."; + + expect(() => v.compile({ $$root: true, type: "number", step: 0 })).toThrow(message); + expect(() => v.compile({ $$root: true, type: "number", step: -1 })).toThrow(message); + expect(() => v.compile({ $$root: true, type: "number", step: NaN })).toThrow(message); + }); + + it('check step', () => { + const check = v.compile({ $$root: true, type: 'number', step: 10 } as RuleNumber); + const message = "The '' field must be a multiple of 10."; + + expect(check(8.5)).toEqual([{ type: "numberStep", expected: 10, actual: 8.5, message }]); + expect(check(-5.5)).toEqual([{ type: "numberStep", expected: 10, actual: -5.5, message }]); + expect(check(15)).toEqual([{ type: "numberStep", expected: 10, actual: 15, message }]); + + expect(check(0)).toEqual(true); + expect(check(-20)).toEqual(true); + expect(check(20)).toEqual(true); + expect(check(100)).toEqual(true); + }); + + it("check step (float)", () => { + const check = v.compile({ $$root: true, type: "number", step: 0.2 } as RuleNumber); + const message = "The '' field must be a multiple of 0.2."; + + expect(check(1.1)).toEqual([{ type: "numberStep", expected: 0.2, actual: 1.1, message }]); + expect(check(-5.5)).toEqual([{ type: "numberStep", expected: 0.2, actual: -5.5, message }]); + expect(check(0.20001)).toEqual([{ type: "numberStep", expected: 0.2, actual: 0.20001, message }]); + + expect(check(0)).toEqual(true); + expect(check(2)).toEqual(true); + expect(check(1.4)).toEqual(true); + expect(check(-20.2)).toEqual(true); + + expect(check(1.4000000001)).toEqual([{ type: "numberStep", expected: 0.2, actual: 1.4000000001, message }]); + }); + it('check positive number', () => { const check = v.compile({ $$root: true, type: 'number', positive: true } as RuleNumber); const message = 'The \'\' field must be a positive number.';