Dennis Nedry
Dennis Nedry

Reputation: 4748

Check if two ranges overlap in ruby

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.

Upvotes: 9

Views: 6490

Answers (7)

spickermann
spickermann

Reputation: 106802

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 

Or, when you are using the latest version of Ruby, then you can use the Range#overlap? method that was introduced in Ruby 3.3, like this:

range_a.overlap?(range_b)

Upvotes: 16

mechnicov
mechnicov

Reputation: 15248

There is Range#overlap? In Ruby 3.3+

It returns true if ranges overlap each other, otherwise false

It works with different kinds of ranges:

(5..10).overlap?(10..20)
# => true

('a'..'e').overlap?('b'..'f')
# => true

It returns false when begin > end:

(5..1).overlap?(2..3)
# => false

It returns false when try to check incomparable kinds of ranges:

(5..10).overlap?('a'..'e') 
# => false

It works with exclusive ranges:

(5..10).overlap?(1...5)
# => false

(1...5).overlap?(5..10)
# => false

It works with endless and beginless ranges:

(1..).overlap?(..1)
# => true

(1..).overlap?(...1)
# => false

It raises error when try to pass not range

(1..5).overlap?(1)
# raise TypeError (expected Range)

Upvotes: 3

JuJoDi
JuJoDi

Reputation: 14965

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

Upvotes: 6

O.Vykhor
O.Vykhor

Reputation: 479

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

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

Upvotes: 4

Cary Swoveland
Cary Swoveland

Reputation: 110675

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.

Upvotes: 8

Darth Kotik
Darth Kotik

Reputation: 2351

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

Upvotes: 0

Michael Kohl
Michael Kohl

Reputation: 66837

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)

Upvotes: 2

Related Questions