skip to content

In Ruby, what is the difference between 1..10 and 1...10, and what do endless (5..) and beginless (..5) ranges mean?

level: juniorimportance: must knowfreq 65%

answer

  1. two dots keep the end
  2. exclude_end? reports the third dot
  3. an omitted endpoint is stored as nil
  4. to_a on (5..) raises RangeError
  5. (..5).each: can't iterate from NilClass

basics

~20 s

1..10 includes 10; 1...10 stops before it, and exclude_end? reports which. An endless range like (5..) has a nil end and never stops; a beginless range like (..5) has a nil begin and supports comparisons but not iteration.

solid answer

~30 s

Two dots build an inclusive range, so `(1..10).cover?(10)` is `true`; three dots exclude the end, so `(1...10)` stops at 9 and `exclude_end?` returns `true`. `Range.new(1, 10, true)` is the same as `1...10`. Leaving out the end, as in `(5..)`, gives an **endless** range whose `end` is `nil`: `(5..).size` is `Infinity` and `(5..).to_a` raises `RangeError`. Leaving out the begin, as in `(..5)`, gives a **beginless** range whose `begin` is `nil`: `cover?` works, but iterating raises `TypeError` (`can't iterate from NilClass`). One trap to name: `(1...4).last` returns `4`, the stored end, while `(1...4).max` returns `3`.

code

ruby · 16 lines
ruby
OPEN_HOURS = (9...17)
OPEN_HOURS.cover?(16)    # => true
OPEN_HOURS.cover?(17)    # => false
OPEN_HOURS.exclude_end?  # => true
OPEN_HOURS.last          # => 17, the stored end
OPEN_HOURS.max           # => 16

senior = (65..)
senior.end               # => nil
senior.size              # => Infinity
senior.cover?(80)        # => true
senior.to_a              # RangeError: cannot convert endless range to an array

minor = (...18)
minor.cover?(17)         # => true
minor.each { }           # TypeError: can't iterate from NilClass

go deeper

for a junior

Recall which literal includes the end, and that an omitted endpoint makes the range endless or beginless with nil in that slot.

for a middle

Explain what a Range object stores, why endless ranges report Infinity for size and refuse to_a, and why a beginless range cannot iterate.

for a senior

Show you choose half-open ranges on purpose for boundaries such as closing times, and that you know last and max disagree on exclusive ranges.

for a principal

Discuss when ranges should be the domain's vocabulary for bounds and rules, and how open-ended ranges keep limits out of scattered comparisons.

## Two dots or three dots A Ruby **range** is an object that describes an interval between a begin value and an end value. The literal syntax decides whether the end value belongs to the interval: | Literal | End included? | `exclude_end?` | `to_a` | |---|---|---|---| | `(1..4)` | yes | `false` | `[1, 2, 3, 4]` | | `(1...4)` | no | `true` | `[1, 2, 3]` | | `Range.new(1, 4)` | yes | `false` | `[1, 2, 3, 4]` | | `Range.new(1, 4, true)` | no | `true` | `[1, 2, 3]` | A common way to remember it: the third dot pushes the end value out of the range. ## What a Range object holds A range stores three things and nothing else: - **`begin`** and **`end`**, the two endpoint objects (either may be `nil`); - **`exclude_end?`**, whether the end is excluded. It does **not** store its elements. `(1..1_000_000)` is three values, not a million integers, which is why membership checks against a range are cheap. Range objects are also **frozen**. Two capabilities depend on the endpoints: - **Comparing** a value against a range needs `<=>` on the endpoints. - **Iterating** a range needs the begin value to respond to `succ`; `(1.0..2.0).each` raises `TypeError` (`can't iterate from Float`). ## Endless ranges An **endless range** leaves out the end: `(5..)` or `(5...)`, the same as `Range.new(5, nil)`. Its `end` is `nil`. - `(5..).cover?(1_000_000)` is `true`: every value from 5 upward is inside. - `(5..).size` returns `Infinity`. - `(5..).to_a` raises `RangeError` (`cannot convert endless range to an array`). - `(5..).last` raises `RangeError`, because there is no last element. - Iterating it never stops on its own, so the loop has to break out. ## Beginless ranges A **beginless range** leaves out the begin: `(..5)` or `(...5)`. Its `begin` is `nil`. - `(..5).cover?(3)` is `true` and `(...5).cover?(5)` is `false`. - It cannot be iterated, since there is no first value: `(..5).each` and `(..5).to_a` raise `TypeError` (`can't iterate from NilClass`). - It reads well as an upper bound, for example `Comparable#clamp(..100)`. ## Traps with exclusive and open ends 1. **`last` without an argument returns the stored end.** `(1...4).last` is `4` even though 4 is excluded; `(1...4).last(2)` is `[2, 3]`, because with an argument it counts real elements. 2. **`max` respects the exclusion for integers.** `(1...4).max` is `3`, but `(1.0...4.0).max` raises `TypeError` (`cannot exclude non Integer end value`), since no float is "the one before 4.0". 3. **`size` counts only numeric ranges.** `(1...4).size` is `3`, while `('a'..'z').size` is `nil`; `count` iterates and returns 26. 4. **An empty range is legal.** `(5..1)` raises nothing; it simply contains no values. ## Choosing inclusive or exclusive The two forms are not interchangeable style choices; each fits a different kind of boundary: - **Half-open ranges (`...`)** fit measured quantities such as times, offsets and indexes. Adjacent ranges such as `9...12` and `12...17` share no value, and the length is simply `end - begin`. - **Inclusive ranges (`..`)** fit labelled categories that people say out loud, such as "ages 18 to 64", where the last value clearly belongs. - **Mixing them** is the usual source of off-by-one bugs: one method builds `9..17`, another assumes 17 is closed, and a 17:00 booking slips through. Picking one convention per concept, and naming it in the code, avoids that. ## A clinic example A clinic booking tool stores opening hours as whole hours, open from 9 until 17. Writing `OPEN_HOURS = (9...17)` says exactly that: a booking at 16 is accepted and one at 17 is not, with no `- 1` arithmetic. An open-ended rule, such as "patients aged 65 and over get the long slot", is naturally an endless range `(65..)`, and "under 18" is the beginless `(...18)`.

  • How do you build an exclusive range without the three-dot literal?
    Pass `true` as the third argument: `Range.new(9, 17, true)` equals `9...17`. That is useful when the exclusion is a runtime choice, for example a setting that says whether a closing time is bookable. `Range.new(9, nil)` builds the endless range `9..`.
  • Why does (1...4).last return 4 while (1...4).last(2) returns [2, 3]?
    With no argument, `Range#last` returns the stored `end` object and ignores `exclude_end?`. With a count, it returns that many actual elements, so the excluded end is left out. When you want the largest element of an integer range, use `max` instead.
  • What does (1..).size return, and why can (..1).each not run?
    `(1..).size` returns `Infinity`, because an endless integer range has no upper bound. `(..1).each` raises `TypeError` with `can't iterate from NilClass`: iteration starts at `begin` and calls `succ`, and a beginless range has no begin value to start from.

saying these in an interview costs you the question

  • Three dots include the end because the literal is longer
  • An endless range stores Float::INFINITY as its end
  • (1...4).last returns 3 because the end is excluded
  • A beginless range starts counting at 0 when iterated
  • A range like 1..1_000_000 allocates every integer up front