skip to content

In Ruby, how do Range#include? and Range#cover? differ, and why can ('1'..'10').include?('5') and cover?('5') disagree?

level: middleimportance: should knowfreq 45%

answer

  1. numbers: both just compare
  2. strings: include? walks String#succ
  3. cover? only uses <=> on the endpoints
  4. cover? also accepts a Range
  5. open string range: TypeError from include?

basics

~10 s

For numeric ranges the two agree. For String ranges, include? walks the String#succ sequence while cover? only checks begin <= value <= end with <=>, so ('1'..'10').include?('5') is true but cover?('5') is false.

solid answer

~30 s

When the endpoints are numeric (or `Time`), `include?` behaves like `cover?`: both compare the value against `begin` and `end`. For String ranges they differ. `cover?` only checks `begin <= value <= end` using `<=>`, which is lexicographic, so `('a'..'z').cover?('bb')` is `true` and `('1'..'10').cover?('5')` is `false`, because "5" sorts after "10". `include?` (alias `member?`) asks whether the value is one of the strings the range would generate with `String#succ`, so `('a'..'z').include?('bb')` is `false` and `('1'..'10').include?('5')` is `true`. `include?` on an endless or beginless String range raises `TypeError`, while `cover?` still answers, and only `cover?` accepts a whole Range as its argument.

code

ruby · 12 lines
ruby
('a'..'z').include?('bb')    # => false
('a'..'z').cover?('bb')      # => true
('1'..'10').include?('5')    # => true
('1'..'10').cover?('5')      # => false, "5" sorts after "10"

opening = ('09:00'..'17:30')
opening.cover?('12:15')      # => true

('a'..).cover?('b')          # => true
('a'..).include?('b')        # TypeError: cannot determine inclusion in beginless/endless ranges

(1..10).cover?(3..5)         # => true

go deeper

for a junior

Recall that cover? is a between check and include? asks about generated elements, and that they agree on numbers.

for a middle

Explain String#succ against <=> ordering, with the ('1'..'10') and ('a'..'z') cases, and the TypeError on open string ranges.

for a senior

Reach for cover? on bounds checks by default and spot include? on long string ranges as a hidden linear walk in a hot path.

for a principal

Weigh modelling times or codes as strings against real types, since string ranges make interval semantics depend on sort order.

## Two questions a range can answer A Ruby range can be asked two different questions about a value: - **`cover?(value)`**: does the value lie **between** the endpoints? It evaluates `begin <= value <= end` (or `< end` for a three-dot range) with the `<=>` operator. - **`include?(value)`**, also spelled **`member?`**: is the value one of the range's **elements**, the values it would produce when iterated? For most ranges in real code the two give the same answer, which is why the difference surprises people when it appears. ## Numeric ranges: the same answer When either endpoint is an `Integer`, `Float`, another `Numeric` or a `Time`, `include?` takes the same comparison path as `cover?`: - `(1..3).include?(1.5)` is `true`, even though iterating `1..3` never yields 1.5. - `(1..).include?(10**12)` is `true` immediately, with no iteration. So on numeric ranges both methods are constant-time comparisons. ## String ranges: the answers diverge For a range whose endpoints are both strings, `include?` asks whether the value appears in the sequence `begin`, `begin.succ`, `begin.succ.succ` and so on, up to `end`. `cover?` still compares with `<=>`, which orders strings character by character. | Expression | `include?` | `cover?` | Why | |---|---|---|---| | `('a'..'z')` with `'bb'` | `false` | `true` | `'bb'` sorts between `'a'` and `'z'`, but `succ` never produces it before `'z'` | | `('1'..'10')` with `'5'` | `true` | `false` | all-digit strings step numerically, but `'5'` sorts after `'10'` | | `('a'..'d')` with `'c'` | `true` | `true` | both agree for single characters | Two cost facts follow: 1. For **single ASCII characters** `include?` takes a fast comparison path. 2. For **longer strings** it generates candidate strings one by one until it finds the value or passes the end, so its cost grows with the distance between the endpoints. ## Open-ended ranges and other types - **Endless or beginless String ranges**: `('a'..).include?('b')` raises `TypeError` with `cannot determine inclusion in beginless/endless ranges`, while `('a'..).cover?('b')` returns `true`. - **Other non-numeric types**: `include?` falls back to `Enumerable#include?`, which iterates with `each`. That needs `succ` on the begin value and can be slow. - **Non-comparable values**: `cover?` returns `false` when `<=>` returns `nil`, as in `(1..4).cover?('foo')`. ## Why two methods exist `include?` is the Enumerable idea of membership: is the value one of the elements that `each` would yield? For numbers that question is answered by comparison, because iterating could be enormous or, for floats, impossible, so Range takes the shortcut. For strings the element sequence is well defined through `String#succ`, so `include?` keeps the element meaning, and the lexicographic ordering that `cover?` uses can disagree with it. `cover?` never iterates for any type, which is why it is the safe default whenever you mean "between". ## cover? with a Range argument `cover?` also accepts a Range and returns `true` when that range lies entirely inside the receiver: - `(1..10).cover?(3..5)` is `true`. - `(1...4).cover?(1..4)` is `false`, because 4 is excluded from the receiver. `include?` has no such form; passed a range, it treats it as a single value. ## Picking one A clinic booking tool keeps opening hours as zero-padded `"HH:MM"` strings, `"09:00".."17:30"`. The question it asks is "is 12:15 between opening and closing?", which is a **between** question, so `cover?` is right: zero-padded times sort correctly as strings, and the check is two comparisons. `include?` would step through `String#succ` values one at a time and would treat the strings as a sequence rather than an interval. The rule of thumb: - Use **`cover?`** for bounds checks on any comparable values. - Use **`include?`** only when you mean "one of the generated elements", which for strings is rare.

  • Why does (1..3).include?(1.5) return true when iterating 1..3 never yields 1.5?
    For numeric endpoints `include?` does not iterate. It takes the same path as `cover?` and checks `1 <= 1.5 <= 3`, which is true. Element-by-element membership only applies to String ranges and to other non-numeric types, where `include?` falls back to walking the range.
  • How would you check that a whole appointment range fits inside opening hours?
    Call `cover?` with the range: `(540...1020).cover?(600...660)` is `true` when the slot lies entirely within the opening minutes. `include?` cannot do this, since it treats the argument as one value. Mind exclusive ends: `(1...4).cover?(1..4)` is `false`.

saying these in an interview costs you the question

  • include? and cover? are aliases that always agree
  • cover? iterates the range, so it is slower than include?
  • include? on a numeric range walks every integer
  • ('a'..).include?('b') returns true like cover? does
  • cover? checks string ranges by generating each string