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

number.multiple() enhancement - Issue #916 #918

Closed
wants to merge 11 commits into from
Closed

number.multiple() enhancement - Issue #916 #918

wants to merge 11 commits into from

Conversation

santanu-biswas
Copy link
Contributor

‘number.multiple()’ enhanced for:

  1. Accept any Number (integer or float) as argument.
  2. Accept references as argument.
  3. For any non-number or zero multiple base, validate now returns error
    instead of throwing it.

‘number.multiple()’ enhanced for:
1. Accept any Number (integer or float) as argument.
2. Accept references as argument.
3. For any non-number or zero multiple base, validate now returns error
instead of throwing it.
‘number.multiple()’ enhanced for:
1. Accept any Number (integer or float) as argument.
2. Accept references as argument.
3. For any non-number or zero multiple base, validate now returns error
instead of throwing it.
4. Required tests added & checked.
@santanu-biswas
Copy link
Contributor Author

Rebase done

@Marsup
Copy link
Collaborator

Marsup commented Jun 17, 2016

You need to check error messages as well, false is not enough (eg. https://github.com/hapijs/joi/blob/master/test/number.js#L548). That's a new policy I intend to apply to all the tests but didn't have time to do.


return this._test('multiple', base, function (value, state, options) {
const mult = Math.pow(10, Math.max(internals.precision(value), internals.precision(divisor)));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which case it this trying to prevent ?

Copy link
Contributor Author

@santanu-biswas santanu-biswas Jun 17, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Marsup
It is to allow number.multiple() to allow float numbers to be checked. Currently number.multiple() cannot handle floats. However % operator cannot handle decimal numbers correctly. So the idea is to change the decimal numbers to integers before using % operator.

Example:

  • To check if 4.523 is a multiple of 1.3, it checks if 4523 (4.523 * 10^3) is a multiple of 1300 (1.3 * 10^3).
  • To check if 4.2 is a multiple of 1.05, it checks if 420 (4.2 * 10^2) is a multiple of 105 (1.05 * 10^2)

Added internals.precision(num) to find the number of decimal places in a any number.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it a real world problem for existing use cases though ? We don't want accurate modulo, we just want 0 when it's a multiple.

Copy link
Contributor Author

@santanu-biswas santanu-biswas Jun 17, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes @Marsup it is a real life problem for existing use cases in financial application involving exchange rates that can have 2 to 5 decimal accuracy.

number.multiple() fails to validate entered currency value as exact multiple of an amount in another currency because of modulo operator's inability to handle decimal/floats correctly.

Example:
6711 % 67.11 results in 5.684341886080802e-14
3230 % 32.3 results in 2.8421709430404007e-13

Although in both the above cases it should have been 0 but modulo operator is broken for decimal/float values. (Check JSFiddle)

Similar real world use cases also exist in stock trading application.

@santanu-biswas
Copy link
Contributor Author

@Marsup
I have added the required error message checks for tests that return false.

@Marsup Marsup added the feature New functionality or improvement label Jun 17, 2016
@Marsup Marsup self-assigned this Jun 17, 2016
@Marsup
Copy link
Collaborator

Marsup commented Jun 18, 2016

I'm now wondering whether it should be an extension or not, it's still unsafe and we'd be better off using something like big.js/decimal.js or whatever module works.

@santanu-biswas
Copy link
Contributor Author

santanu-biswas commented Jun 18, 2016

@Marsup
Initially I did think about adding a module float.js, however, then I realized that number.js will have to be modified to handle only integers. Otherwise, there would be an ambiguity between Joi.number() and Joi.float(). But then if this change is done then it would break current usage code wherever Joi.number() is being used to validate integers.

Thats why I felt that for now doing it in number.js will be better and then upgrade by adding float.js to clearly distinguish between integer and float validation.

@Marsup
Copy link
Collaborator

Marsup commented Jun 18, 2016

You misunderstood me, I was talking about modules, not files. I don't see any point in having another type since javascript doesn't make this difference either.

@santanu-biswas
Copy link
Contributor Author

@Marsup
OK so what do you suggest now?
What makes you feel that it is unsafe?

@Marsup
Copy link
Collaborator

Marsup commented Jun 18, 2016

Sorry not unsafe, unreliable. However unlikely, given enough digits, you'll overflow the maximum capacity of javascript numbers with the multiplication. I'm still looking whether we can achieve the same thing without increasing original values but I can't find anything.

@santanu-biswas
Copy link
Contributor Author

santanu-biswas commented Jun 18, 2016

Marsup I understand your point now.
So, is it alright if I change it to simple modulus like before and keep the other enhancements:

  1. Accept references as argument.
  2. For any non-number or zero multiple base, validate now returns error
    instead of throwing it.

@santanu-biswas
Copy link
Contributor Author

@Marsup
Please have a look at decimal.js. It has a robust & reliable mod function.

(A lighter version of it is also available at decimal.js-light).

I tested the mod function with fairly large numbers (See JSFiddle) and it works perfectly alright.

If you agree, it can be used.

@Marsup
Copy link
Collaborator

Marsup commented Jun 18, 2016

I already mentioned that library a few comments ago, it's also why I said I might have it as an extension, since this is exactly why it was made.

@santanu-biswas
Copy link
Contributor Author

@Marsup
Added big.js and made the required changes in modulo function and completed all tests.

@Marsup
Copy link
Collaborator

Marsup commented Jun 19, 2016

When I said an extension, I meant a separate module (see Joi.extend()), sorry if that was unclear. I'm not adding any dependencies to the core unless they serve a common interest.
What can be done in the core is accepting floats and references though, but documenting that it has the same flaws as javascript itself.

Removed big.js and reverted back to regular modulo operator. Also added
notes to the documentation about the limitations of number.multiple()
with decimals/floats.
@santanu-biswas
Copy link
Contributor Author

@Marsup Okay... I understand. Removed big.js but kept float acceptance and refs.
Also added Notes to documentation as suggested by you. Hope I got it alright now.

@@ -53,12 +53,29 @@ internals.Number = class extends Any {

multiple(base) {

Hoek.assert(Hoek.isInteger(base), 'multiple must be an integer');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't remove those, just allow floats.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you don't remove this then it throws error instead of gracefully validating (thats how it happens even now). It starts throwing exception for non-integer number, so what is the point in allowing float numbers?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And that's intended, this is to prevent developers mistakes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So that means:

  • The developer should first validate if the multiple is zero (the developer is using Joi for validation and there is no way to validate that in Joi).
  • Allowing floats has no meaning, because it will throw exception anyways.

Am I right?
So in effect we are just making the changes for allowing refs.?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The developer is using joi to validate external inputs, not to validate its own inputs, there are synchronous throws for that yes.
How would allowing floats crash anything ?


return this._test('multiple', base, function (value, state, options) {
return this._test('multiple', base, (value, state, options) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not use => here.

Restored errors to be thrown for non-integer and zero value of multiple.
const isRef = Ref.isRef(base);

if (!isRef) {
Hoek.assert(Hoek.isInteger(base), 'multiple must be an integer');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So why aren't you allowing floats here ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Marsup Allowed floats now.

@santanu-biswas
Copy link
Contributor Author

@Marsup Have allowed all numbers (Integers & floats). Is there anything else that I have missed?

@Marsup
Copy link
Collaborator

Marsup commented Jun 20, 2016

I don't think so but I'll review more in depth tonight.

@Marsup Marsup added this to the 9.0.0 milestone Jun 22, 2016
@Marsup
Copy link
Collaborator

Marsup commented Jun 22, 2016

Merged as 3a32b92. Cleaned it up a bit before merging, thanks for your contribution.

@Marsup Marsup closed this Jun 22, 2016
@lock
Copy link

lock bot commented Jan 9, 2020

This thread has been automatically locked due to inactivity. Please open a new issue for related bugs or questions following the new issue template instructions.

@lock lock bot locked as resolved and limited conversation to collaborators Jan 9, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
feature New functionality or improvement
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants