9

I know that I can do:

(1..30).cover?(2)
=> true

But when I try to do the same with another range it always returns false:

(1..30).cover?(2..3)
=> false

So my question is - is there any elegant way to compare two ranges in ruby? In my case I want to check if two DateTime-ranges overlap. Thanks in advance.

Bergrebell
  • 4,263
  • 4
  • 40
  • 53

6 Answers6

16

Two ranges overlap for a given range A when:

  1. range B starts within range A,
  2. range B ends within range A, or
  3. range B starts before range A and ends after range A.

Examples:

Range A    |-----|
             |-----|  Case 1
         |-----|      Case 2
             |-|      Case 1 + 2
         |---------|  Case 3

Looking closer, the rule is: Two ranges overlap when Range B starts before the range A ends and range B ends after the range A starts.

def ranges_overlap?(range_a, range_b)
  range_b.begin <= range_a.end && range_a.begin <= range_b.end 
end 
spickermann
  • 100,941
  • 9
  • 101
  • 131
  • The `cover?` method does not check for overlap though, right? `(2..4).cover?(1..3) => false` for example. – lsunsi Sep 14 '20 at 10:02
  • 1
    @lsunsi You are right. At the time of writing, I thought that the new `Range#cover?` method would return `true` too when there is only an overlap. But that is not the case. `a.cover?(b)` only returns `true` when `a` complete covers `b`. Partial overlaps will return `false`. I removed the note on `Range#cover?`. It doesn't make sense in the context of the OP's use-case. – spickermann Sep 14 '20 at 10:09
8
def overlap?(r1,r2)
  !(r1.first > r2.last || r1.last < r2.first)
end

overlap? 1..5, 4..10 #=> true
overlap? 1..5, 6..10 #=> false
overlap? 1..10, 4..8 #=> true
overlap? 1..4, 4..8  #=> true

The operative line is equivalent to:

r1.first <= r2.last && r1.last >= r2.first

I normally try to avoid negation, but in this case I think it reads better with it.

Another way:

def overlap?(r1,r2)
  !(([r1.first, r2.first].min..[r1.last, r2.last].max).size >= r1.size + r2.size)
end

overlap? 1..5, 4..10 #=> true
overlap? 1..5, 6..10 #=> false
overlap? 1..10, 4..8 #=> true
overlap? 1..4, 4..8  #=> true

The operative line is equivalent to:

([r1.first, r2.first].min..[r1.last, r2.last].max).size < r1.size + r2.size

Again, I prefer the one with negation.

Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100
5

In rails You can use (1..3).overlaps?(2..4) # true

https://apidock.com/rails/Range/overlaps

O.Vykhor
  • 461
  • 1
  • 5
  • 18
5

If you want the rails implementation you can use

class Range
  def overlaps?(other)
    cover?(other.first) || other.cover?(first)
  end
end
(1..5).overlaps?(4..6) # => true
(1..5).overlaps?(7..9) # => false
JuJoDi
  • 14,627
  • 23
  • 80
  • 126
2

While the conversions may be wasteful, semantically comparing sets seems to make the most sense:

Set.new(1..30).superset?(Set.new(2..3))
#=> true
Set.new(1..30).superset?(Set.new(0..3))
#=> false

If you don't want to do that, you can do something like this (with r1 and r2 being ranges):

r1.cover?(r2.min) && r1.cover?(r2.max)
Michael Kohl
  • 66,324
  • 14
  • 138
  • 158
0

You can check Overlapping using range1.first < range2.last && range2.first < range1.last
You can add it as an instance method of range or as a helper method somewhere in you data. source: https://stackoverflow.com/a/325964/4091324

Community
  • 1
  • 1
Darth Kotik
  • 2,261
  • 1
  • 20
  • 29