Ruby: intersection between two ranges

2019-01-18 12:57发布

问题:

In ruby, given two date ranges, I want the range that represents the intersection of the two date ranges, or nil if no intersection. For example:

(Date.new(2011,1,1)..Date.new(2011,1,15)) & (Date.new(2011,1,10)..Date.new(2011,2,15))
=> Mon, 10 Jan 2011..Sat, 15 Jan 2011

Edit: Should have said that I want it to work for DateTime as well, so interval can be down to mins and secs:

(DateTime.new(2011,1,1,22,45)..Date.new(2011,2,15)) & (Date.new(2011,1,1)..Date.new(2011,2,15))
=> Sat, 01 Jan 2011 22:45:00 +0000..Tue, 15 Feb 2011

回答1:

require 'date'

class Range
  def intersection(other)
    return nil if (self.max < other.begin or other.max < self.begin) 
    [self.begin, other.begin].max..[self.max, other.max].min
  end
  alias_method :&, :intersection
end

p (Date.new(2011,1,1)..Date.new(2011,1,15)) & (Date.new(2011,1,10)..Date.new(2011,2,15))
#<Date: 2011-01-10 ((2455572j,0s,0n),+0s,2299161j)>..#<Date: 2011-01-15 ((2455577j,0s,0n),+0s,2299161j)>


回答2:

You can try this to get a range representing intersection

range1 = Date.new(2011,12,1)..Date.new(2011,12,10)
range2 = Date.new(2011,12,4)..Date.new(2011,12,12)

inters = range1.to_a & range2.to_a

intersected_range = inters.min..inters.max

Converting your example:

class Range  
  def intersection(other)  
    raise ArgumentError, 'value must be a Range' unless other.kind_of?(Range)  

    inters = self.to_a & other.to_a

    inters.empty? ? nil : inters.min..inters.max 
  end  

  alias_method :&, :intersection  
end


回答3:

I found this: http://www.postal-code.com/binarycode/2009/06/06/better-range-intersection-in-ruby/ which is a pretty good start, but does not work for dates. I've tweaked a bit into this:

class Range  
  def intersection(other)  
    raise ArgumentError, 'value must be a Range' unless other.kind_of?(Range)  

    new_min = self.cover?(other.min) ? other.min : other.cover?(min) ? min : nil  
    new_max = self.cover?(other.max) ? other.max : other.cover?(max) ? max : nil  

    new_min && new_max ? new_min..new_max : nil  
  end  

  alias_method :&, :intersection  
end

I've omitted the tests, but they are basically the tests from the post above changed for dates. This works for ruby 1.9.2.

Anyone got a better solution?



回答4:

I baked this solution for ascending ranges, also taking care of the exclude end situations:

intersect_ranges = ->(r1, r2) do
  new_end = [r1.end, r2.end].min
  new_begin = [r1.begin, r2.begin].max
  exclude_end = (r2.exclude_end? && new_end == r2.end) || (r1.exclude_end? && new_end == r1.end)

  valid = (new_begin <= new_end && !exclude_end) 
  valid ||= (new_begin < new_end && exclude_end))
  valid ? Range.new(new_begin, new_end, exclude_end) : nil
end

I'm also a bit worried by you guys adding it to the Range class itself, since the behavior of intersecting ranges is not uniformly defined. (How about intersecting 1...4 and 4...1? Why nil when there is no intersection; we could also say this is an empty range: 1...1 )



回答5:

Try something like this

require 'date'
sample = Date.parse('2011-01-01')
sample1 = Date.parse('2011-01-15')
sample2 = Date.parse('2010-12-19')
sample3 = Date.parse('2011-01-11')

puts (sample..sample1).to_a & (sample2..sample3).to_a

What this will give you is a array of intersection dates!!



回答6:

I have times as [[start, end], ...] and I want to remove the some time ranges from a each initial time range, here is what I did:

def exclude_intersecting_time_ranges(initial_times, other_times)
  initial_times.map { |initial_time|
    other_times.each do |other_time|
      next unless initial_time
      # Other started after initial ended
      next if other_time.first >= initial_time.last
      # Other ended before initial started
      next if other_time.last <= initial_time.first

      # if other time started before and ended after after, no hour is counted
      if other_time.first <= initial_time.first && other_time.last >= initial_time.last
        initial_time = nil
      # if other time range is inside initial time range, split in two time ranges
      elsif initial_time.first < other_time.first && initial_time.last > other_time.last
        initial_times.push([other_time.last, initial_time.last])
        initial_time = [initial_time.first, other_time.first]
      # if start time of other time range is before initial time range
      elsif other_time.first <= initial_time.first
        initial_time = [other_time.last, initial_time.last]
      # if end time of other time range if after initial time range
      elsif other_time.last >= initial_time.last
        initial_time = [initial_time.first, other_time.first]
      end
    end

    initial_time
  }.compact
end


回答7:

Since this question is related to How to combine overlapping time ranges (time ranges union), I also wanted to post my finding of the gem range_operators here, because if helped me in the same situation.



回答8:

I'd transfer them into an array, since arrays know the intersection-operation:

(Date.new(2011,1,1)..Date.new(2011,1,15)).to_a & (Date.new(2011,1,10)..Date.new(2011,2,15)).to_a

Of course this returns an Array. So if you want an Enumerator (Range doesn't seem to be possible since these are not consecutive values anymore) just throw to_enum at the end.



标签: ruby date range