Skip to content
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

Validation of multipleOf keyword with decimal numbers #652

Closed
manahga opened this issue Dec 20, 2017 · 14 comments
Closed

Validation of multipleOf keyword with decimal numbers #652

manahga opened this issue Dec 20, 2017 · 14 comments

Comments

@manahga
Copy link

manahga commented Dec 20, 2017

Due to the rounding issues (IEEE-754) with computations using decimal numbers in several languages including JavaScript, the validation of fields using the "multipleOf" or "divisibleBy" properties will often erroneously fail validation.

As an example, using a field with the "multipleOf" property set to 0.015, the validation will fail when the field is set to valid values such as 0.135, 0.165, 0.195, 0.225, etc. All of said values are divisible by 0.015.

This can be remedied by changing the algorithm used to validate the "multipleOf" and "divisibleBy" properties. The change involves scaling both the field and "multipleOf" values to integer equivalents and then performing the required validation on those values. Below is some code that I had written for another JSON validator which had the same issue.

//The following uses string parsing to determine the number of
//decimal places for a given number.  String parsing is used 
//because any arithmetic method used to determine decimal
//point precision is still subject to the rounding errors within
//JavaScript.  This algorithm handles numbers using exponential
//notation as well.
getDecimalPlaces = function getDecimalPlaces(number) {

    let decimalPlaces = 0;
    if (isNaN(number)) return decimalPlaces;

    if (typeof number !== 'number') {
        number = Number(number);
    }
    
    const parts = number.toString().split('e');
    if (parts.length === 2) {
        if (parts[1][0] !== '-') {
            return decimalPlaces;
        } else {
            decimalPlaces = Number(parts[1].slice(1));
        }
    }

    const decimalParts = parts[0].split('.');
    if (decimalParts.length === 2) {
        decimalPlaces += decimalParts[1].length;
    }

    return decimalPlaces;
}

//The getDecimalPlaces function above can then be used in the 
//validation process to scale the two values out to equivalent
//integer values. The integer values, which are not subject to the
//rounding errors, are then used to perform the validation.

const fieldDecimals = getDecimalPlaces(fieldValue);
const multipleOfDecimals = getDecimalPlaces(multipleOfValue);

//Ensure the values will be the same precision
const maxDecimals = Math.max(fieldDecimals , multipleOfDecimals);
const multiplier = Math.pow(10, maxDecimals);

//Convert values to integers using multiplier above and check 
//modulus for validity
if (Math.round(fieldValue * multiplier) % Math.round(multipleOfValue * multiplier) !== 0) {
    //NOT VALID
}
 

What version of Ajv are you using? Does the issue happen if you use the latest version?
5.5.2

Are you going to resolve the issue?
Unfortunately, I do not have time to resolve the issue on my own...

@epoberezkin
Copy link
Member

epoberezkin commented Dec 20, 2017

There is an option multipleOfPrecision that does a similar thing, see options.

@manahga
Copy link
Author

manahga commented Dec 20, 2017

Ugh...sorry about that. Somehow I overlooked it.

@epoberezkin
Copy link
Member

epoberezkin commented Dec 20, 2017

@manahga some algorithm similar to yours can be a better option still, if it can be simplified. The problem with the current approach is that N should be almost empirically defined. See the comment in #84 that was also added today (strangely).

@epoberezkin epoberezkin changed the title Validation of multipleOf / DivisibleBy fields with decimal numbers Validation of multipleOf keyword with decimal numbers Dec 20, 2017
@manahga
Copy link
Author

manahga commented Dec 21, 2017

@epoberezkin Definitely room for simplification. I tend to leave my code less optimized for readability when submitting to these discussions.

@manahga
Copy link
Author

manahga commented Dec 22, 2017

So, I found a nice drop in replacement for the getDecimalPlaces function which works off of regex pattern matching and is over twice as fast according to jsperf.

function getDecimalPlaces(num) {
  var match = (''+num).match(/(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/);
  if (!match) { return 0; }
  return Math.max(0, (match[1] ? match[1].length : 0) - (match[2] ? +match[2] : 0));
}

The only thing to note with this function is that provided numbers within strings such as '1.00' will return 2.

The actual validation could be simplified to this:

const multiplier = Math.pow(
     10, 
     Math.max(getDecimalPlaces(fieldValue), getDecimalPlaces(multipleOfValue))
);
return Math.round(fieldValue * multiplier) % Math.round(multipleOfValue * multiplier) === 0;

@falkenhawk
Copy link

falkenhawk commented Aug 24, 2018

Could be a related issue:
tdegrunt/jsonschema#187

Maybe a fix could be ported from there?

@pateketrueke
Copy link

We're also suffering from this: json-schema-faker/json-schema-faker#379

@godwinjoel
Copy link

Any updates on when this would be resolved?
would be really helpful.

@leomp12
Copy link

leomp12 commented Jan 10, 2019

Maybe something like that should solve.
It's not an approach for all possible cases, but at least for the great majority (I guess), with no need to work with strings or loose much performance.

@mxdxgx
Copy link

mxdxgx commented Mar 11, 2019

Hi everyone, i'm using this lib currently, is there a dev release with a fix ?

@WeikunYe
Copy link

For people who are using AJV with JavaScript or TypeScript and having the issue of validate floating point number, try to build a custom keyword.

Problem

import Ajv from "ajv";

const ajv = new Ajv();
const schema = {
	type: "object",
	properties: {
		price: { type: "number", minimum: 0, multipleOf: 0.01 },
	},
	required: ["price"],
	additionalProperties: false,
};

const data = {price: 2.22}

// will be false
ajv.validate(schema, data)

Because in JS 2.22 / 0.01 = 222.00000000000003 not 222.

Solution
I try to build a custom keyword and use a package called number-precision as a workaround.

import Ajv from "ajv";
import NP from "number-precision";

const ajv = new Ajv();
ajv.addKeyword({
    keyword: "customMultipleOf",
    type: "number",
    compile(schema) {
        return (data) => Number.isInteger(NP.divide(data, schema));
    },
    errors: false,
    metaSchema: {
        type: "number",
    },
});

const schema = {
	type: "object",
	properties: {
		price: { type: "number", minimum: 0, customMultipleOf: 0.01 },
	},
	required: ["price"],
	additionalProperties: false,
};

const data = {price: 2.22}

This will work at least in my case.

@epoberezkin
Copy link
Member

You can actually replace the standard implementation too using removeKeyword first.

@WeikunYe
Copy link

Yes, I just ty and it works.

import Ajv from "ajv";
import NP from "number-precision";

const ajv = new Ajv();
// remove default keyword: multipleOf
ajv.removeKeyword("multipleOf");

ajv.addKeyword({
    keyword: "multipleOf",
    type: "number",
    compile(schema) {
        return (data) => Number.isInteger(NP.divide(data, schema));
    },
    errors: false,
    metaSchema: {
        type: "number",
    },
});

const schema = {
	type: "object",
	properties: {
		price: { type: "number", minimum: 0, multipleOf: 0.01 },
	},
	required: ["price"],
	additionalProperties: false,
};

const data = {price: 2.22}

@jasoniangreen
Copy link
Collaborator

Reviewing the history it doesn't look like this will be changed plus there is a workaround.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

No branches or pull requests

10 participants