nicoolas25
7/4/2012 - 12:00 PM

Some class that may be useful for dealing with time ranges.

Some class that may be useful for dealing with time ranges.

# This class assumes that you have ranges associated to a value
# and that you want to merge those values. This is, for instance,
# useful to count overlaps of date ranges.
#
# Example of uses:
#
#   rm = RangeMixer.new(0..10, 0)
#   rm.add(0..5){ |memo| memo + 1 }
#   rm.add(0..5){ |memo| memo + 4 }
#
#   puts rm.hash.inspect # => {0=>5, 5=>4, 7=>0}
#
# The previous example can also be written as:
#
#   rm = RangeMixer.new(0..10, 0){ |memo, value| memo + value }
#   rm.add(0..5, 1)
#   rm.add(0..7, 4)
#
# The last method avoid the creation of useless Proc objects.
#
# This is an other context (where ActiveSupport is required):
#
#   from = Date.today ; to = from + 1.month
#   rm = RangeMixer.new(from..to, 0){ |memo, _| memo + 1 }
#   rm.add(from..(from + 1.week))
#   rm.add((from + 1.week)..to)
#
#   # Check if there is an overlap
#   rm.hash.values.any?{ |v| v > 1 } # => false
#
#   rm.add((from + 3.days)..to)
#
#   # Check if there is an overlap
#   rm.hash.values.any?{ |v| v > 1 } # => true
class RangeMixer
  attr_reader :hash

  def initialize(range, memo, &mixer)
    @start = range.begin
    @stop  = range.end
    @memo  = memo
    @mixer = mixer
    @hash  = {@start => @memo}
  end

  def add(range, value=nil, &block)
    return if range.end <= @start || range.begin >= @stop
    insert_steps!(range)
    fill_steps!(range, value || block)
  end

  private

  def fill_steps!(range, pv)
    @hash.each do |step, memo|
      if range.begin <= step && step < range.end
        @hash[step] = pv.kind_of?(Proc) ? pv.call(memo) : @mixer.call(memo, pv)
      end
    end
    @hash
  end

  def insert_steps!(range)
    @hash[range.begin] ||= previous(range.begin) if range.begin > @start
    @hash[range.end]   ||= previous(range.end) if range.end   < @stop
  end

  def previous(step)
    prev = @start
    @hash.keys.sort.find{ |d| if d > step then true else prev = d and false end }
    @hash[prev]
  end
end