Integers, floats and rounding
Integer division
Two integers divided give an integer: the fractional part is dropped, not rounded. That is the first surprise for a reader who expects a float:
ruby -e 'puts 10 / 3'
3
Make one side a float and the result is a float, with the usual binary fraction behind it. fdiv does the same without converting both sides, and divmod returns the quotient and the remainder together.
Floats are not exact
A float is a binary fraction, and 0.1 has no exact binary form. The result of adding two of them is the classic line every language prints:
ruby -e 'puts 0.1 + 0.2'
0.30000000000000004
The value is not wrong; it is the nearest float to the sum of the two nearest floats. Comparing it with 0.3 therefore fails, and a loop that counts money in floats drifts. Compare with a tolerance ((a - b).abs < 0.001) when a float is all you have.
Rational and BigDecimal
Rational keeps an exact fraction, which is enough for 10/3 and for splitting a bill. BigDecimal keeps decimal digits exactly, which is what money wants:
ruby -e 'puts Rational(10, 3); puts Rational(10, 3).to_f'
10/3
3.3333333333333335
BigDecimal is a gem, not a core class. Install it first:
gem install bigdecimal
1 gem installed
Then use it. The sum of the two values that disagree in a float agrees here, and the last line compares them as numbers:
require "bigdecimal"
sum = BigDecimal("0.1") + BigDecimal("0.2")
puts sum
puts sum.to_s("F")
puts sum == BigDecimal("0.3")
0.3e0
0.3
true
to_s without an argument prints the scientific
form (0.3e0), which is how BigDecimal keeps the
scale visible; to_s("F") prints the fixed-point
form a person expects in a report. Both are the same number, and the
comparison with BigDecimal("0.3") is true.
The gem builds a native extension, so it needs a compiler. The commands
above were run on the host in the ruby:3.4 image,
which has one; on a machine without a compiler, install the prebuilt
package or stay with integer cents.
Rounding money
round(2) rounds half away from zero, and the float underneath decides which side of half a value falls:
ruby -e 'puts 1.005.round(2); puts 2.675.round(2)'
1.01
2.68
Both look right here, and one of them is luck: 1.005 in binary is a little below the exact half, so round reaches 1.01 only because of the float error above. With money, work in integer cents or in BigDecimal; leave the float for display.
What to watch for
A test that compares floats with == fails
eventually. Use assert_in_delta, or compare the
rounded value when the rule is "two decimals".
Integer division is a feature, not a bug: the result keeps its type.
Printing 10 / 3.0 when a float is wanted is
clearer than a conversion after the fact.
Next: Formatting strings and numbers
Article author: Arthur Isaev