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

Banner image