skip to content

Integers, Floats & Rationals

Integer has arbitrary precision, Float rounds in binary so 0.1 + 0.2 misses 0.3, and Rational and BigDecimal keep exact values. Interviewers probe money math and rounding.

on this pageshow

explore

questions

6

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.
open as a page

In a Ruby 4.0 service quoting foreign-exchange amounts, how would you use BigDecimal to convert and round to cents correctly?

level: seniorimportance: must knowfreq 55%

basics

~20 s

Add bigdecimal to the Gemfile, since it is a bundled gem from Ruby 3.4, build amounts and rates from Strings, multiply as BigDecimal and round once at the end with an explicit mode such as round(2, half: :even).

open as a page

In Ruby 4.0, what class does (1..30).reduce(:*) return, and what happened to Fixnum and Bignum?

level: juniorimportance: should knowfreq 45%

basics

~10 s

It returns Integer. Ruby has one arbitrary-precision Integer class that grows instead of overflowing; Fixnum and Bignum were unified into Integer in Ruby 2.4 and their deprecated constants were removed in 3.2.

open as a page

In Ruby, how do Kernel#rand, Random.rand and Random.new(seed) differ, and how do you make random results reproducible?

level: middleimportance: should knowfreq 30%

basics

~20 s

Kernel#rand and Random.rand draw from Ruby's shared default generator, while Random.new(seed) creates an independent one. The same seed replays the same sequence, and shuffle and sample accept random: for repeatable results. None of them is fit for secrets.

open as a page

In Ruby, what do Float#round, floor and ceil return when you pass a digits argument or the half: keyword?

level: middleimportance: should knowfreq 40%

basics

~20 s

With no digits, zero or negative digits they return an Integer; with positive digits a Float. round breaks ties away from zero unless half: :even or half: :down is given; floor and ceil always move toward negative or positive infinity.

open as a page

In Ruby, how do Rational values like 3r and 0.1r stay exact, and what do quo, fdiv and Float#to_r return?

level: middleimportance: nice to knowfreq 25%

basics

~10 s

A Rational stores a reduced Integer numerator and denominator, so 1/3r + 1/6r is exactly (1/2). 7.quo(2) returns (7/2), 7.fdiv(2) returns 3.5, and 0.1.to_r returns the stored double's exact fraction, not (1/10).

open as a page