Skip to content

Commit 9f60493

Browse files
authored
Merge pull request hapijs#3013 from hapijs/fix/multiple-precision
fix: precision issue on number().multiple()
2 parents aed0920 + cf2b6fa commit 9f60493

File tree

4 files changed

+79
-8
lines changed

4 files changed

+79
-8
lines changed

.github/workflows/ci-module.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ on:
44
push:
55
branches:
66
- master
7+
- v17
78
pull_request:
89
workflow_dispatch:
910

lib/types/number.js

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,17 @@ const internals = {
1212
exponentialPartRegex: /[eE][+-]?\d+$/,
1313
leadingSignAndZerosRegex: /^[+-]?(0*)?/,
1414
dotRegex: /\./,
15-
trailingZerosRegex: /0+$/
15+
trailingZerosRegex: /0+$/,
16+
decimalPlaces(value) {
17+
18+
const str = value.toString();
19+
const dindex = str.indexOf('.');
20+
const eindex = str.indexOf('e');
21+
return (
22+
(dindex < 0 ? 0 : (eindex < 0 ? str.length : eindex) - dindex - 1) +
23+
(eindex < 0 ? 0 : Math.max(0, -parseInt(str.slice(eindex + 1))))
24+
);
25+
}
1626
};
1727

1828

@@ -168,23 +178,40 @@ module.exports = Any.extend({
168178
multiple: {
169179
method(base) {
170180

171-
return this.$_addRule({ name: 'multiple', args: { base } });
181+
const baseDecimalPlace = typeof base === 'number' ? internals.decimalPlaces(base) : null;
182+
const pfactor = Math.pow(10, baseDecimalPlace);
183+
184+
return this.$_addRule({
185+
name: 'multiple',
186+
args: {
187+
base,
188+
baseDecimalPlace,
189+
pfactor
190+
}
191+
});
172192
},
173-
validate(value, helpers, { base }, options) {
193+
validate(value, helpers, { base, baseDecimalPlace, pfactor }, options) {
174194

175-
if (value * (1 / base) % 1 === 0) {
176-
return value;
195+
const valueDecimalPlace = internals.decimalPlaces(value);
196+
197+
if (valueDecimalPlace > baseDecimalPlace) {
198+
// Value with higher precision than base can never be a multiple
199+
return helpers.error('number.multiple', { multiple: options.args.base, value });
177200
}
178201

179-
return helpers.error('number.multiple', { multiple: options.args.base, value });
202+
return Math.round(pfactor * value) % Math.round(pfactor * base) === 0 ?
203+
value :
204+
helpers.error('number.multiple', { multiple: options.args.base, value });
180205
},
181206
args: [
182207
{
183208
name: 'base',
184209
ref: true,
185210
assert: (value) => typeof value === 'number' && isFinite(value) && value > 0,
186211
message: 'must be a positive number'
187-
}
212+
},
213+
'baseDecimalPlace',
214+
'pfactor'
188215
],
189216
multi: true
190217
},

test/errors.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ describe('errors', () => {
3838
const err = Joi.valid('foo').validate('bar', { errors: { stack: true } }).error;
3939
expect(err).to.be.an.error();
4040
expect(err.isJoi).to.be.true();
41-
expect(err.stack).to.contain('at Object.exports.process');
41+
expect(err.stack).to.contain(' at ');
42+
expect(err.stack).to.contain('exports.process');
4243
});
4344

4445
it('supports custom errors when validating types', () => {

test/types/number.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -747,6 +747,48 @@ describe('number', () => {
747747
]);
748748
});
749749

750+
it('handles precision errors correctly', () => {
751+
752+
const cases = [
753+
[3600000, [
754+
[14400000, true]
755+
]],
756+
[0.01, [
757+
[2.03, true],
758+
[2.029999999999, false, {
759+
message: '"value" must be a multiple of 0.01',
760+
path: [],
761+
type: 'number.multiple',
762+
context: { multiple: 0.01, value: 2.029999999999, label: 'value' }
763+
}],
764+
[2.030000000001, false, {
765+
message: '"value" must be a multiple of 0.01',
766+
path: [],
767+
type: 'number.multiple',
768+
context: { multiple: 0.01, value: 2.030000000001, label: 'value' }
769+
}],
770+
[0.03, true]
771+
]],
772+
[0.0000000001, [
773+
[0.2, true]
774+
]],
775+
[0.000000101, [
776+
[0.101, true],
777+
[0.10101, false, {
778+
message: '"value" must be a multiple of 1.01e-7',
779+
path: [],
780+
type: 'number.multiple',
781+
context: { multiple: 0.000000101, value: 0.10101, label: 'value' }
782+
}]
783+
]]
784+
];
785+
786+
for (const [multiple, tests] of cases) {
787+
const schema = Joi.number().multiple(multiple);
788+
Helper.validate(schema, tests);
789+
}
790+
});
791+
750792
it('handles floats multiples correctly', () => {
751793

752794
const schema = Joi.number().multiple(3.5);

0 commit comments

Comments
 (0)