6 Jun 2009, 2:54pm
/dev/random
by chris

leave a comment

Better Range Intersection in Ruby

A little while ago I posted a solution for finding intersections between timespans (“Detecting DateTime Timespan Overlap In Ruby”). It works but, as Dan Kubb pointed out to me, it doesn’t use the most graceful means of determining intersections between Range instances, particularly for very large ranges.

Not content to leave it at that, Dan of course provides a better solution, complete with tests:

#!/usr/bin/env ruby

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

    min, max = first, exclude_end? ? max : last
    other_min, other_max = other.first, other.exclude_end? ? other.max : other.last

    new_min = self === other_min ? other_min : other === min ? min : nil
    new_max = self === other_max ? other_max : other === max ? max : nil

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

  alias_method :&, :intersection
end

if __FILE__ == $0
  range = 5..10

  tests = {
    1..4   => nil,     # before
    11..15 => nil,     # after
    1..6   => 5..6,    # overlap_begin
    9..15  => 9..10,   # overlap_end
    1..5   => 5..5,    # overlap_begin_edge
    10..15 => 10..10,  # overlap_end_edge
    5..10  => 5..10,   # overlap_all
    6..9   => 6..9,    # overlap_inner

    1...5  => nil,     # before       (exclusive range)
    1...7  => 5..6,    # overlap_begin      (exclusive range)
    1...6  => 5..5,    # overlap_begin_edge (exclusive range)
    5...11 => 5..10,   # overlap_all  (exclusive range)
    6...10 => 6..9,    # overlap_inner      (exclusive range)
  }

  tests.each do |other, expected|
    result = range.intersection(other)
    result_status = ( result == expected ) ? "passed" : "failed"
    puts "#{range.inspect} #{other.inspect} result #{result_status}: #{result.inspect} (#{expected.inspect})"
  end
end

I like it.