skip to content

In Ruby 3.3 and later, how does Range#overlap? decide whether two appointment slots overlap, and how do exclusive ends change the answer?

level: middleimportance: should knowfreq 35%

answer

  1. added in Ruby 3.3
  2. shares at least one value
  3. back-to-back half-open slots: false
  4. empty range never overlaps
  5. non-Range argument: TypeError

basics

~10 s

Range#overlap?, added in Ruby 3.3, returns true when two ranges share at least one value. Back-to-back exclusive slots such as (540...600) and (600...660) do not overlap, while an inclusive (540..600) touches 600 and does.

solid answer

~30 s

`Range#overlap?(other)` arrived in Ruby 3.3 and returns `true` when the two ranges have at least one value in common. It needs a Range argument (`TypeError` otherwise), returns `false` when either range is empty, such as `(4..1)` or `(2...2)`, and returns `false` when `<=>` cannot compare the endpoints. Exclusive ends matter at the boundary: `(540...600).overlap?(600...660)` is `false` because 600 is excluded from the first slot, while `(540..600).overlap?(600...660)` is `true`. So model appointment slots as half-open ranges, `start...finish`, and back-to-back bookings stop colliding. Ruby 4.0 also fixed how `overlap?` handles beginless and endless ranges. Use `cover?(range)` instead when you need full containment.

code

ruby · 12 lines
ruby
OPENING = (540...1020)                  # 09:00 to 17:00, in minutes
booked  = [(540...600), (660...720)]     # 09:00-10:00 and 11:00-12:00

def bookable?(slot, booked)
  OPENING.cover?(slot) && booked.none? { |b| b.overlap?(slot) }
end

bookable?((600...660), booked)   # => true, touches both neighbours
bookable?((590...650), booked)   # => false, overlaps 09:00-10:00
bookable?((990...1050), booked)  # => false, runs past closing

(540..600).overlap?(600...660)   # => true, inclusive end touches 600

go deeper

for a junior

Recall that overlap? means sharing any value, that it arrived in Ruby 3.3, and that it needs a Range argument.

for a middle

Explain the boundary rule with exclusive ends, the empty-range rule, and how overlap? differs from cover? with a range.

for a senior

Model every slot as a half-open range so touching bookings are legal, and replace hand-written comparisons that break on inclusive or nil ends.

for a principal

Decide how the domain represents intervals across services and storage, so every layer agrees on whether the end instant is included.

## What overlap? answers `Range#overlap?(other)` returns `true` when the receiver and `other` share **at least one value**, and `false` otherwise. It was added in **Ruby 3.3**; before that, code wrote the comparison by hand. Ruby 4.0 corrected its handling of **unbounded** ranges, those with a `nil` begin or end, so answers involving `(0..)` or `(..10)` should be checked against 4.0. ```ruby (0..2).overlap?(1..3) # => true (0..2).overlap?(3..4) # => false (0..).overlap?(..0) # => true ``` ## The rules it applies The method compares endpoints with `<=>` and never iterates, so it is cheap for any comparable type: 1. **The argument must be a Range.** `(1..3).overlap?(1)` raises `TypeError`. 2. **Incomparable endpoints give `false`.** `(1..3).overlap?('a'..'d')` is `false`, because `<=>` returns `nil`. 3. **An empty range overlaps nothing.** A range is empty when its begin is larger than its end, or equal to it for a three-dot range: `(4..1)` and `(2...2)` both return `false`. 4. **The boundary counts only if both sides include it.** If one range ends exactly where the other begins, they overlap only when the ending range includes its end. ## Exclusive ends and back-to-back slots The fourth rule is the one interviews probe: | Receiver | Argument | `overlap?` | Why | |---|---|---|---| | `(540...600)` | `(600...660)` | `false` | 600 is excluded from the receiver | | `(540..600)` | `(600...660)` | `true` | both contain 600 | | `(540...600)` | `(599...660)` | `true` | 599 is in both | | `(4..5)` | `(2...4)` | `false` | 4 is excluded from the argument | This is why appointment slots are best modelled as **half-open** ranges, `start...finish`. A 9:00-10:00 appointment is `(540...600)` in minutes after midnight, and a 10:00-11:00 one is `(600...660)`. They sit back to back without colliding, and the length of each slot is simply `finish - start`. ## overlap? compared with cover? The two methods answer different questions: - **`overlap?`**: do the ranges share any value? True for a slot that sticks out of the day. - **`cover?(range)`**: does the argument lie **entirely** inside the receiver? True only for a slot that fits. A booking check usually needs both: the slot must be covered by opening hours and must not overlap any existing booking. ## Unbounded ranges A `nil` begin or end means the range runs on without limit in that direction, and `overlap?` treats it that way: - `(0..).overlap?(..0)` is `true`, because both contain 0. - `(..5).overlap?(..3)` is `true`, because two beginless ranges with comparable ends share their lower values. - `(1..).overlap?(3..2)` is `false`, because the argument is empty. Ruby 4.0 fixed how `overlap?` handles these infinite cases, so on 3.3 and 3.4 some unbounded comparisons could answer differently; test them on the version you run. ## Before Ruby 3.3 Older code expressed the same test by hand for half-open ranges as `a.begin < b.end && b.begin < a.end`. That formula is correct only for three-dot ranges with both endpoints present; with inclusive ends the comparisons change to `<=`, and a `nil` endpoint makes the comparison raise (`ArgumentError` when an Integer is compared with `nil`, `NoMethodError` when `nil` is the receiver). Replacing hand-written checks with `overlap?` removes those edge cases, provided the application runs on Ruby 3.3 or later. ## A clinic booking check - Store **opening hours** as one half-open range of minutes, for example `(540...1020)` for 9:00 to 17:00. - Store **existing bookings** as half-open ranges too. - Accept a new slot when opening hours `cover?` it and no existing booking `overlap?`s it. The code example shows this check; the only design decision is using `...` everywhere so touching slots are allowed.

  • What does (2...2).overlap?(1..3) return, and why?
    `false`. `(2...2)` is empty, because its begin equals its excluded end, and `overlap?` returns `false` whenever either range is empty, even though 2 lies inside `1..3`.
  • Why prefer overlap? over the hand-written a.begin < b.end && b.begin < a.end?
    The formula only holds for exclusive ranges with both ends present. Inclusive ends need `<=`, and a `nil` endpoint makes the comparison raise an error instead of answering. `overlap?` handles inclusive, exclusive, empty and, with Ruby 4.0's fix, unbounded ranges in one call.

saying these in an interview costs you the question

  • (540...600).overlap?(600...660) is true because both mention 600
  • overlap? iterates both ranges to find a shared element
  • overlap? accepts a single value and checks membership
  • Range#overlap? has been in Ruby since endless ranges arrived
  • overlap? is true only when one range contains the other