skip to content

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%

answer

  1. return class follows the digits sign
  2. positive digits: Float; else Integer
  3. default half: :up, away from zero
  4. half: :even is banker's rounding
  5. floor and ceil take no half:

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.

solid answer

~30 s

`Float#round`, `floor` and `ceil` take an optional `ndigits`. Positive digits keep that many decimals and return a `Float` (`1234.567.round(1) # => 1234.6`); zero or negative digits return an `Integer` with trailing zeros (`1234.567.round(-2) # => 1200`). `round` also takes `half:` for exact ties: the default `:up` rounds away from zero (`2.5.round # => 3`, `(-2.5).round # => -3`), `:even` is banker's rounding (`2.5.round(half: :even) # => 2`), `:down` rounds toward zero, and anything else raises `ArgumentError`. `floor` and `ceil` have no `half:`; they move toward negative and positive infinity, so `-1234.567.floor(1)` is `-1234.6`. `Integer#round` and `Rational#round` accept the same `half:` keyword.

code

ruby · 13 lines
ruby
2.5.round                 # => 3
(-2.5).round              # => -3
2.5.round(half: :even)    # => 2
2.5.round(half: :down)    # => 2

1234.567.round(1)         # => 1234.6
1234.567.round(-2)        # => 1200
-1234.567.floor(1)        # => -1234.6
1234.567.ceil(-2)         # => 1300

25.round(-1)              # => 30
25.round(-1, half: :even) # => 20
(0.3 / 0.1).floor         # => 2

go deeper

for a junior

Know that round, floor and ceil take a digits argument and that plain round gives an Integer, with 2.5 rounding to 3.

for a middle

Explain how the sign of ndigits sets the return class, what each half: mode does at exact ties, and why floor differs from truncate below zero.

for a senior

Spot rounding done on Float intermediates in billing or reporting code and move it to BigDecimal or Rational with an explicit half: mode.

for a principal

Treat rounding policy as a business rule: pick one mode per domain, document it, and apply it once at a defined boundary.

## One digits argument, three directions `Float#round`, `Float#floor` and `Float#ceil` all accept an optional **`ndigits`** argument that says *where* to cut: | Call | Result | Class | |---|---|---| | `1234.567.round` | `1235` | `Integer` | | `1234.567.round(1)` | `1234.6` | `Float` | | `1234.567.round(-2)` | `1200` | `Integer` | | `1234.567.floor(1)` | `1234.5` | `Float` | | `-1234.567.floor(1)` | `-1234.6` | `Float` | | `1234.567.ceil(-2)` | `1300` | `Integer` | | `12.34.floor` | `12` | `Integer` | ## Return types follow the sign of ndigits - **No argument or `0`**: an `Integer`. `2.7.round` is `3`, not `3.0`. - **Positive `ndigits`**: a `Float` with at most that many decimals, `1.0.round(2)` is still `1.0` - the method does not pad. - **Negative `ndigits`**: an `Integer` with at least `ndigits.abs` trailing zeros, for rounding to tens, hundreds or thousands. `round` never returns a formatted string. For display with fixed decimals use `format("%.2f", x)`; for stored values that must keep two decimals, use `BigDecimal` or integer minor units. ## Tie-breaking with half: `half:` matters only when the value is **exactly** halfway between the two candidates: | `half:` | `2.5.round(half: ...)` | `3.5.round(...)` | `(-2.5).round(...)` | |---|---|---|---| | `:up` (default, or `nil`) | `3` | `4` | `-3` | | `:down` | `2` | `3` | `-2` | | `:even` | `2` | `4` | `-2` | - `:up` means **away from zero**, not toward positive infinity. - `:even` is **banker's rounding**: ties go to the even neighbour, so repeated rounding does not drift upward on average. - An unknown mode such as `half: :odd` raises `ArgumentError` (`invalid rounding mode: odd`). - The same keyword works on `Integer#round` (`25.round(-1, half: :even) # => 20`, while `25.round(-1)` is `30`) and on `Rational#round`, and `BigDecimal#round` accepts it too. ## floor and ceil `floor` and `ceil` take `ndigits` but **no `half:`**; they have nothing to break. `floor` moves toward **negative infinity** and `ceil` toward **positive infinity**, which differs from truncation for negative numbers: `-1234.567.floor(1)` is `-1234.6`, while `truncate(1)` would give `-1234.5`. ## Binary surprises The digits argument is decimal, but the value is still a binary double: 1. `0.3 / 0.1` is `2.9999999999999996`, so `(0.3 / 0.1).floor` returns `2`, not `3`. 2. `2.675` is stored slightly below 2.675, yet `2.675.round(2)` returns `2.68`: for positive digits in the default `half: :up` mode, CRuby's `round` checks whether the decimal halfway point, computed in double arithmetic, is not above the value, so it rounds as the decimal text suggests. 3. `floor` and `ceil` have no such correction, which is why the first case bites. ## The same API on other numeric types The digits-and-`half:` design is shared across Ruby's numeric classes, which makes it easy to move rounding off Float without learning a new vocabulary: | Receiver | Positive digits return | Tie example | |---|---|---| | `Float` | `Float` | `2.5.round(half: :even) # => 2` | | `Integer` | the Integer itself (nothing to cut) | `25.round(-1, half: :even) # => 20` | | `Rational` | `Rational` | `Rational(5, 2).round(half: :even) # => 2` | | `BigDecimal` (gem) | `BigDecimal` | `BigDecimal("0.125").round(2, half: :even) # => 0.12` | `Rational` and `BigDecimal` apply the mode to an exact value, so a tie is really a tie: `(100r / 3).round(2)` is `(3333/100)`, and nothing depends on how a double happened to be stored. ## Common mistakes 1. Expecting `round(2)` to produce a string with two decimals for a receipt - it returns a Float, and `1.5.round(2)` prints as `1.5`. 2. Assuming banker's rounding is the default because another language or a database uses it. 3. Using `floor` where `truncate` was meant, and getting a different answer for negative amounts. 4. Rounding a Float intermediate in the middle of a calculation, then rounding again at the end - double rounding can move a value across a tie. ## Choosing a method in real code - Rounding a **displayed** measurement: `round(n)` on a Float is fine. - Rounding **money**: do it on `BigDecimal` or `Rational` with an explicit `half:`, never on a Float intermediate. - Bucketing (for example to the nearest hundred): `round(-2)`, `floor(-2)` or `ceil(-2)` return Integers, which suits keys and labels.

  • In Ruby, what does 2.675.round(2) return, given that 2.675 is stored slightly below 2.675?
    It returns `2.68`. For positive digits, CRuby's `Float#round` in the default `half: :up` mode checks whether the decimal halfway point, computed in double arithmetic, is not above the stored value, so it rounds as the decimal literal suggests; `1.005.round(2)` gives `1.01` the same way. `floor` and `ceil` have no such correction, and money should not depend on either.
  • In Ruby, how do floor and truncate differ for negative Floats?
    `floor` moves toward negative infinity and `truncate` toward zero. For positive numbers they agree, but `-1234.567.floor(1)` is `-1234.6` while `-1234.567.truncate(1)` is `-1234.5`. Both take the same `ndigits` argument and neither accepts `half:`.

saying these in an interview costs you the question

  • Ruby's Float#round uses banker's rounding by default, so 2.5.round is 2.
  • round with a negative digits count returns a Float such as 1200.0.
  • floor and ceil accept the half: keyword just like round.
  • round(2) returns a string padded to exactly two decimals.
  • floor always moves toward zero, so -1234.567.floor(1) is -1234.5.