skip to content

In Ruby, why does 0.1 + 0.2 == 0.3 return false, and how should you compare or compute such values instead?

level: juniorimportance: must knowfreq 75%

answer

  1. binary fractions, not decimal
  2. IEEE 754 double, 53-bit mantissa
  3. 0.30000000000000004
  4. tolerance scaled to magnitude
  5. 0.1r and BigDecimal("0.1") stay exact

basics

~20 s

Float stores IEEE 754 binary doubles, and 0.1, 0.2 and 0.3 have no exact binary form, so the sum is 0.30000000000000004. Compare Floats with a tolerance, or compute with Rational, BigDecimal or integer cents when exactness matters.

solid answer

~40 s

A Ruby `Float` is a 64-bit IEEE 754 double. Tenths are repeating fractions in binary, so `0.1` and `0.2` are stored as the nearest doubles, both slightly above the decimal value, and their sum rounds to `0.30000000000000004`, while `0.3` is a different double. `Float#==` compares those exact doubles, so the result is `false`; `eql?` and `===` behave the same. To compare measured values, test `(a - b).abs` against a tolerance scaled to the size of the numbers, not against `Float::EPSILON` alone. When the values must be exact - money, quantities, ratios - avoid Float entirely: `0.1r + 0.2r == 0.3r` is `true`, `BigDecimal("0.1")` keeps decimal digits, and integer minor units never round.

code

ruby · 10 lines
ruby
0.1 + 0.2          # => 0.30000000000000004
0.1 + 0.2 == 0.3   # => false
0.1.to_r           # => (3602879701896397/36028797018963968)

def close?(a, b, rel: 1e-9, abs: 1e-12)
  (a - b).abs <= [rel * [a.abs, b.abs].max, abs].max
end

close?(0.1 + 0.2, 0.3) # => true
0.1r + 0.2r == 0.3r    # => true

go deeper

for a junior

Recall that Float is binary, that 0.1 + 0.2 prints 0.30000000000000004, and that money belongs in integer cents, BigDecimal or Rational.

for a middle

Explain the 53-bit mantissa, why to_s hides the error, and how to write a tolerance comparison that scales with magnitude.

for a senior

Show judgement about where exactness is required, and catch Float leaking into totals, rounding thresholds or equality checks during review.

for a principal

Treat numeric type choice as a data-model decision: once Float enters a pipeline, later conversions cannot restore exactness, so fix types at the boundary.

## What a Float really stores A Ruby **`Float`** uses the platform's double-precision IEEE 754 format: a sign, an exponent and a **53-bit binary mantissa**. Any value that is a sum of powers of two in that range is stored exactly - `0.5`, `37.5`, `12.3125`. A decimal fraction such as `0.1` is not: in binary it is a repeating pattern, like one third in decimal, so Ruby stores the **nearest representable double**. You can see the stored value directly: ```ruby 0.1.to_r # => (3602879701896397/36028797018963968) format("%.20f", 0.1) # => "0.10000000000000000555" ``` `0.1` still *prints* as `0.1` because `Float#to_s` chooses the shortest decimal string that converts back to the same double. The display hides the error; it does not remove it. ## Why the sum misses 0.3 1. `0.1` is stored slightly above one tenth, and `0.2` slightly above two tenths. 2. Adding them produces an exact binary sum, which is then rounded to the nearest double: `0.30000000000000004`. 3. The literal `0.3` is rounded independently to a *different* nearby double, `0.3`. 4. `Float#==` compares the two stored doubles exactly, so `0.1 + 0.2 == 0.3` is `false`. Nothing here is a Ruby bug; every language that uses binary doubles gives the same result. Changing the method does not help either: `eql?` and `===` on Floats also compare the stored values. ## Comparing Floats safely When the values come from measurement or calculation and small error is acceptable, compare with a **tolerance**: - An **absolute** tolerance such as `(a - b).abs < 1e-9` works only when you know the magnitude of the values. - A **relative** tolerance scales with the operands: `(a - b).abs <= 1e-9 * [a.abs, b.abs].max`. - Combining both covers values near zero, where a relative test becomes too strict. - **`Float::EPSILON`** (`2.220446049250313e-16`) is the gap between `1.0` and the next Float. It is not a universal tolerance: near `1000.0` adjacent Floats are about `1.1e-13` apart, so an EPSILON test rejects values that are as close as a double can make them. - `Float#next_float` and `Float#prev_float` show the actual spacing at any magnitude. ## When not to use Float at all | Need | Use | Example | |---|---|---| | fast approximate maths, measurements | `Float` | `9.81 * t * t / 2` | | exact fractions and ratios | `Rational` | `0.1r + 0.2r == 0.3r # => true` | | exact decimal digits, money, rounding modes | `BigDecimal` (bundled gem) | `BigDecimal("0.1") + BigDecimal("0.2")` | | counts of the smallest unit | `Integer` | `1999` cents instead of `19.99` | The key is to choose before the first calculation: converting a Float result to `Rational` or `BigDecimal` afterwards keeps the error it already contains - `(0.1 + 0.2).to_r` is not `3/10r`. ## Accumulated error A single operation is off by at most half a step, but loops add those half-steps up: ```ruby x = 0.0 10.times { x += 0.1 } x # => 0.9999999999999999 x == 1.0 # => false ``` Each addition rounds again, and the running total drifts. This is why a loop that sums prices, or a `while t < 1.0` loop stepping by `0.1`, can run one iteration too many or end on an unexpected total. Iterating with an Integer counter and computing `i / 10r` or `i * step` from it avoids the drift. ## Traps worth naming - **Printing is not proof.** `puts 0.1` looks exact; `0.1.to_r` shows it is not. - **Rounding for display is fine; rounding to compare is fragile.** Two values on either side of a rounding boundary can round apart even when they differ by one step. - **Accumulation grows the error**, as the loop above shows. - **Money in Float** is the classic interview follow-up: the answer is integer minor units, `BigDecimal` or `Rational`, never "round when saving". - **Float is still the right default** for physics, statistics, graphics and anything measured; the fix is choosing the type deliberately, not banning Float.

  • In Ruby, if 0.1 is not stored exactly, why does puts 0.1 print 0.1?
    `Float#to_s` prints the shortest decimal string that converts back to the same double, and for the double nearest one tenth that string is `0.1`. The stored value is still slightly larger: `0.1.to_r` returns `(3602879701896397/36028797018963968)` and `format("%.20f", 0.1)` shows `0.10000000000000000555`. The display hides the error without removing it.
  • In Ruby, why is (a - b).abs < Float::EPSILON a poor general comparison?
    `Float::EPSILON` is the gap between `1.0` and the next Float, about `2.2e-16`. Float spacing grows with magnitude: near `1000.0` neighbouring doubles are about `1.1e-13` apart, so two results that differ by a single step already fail the test, while for tiny values the same tolerance is far too loose. Scale the tolerance to the operands, with an absolute floor near zero.

Writing one third on a paper receipt: you must stop at some digit, so three copies of 0.333 add up to 0.999, not 1. A Float has the same problem with tenths, only in binary.

saying these in an interview costs you the question

  • Ruby's Float is buggy because 0.1 + 0.2 is not exactly 0.3.
  • eql? or === compare Floats more precisely than ==.
  • Float::EPSILON is the right tolerance for comparing any two Floats.
  • Floats are fine for money as long as you round the result when printing.
  • Converting a Float result with to_r afterwards recovers the exact decimal value.