Skip to content

Commit 743ff3d

Browse files
author
Sascha Goldhofer
committed
fix: #43 floating point issue in multipleOf validation
- fix multipleOf validation logic - remove floatingPointPrecision setting
1 parent d6a307c commit 743ff3d

File tree

12 files changed

+151
-16
lines changed

12 files changed

+151
-16
lines changed

dist/jsonSchemaLibrary.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/lib/config/settings.d.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
declare const _default: {
22
DECLARATOR_ONEOF: string;
33
GET_TEMPLATE_RECURSION_LIMIT: number;
4-
floatingPointPrecision: number;
54
propertyBlacklist: string[];
65
templateDefaultOptions: {
76
addOptionalProps: boolean;

dist/lib/utils/getPrecision.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/**
2+
* returns the floating point precision of a decimal number or 0
3+
*/
4+
export declare function getPrecision(value: number): number;

dist/module/lib/config/settings.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
export default {
22
DECLARATOR_ONEOF: "oneOfProperty",
33
GET_TEMPLATE_RECURSION_LIMIT: 1,
4-
floatingPointPrecision: 10000,
54
propertyBlacklist: ["_id"],
65
templateDefaultOptions: {
76
addOptionalProps: false,

dist/module/lib/utils/getPrecision.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/**
2+
* returns the floating point precision of a decimal number or 0
3+
*/
4+
export function getPrecision(value) {
5+
const string = `${value}`;
6+
const index = string.indexOf(".");
7+
return index === -1 ? 0 : string.length - (index + 1);
8+
}

dist/module/lib/validation/keyword.js

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import { validateAllOf } from "../features/allOf";
77
import { validateAnyOf } from "../features/anyOf";
88
import { validateDependencies } from "../features/dependencies";
99
import { validateOneOf } from "../features/oneOf";
10+
import { getPrecision } from "../utils/getPrecision";
1011
import deepEqual from "fast-deep-equal";
11-
const FPP = settings.floatingPointPrecision;
1212
const hasOwnProperty = Object.prototype.hasOwnProperty;
1313
const hasProperty = (value, property) => !(value[property] === undefined || !hasOwnProperty.call(value, property));
1414
// list of validation keywords: http://json-schema.org/latest/json-schema-validation.html#rfc.section.5
@@ -283,12 +283,24 @@ const KeywordValidation = {
283283
return undefined;
284284
},
285285
multipleOf: (draft, schema, value, pointer) => {
286-
if (isNaN(schema.multipleOf)) {
286+
if (isNaN(schema.multipleOf) || typeof value !== "number") {
287287
return undefined;
288288
}
289-
// https://github.com/cfworker/cfworker/blob/master/packages/json-schema/src/validate.ts#L1061
290-
// https://github.com/ExodusMovement/schemasafe/blob/master/src/compile.js#L441
291-
if (((value * FPP) % (schema.multipleOf * FPP)) / FPP !== 0) {
289+
const valuePrecision = getPrecision(value);
290+
const multiplePrecision = getPrecision(schema.multipleOf);
291+
if (valuePrecision > multiplePrecision) {
292+
// higher precision of value can never be multiple of value
293+
return draft.errors.multipleOfError({
294+
multipleOf: schema.multipleOf,
295+
value,
296+
pointer,
297+
schema
298+
});
299+
}
300+
const precision = Math.pow(10, multiplePrecision);
301+
const val = Math.round(value * precision);
302+
const multiple = Math.round(schema.multipleOf * precision);
303+
if ((val % multiple) / precision !== 0) {
292304
return draft.errors.multipleOfError({
293305
multipleOf: schema.multipleOf,
294306
value,

lib/config/settings.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
export default {
22
DECLARATOR_ONEOF: "oneOfProperty",
33
GET_TEMPLATE_RECURSION_LIMIT: 1,
4-
floatingPointPrecision: 10000,
54
propertyBlacklist: ["_id"],
65
templateDefaultOptions: {
76
addOptionalProps: false,

lib/getChildSchemaSelection.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ export default function getChildSchemaSelection(
2424
}
2525

2626
const result = draft.step(property, schema, {}, "#");
27-
2827
if (isJsonError(result)) {
2928
return result;
3029
}

lib/utils/getPrecision.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/**
2+
* returns the floating point precision of a decimal number or 0
3+
*/
4+
export function getPrecision(value: number): number {
5+
const string = `${value}`;
6+
const index = string.indexOf(".");
7+
return index === -1 ? 0 : string.length - (index + 1);
8+
}

lib/validation/keyword.ts

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import { validateAllOf } from "../features/allOf";
77
import { validateAnyOf } from "../features/anyOf";
88
import { validateDependencies } from "../features/dependencies";
99
import { validateOneOf } from "../features/oneOf";
10+
import { getPrecision } from "../utils/getPrecision";
1011
import deepEqual from "fast-deep-equal";
11-
const FPP = settings.floatingPointPrecision;
1212

1313
const hasOwnProperty = Object.prototype.hasOwnProperty;
1414
const hasProperty = (value: Record<string, unknown>, property: string) =>
@@ -316,20 +316,36 @@ const KeywordValidation: Record<string, JsonValidator> = {
316316
return undefined;
317317
},
318318
multipleOf: (draft, schema, value: number, pointer) => {
319-
if (isNaN(schema.multipleOf)) {
319+
if (isNaN(schema.multipleOf) || typeof value !== "number") {
320320
return undefined;
321321
}
322-
// https://github.com/cfworker/cfworker/blob/master/packages/json-schema/src/validate.ts#L1061
323-
// https://github.com/ExodusMovement/schemasafe/blob/master/src/compile.js#L441
324-
if (((value * FPP) % (schema.multipleOf * FPP)) / FPP !== 0) {
322+
323+
const valuePrecision = getPrecision(value);
324+
const multiplePrecision = getPrecision(schema.multipleOf);
325+
if (valuePrecision > multiplePrecision) {
326+
// value with higher precision then multipleOf-precision can never be multiple
327+
return draft.errors.multipleOfError({
328+
multipleOf: schema.multipleOf,
329+
value,
330+
pointer,
331+
schema
332+
});
333+
}
334+
335+
const precision = Math.pow(10, multiplePrecision);
336+
const val = Math.round(value * precision);
337+
const multiple = Math.round(schema.multipleOf * precision);
338+
if ((val % multiple) / precision !== 0) {
325339
return draft.errors.multipleOfError({
326340
multipleOf: schema.multipleOf,
327341
value,
328342
pointer,
329343
schema
330344
});
331345
}
332-
// also check https://stackoverflow.com/questions/1815367/catch-and-compute-overflow-during-multiplication-of-two-large-integers
346+
347+
// maybe also check overflow
348+
// https://stackoverflow.com/questions/1815367/catch-and-compute-overflow-during-multiplication-of-two-large-integers
333349
return undefined;
334350
},
335351
not: (draft, schema, value, pointer) => {
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { expect } from "chai";
2+
import { Draft07 as Draft } from "../../../lib/draft07";
3+
4+
describe("issue#43 - multipleOf .01", () => {
5+
let draft: Draft;
6+
beforeEach(() => {
7+
draft = new Draft({
8+
type: "number",
9+
multipleOf: 0.01
10+
});
11+
});
12+
13+
it("should validate 1", () => {
14+
const result = draft.validate(1);
15+
expect(result).to.have.length(0);
16+
});
17+
18+
it("should validate .2", () => {
19+
const result = draft.validate(0.2);
20+
expect(result).to.have.length(0);
21+
});
22+
23+
it("should validate .02", () => {
24+
const result = draft.validate(0.02);
25+
expect(result).to.have.length(0);
26+
});
27+
28+
it("should not validate .025", () => {
29+
const result = draft.validate(0.025);
30+
expect(result).to.have.length(1);
31+
});
32+
33+
it("should validate 1.36", () => {
34+
const result = draft.validate(1.36);
35+
expect(result).to.have.length(0);
36+
});
37+
38+
it("should validate 2.74", () => {
39+
const result = draft.validate(2.74);
40+
expect(result).to.have.length(0);
41+
});
42+
43+
it("should validate 123456789", () => {
44+
const result = draft.validate(123456789);
45+
expect(result).to.have.length(0);
46+
});
47+
48+
it("should not validate Infinity", () => {
49+
const result = draft.validate(1e308);
50+
expect(result).to.have.length(1);
51+
});
52+
53+
it("should validate all floats with two decimals", () => {
54+
for (let i = 0; i <= 100; i++) {
55+
let num = `2.${i}`;
56+
expect(draft.validate(parseFloat(num))).to.have.length(
57+
0,
58+
`should have validated '${num}'`
59+
);
60+
}
61+
});
62+
63+
it("should still validate multiple of integers", () => {
64+
draft = new Draft({
65+
type: "number",
66+
multipleOf: 3
67+
});
68+
const result = draft.validate(9);
69+
expect(result).to.have.length(0);
70+
});
71+
72+
it("should still invalidate non-multiples of integers", () => {
73+
draft = new Draft({
74+
type: "number",
75+
multipleOf: 3
76+
});
77+
const result = draft.validate(7);
78+
expect(result).to.have.length(1);
79+
});
80+
});

test/unit/utils/getPrecision.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { expect } from "chai";
2+
import { getPrecision } from "../../../lib/utils/getPrecision";
3+
4+
describe("getPrecision", () => {
5+
it("should return decimal precision", () => {
6+
expect(getPrecision(1.1)).to.equal(1);
7+
expect(getPrecision(0.12)).to.equal(2);
8+
expect(getPrecision(0.123)).to.equal(3);
9+
expect(getPrecision(123.4567)).to.equal(4);
10+
});
11+
});

0 commit comments

Comments
 (0)