In Ruby, what is the difference between 1..10 and 1...10, and what do endless (5..) and beginless (..5) ranges mean?
answer
- two dots keep the end
- exclude_end? reports the third dot
- an omitted endpoint is stored as nil
- to_a on (5..) raises RangeError
- (..5).each: can't iterate from NilClass
basics
~20 s1..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 sTwo 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 linesOPEN_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 NilClassgo deeper
Recall which literal includes the end, and that an omitted endpoint makes the range endless or beginless with nil in that slot.
Explain what a Range object stores, why endless ranges report Infinity for size and refuse to_a, and why a beginless range cannot iterate.
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.
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