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

Avoid big numbers for money #3189

Merged
merged 1 commit into from
Aug 21, 2017
Merged

Avoid big numbers for money #3189

merged 1 commit into from
Aug 21, 2017

Conversation

mgrassotti
Copy link
Contributor

@mgrassotti mgrassotti commented Aug 17, 2017

Trello

https://trello.com/c/y3bOwPKj/293-2-floatdomainerror-infinity-in-multiple-smart-answers

Problem

We noticed some Float::Infinity errors were happening in production, leading to a 500 Error. That’s probably due to some users trying to input very big numbers.

Staging

https://smart-answers-preview-pr-3189.herokuapp.com/am-i-getting-minimum-wage/y/current_payment/not_an_apprentice/111/23/2.0/12.0/2.4132314123431646e+273/111,1111111111111122222222222222222222222222222222222222222222222222222222222222222222222222222222225555555555555555555555555555555555555555555555555555555555222222222222222222222211111111111111111133333333333333333333555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555444444444444444444444444444448888888888888888888888888888888888888888888888888888888888887777777777777778888888888888889999999999.0

Production

https://www.gov.uk/am-i-getting-minimum-wage/y/current_payment/not_an_apprentice/111/23/2.0/12.0/2.4132314123431646e+273/1111111111111111122222222222222222222222222222222222222222222222222222222222222222222222222222222225555555555555555555555555555555555555555555555555555555555222222222222222222222211111111111111111133333333333333333333555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555444444444444444444444444444448888888888888888888888888888888888888888888888888888888888887777777777777778888888888888889999999999.0

Solution

Raise an InvalidResponse error if the value for money is too
big to be represented in a Float variable.

@chao-xian chao-xian temporarily deployed to smart-answers-preview-pr-3189 August 17, 2017 10:38 Inactive
@alecgibson
Copy link
Contributor

alecgibson commented Aug 17, 2017

Shouldn't the infinite? check go last? Will this pass if I feed in eg "999, 999, 999, ...,999" ?

@alecgibson
Copy link
Contributor

Okay, so apparently in Ruby:
"999,999,999".to_f # => 999.0

Which means that "999,999,<loads of numbers>,999".to_f.infinte? # => nil

@mgrassotti
Copy link
Contributor Author

@alecgibson I put it at the end because otherwise it would fall in the condition raw_input.is_a?(Numeric). Why do you think it should go at the end? Thanks

@alecgibson
Copy link
Contributor

@mgrassotti have you clicked the example link above? It still breaks the site.

At the moment we have two paths to generating a BigDecimal:

  1. Check if the input is Numeric, and if so, cast it
  2. Do some logic to a string to try and turn it into a Numeric, and then cast it

The above link breaks the code, because it follows the second path, where we don't check infinite?.

I think logically we should do all casting first, and then check infinite?, kind of like:

money_as_float = some_function_that_definitely_returns_a_number_or_raises(some_input_string)
raise if money_as_float.infinite?

@chao-xian chao-xian temporarily deployed to smart-answers-preview-pr-3189 August 18, 2017 10:01 Inactive
@chao-xian chao-xian temporarily deployed to smart-answers-preview-pr-3189 August 18, 2017 10:02 Inactive
@mgrassotti
Copy link
Contributor Author

@alecgibson sorry I understand now. I pushed an update

@@ -9,14 +9,11 @@ class Money
attr_reader :value

def initialize(raw_input)
if raw_input.is_a?(Numeric)
@value = BigDecimal.new(raw_input.to_s)
number, error_message = self.class.raw_input_to_number(raw_input)
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure I like this pattern of returning a tuple with an error message. Why not just raise directly from raw_input_to_number? It should hopefully tidy up your control logic a lot more.

[raw_input, nil]
else
raw_input = raw_input.to_s.delete(',').gsub(/\s/, '')
if raw_input.to_f.infinite?
Copy link
Contributor

Choose a reason for hiding this comment

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

As per my previous comment, you could avoid having to check infinite? multiple times if you just call it once when you know you have a Numeric (probably outside this method).

@chao-xian chao-xian temporarily deployed to smart-answers-preview-pr-3189 August 18, 2017 11:17 Inactive
@mgrassotti
Copy link
Contributor Author

mgrassotti commented Aug 18, 2017

@alecgibson ok please check it now

@chao-xian chao-xian temporarily deployed to smart-answers-preview-pr-3189 August 18, 2017 11:19 Inactive
end
end

def self.valid_raw_input(raw_input)
Copy link
Contributor

Choose a reason for hiding this comment

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

Would be nice to keep the validation predicate separate from the validation method. So, I'd expect this to be two methods:

  • valid_raw_input?, which returns a boolean
  • validate_raw_input, which calls valid_raw_input? and will raise if it's invalid. You could also move the infinite? check into the validation, because it's part of what makes the input valid.

Copy link
Contributor Author

@mgrassotti mgrassotti Aug 21, 2017

Choose a reason for hiding this comment

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

@alecgibson ok. I think if could be tidy also if done in 1 method. Have a look to my latest changes. Thanks

if raw_input.is_a?(Numeric) || raw_input.is_a?(Money) || raw_input =~ /\A *[0-9]+(\.[0-9]{1,2})? *\z/
raw_input
else
raise InvalidResponse, "Sorry, I couldn't understand that number. Please try again.", caller
Copy link
Contributor

Choose a reason for hiding this comment

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

(I know this isn't part of your change, but while we're here, do you think "I couldn't understand that number" is a normal sort of error message to return? I don't think I'd expect a validation message to use the first person. Maybe more like "The entered value is not a valid number. Please try again."?)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok.

def self.raw_input_to_number(raw_input)
raw_input = raw_input.is_a?(Numeric) ? raw_input : raw_input.to_s.delete(',').gsub(/\s/, '')
if raw_input.to_f.infinite?
raise InvalidResponse, "Sorry, that number is too big. Please try again.", caller
Copy link
Contributor

Choose a reason for hiding this comment

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

I still find it a bit untidy how our validation logic is tangled up in the casting logic.

I basically expect these three separate steps to happen:

  • Attempt to coerce the raw input into a number
  • Validate the number (and raise if appropriate)
  • Cast the number to a BigDecimal

I'd expect these three steps to be represented at a top-level function (probably initialize), and to each have a single line of code, calling to methods if required. This way you get proper separation of concerns.

Copy link
Contributor Author

@mgrassotti mgrassotti Aug 21, 2017

Choose a reason for hiding this comment

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

@value = BigDecimal.new(raw_input.to_s)
end
number = self.class.raw_input_to_number(raw_input)
@value = BigDecimal.new(number.to_s)
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we call .to_s?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@alecgibson that should be useless indeed

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@alecgibson indeed without the .to_s call it is returning ArgumentError: can't omit precision for a Float.. It probably guesses the precision needed if the argument is a String. That's a little bit weird. Anyway I would leave it as it is for moment, if it is ok for you.

Copy link
Contributor

Choose a reason for hiding this comment

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

Huh, weird. Yeah fine to leave then.

@chao-xian chao-xian temporarily deployed to smart-answers-preview-pr-3189 August 21, 2017 09:47 Inactive
@chao-xian chao-xian temporarily deployed to smart-answers-preview-pr-3189 August 21, 2017 09:58 Inactive
@value = BigDecimal.new(raw_input.to_s)
end
input = self.class.parse(raw_input)
number = self.class.valid?(input) && input
Copy link
Contributor

Choose a reason for hiding this comment

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

Why bother assigning number? Given that valid? raises, couldn't you just do something like:

input = self.class.parse(raw_input)
self.class.validate(input)
@value = BigDecimal.new(input.to_s)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍

@chao-xian chao-xian temporarily deployed to smart-answers-preview-pr-3189 August 21, 2017 10:02 Inactive
Copy link
Contributor

@alecgibson alecgibson left a comment

Choose a reason for hiding this comment

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

Looks great! 🚀

@chao-xian chao-xian temporarily deployed to smart-answers-preview-pr-3189 August 21, 2017 10:03 Inactive
Raise an InvalidResponse error if the user tries to insert numbers too
big to be represented in a Float variable.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants