Let full_range be a range, and ranges an array of ranges representing what period and incidents asked. I suggested that the elements contained in all ranges can be any objects, provided that they can be compared with the applicable class' method <=> and that the Comparable module was include d.
the code
def uncovered_ranges(full_range, ranges) ucrs = [full_range] ranges.each do |r| ucrs = ucrs.flat_map do |ucr| if ucr.first >= r.last || ucr.last <= r.first ucr elsif r.first <= ucr.first && r.last >= ucr.last nil elsif r.first <= ucr.first && r.last < ucr.last r.last..ucr.last elsif r.first > ucr.first && r.last >= ucr.last ucr.first..r.first else [ucr.first..r.first, r.last..ucr.last] end end.compact end ucrs end
<strong> Examples
full_range = 1..20 ranges = [3..4, 6..8, 10..12, 8..14, 16..17, 20..20] uncovered_ranges(full_range, ranges) #=> [1..3, 4..6, 14..16, 17..20] require 'time' full_range = Time.parse('8:00am')..Time.parse('8:00pm') #=> 2016-12-22 08:00:00 -0800..2016-12-22 20:00:00 -0800 ranges = [ Time.parse('7:00am')..Time.parse('9:00am'), Time.parse('1:00pm')..Time.parse('3:00pm'), Time.parse('1:30pm')..Time.parse('3:30pm'), Time.parse('7:00pm')..Time.parse('9:00pm'), ] #=> [2016-12-22 07:00:00 -0800..2016-12-22 09:00:00 -0800, # 2016-12-22 13:00:00 -0800..2016-12-22 15:00:00 -0800, # 2016-12-22 13:30:00 -0800..2016-12-22 15:30:00 -0800, # 2016-12-22 19:00:00 -0800..2016-12-22 21:00:00 -0800] uncovered_ranges(full_range, ranges) #=> [2016-12-22 09:00:00 -0800..2016-12-22 13:00:00 -0800, # 2016-12-22 15:30:00 -0800..2016-12-22 19:00:00 -0800]
Explanation
Perhaps the easiest and most capable way to explain what is happening is to insert some puts statements and run the code for the first example above.
def uncovered_ranges(full_range, ranges) ucrs = [full_range] puts "ucrs initially=#{ucrs}" ranges.each do |r| puts "\ncovering range r=#{r}" ucrs = ucrs.flat_map do |ucr| puts " range uncovered so far ucr=#{ucr}" if ucr.first >= r.last || ucr.last <= r.first puts " in if #1, returning #{ucr}" ucr elsif r.first <= ucr.first && r.last >= ucr.last puts " in if #2, returning nil" nil elsif r.first <= ucr.first && r.last < ucr.last puts " in if #3, returning #{r.last..ucr.last}" r.last..ucr.last elsif r.first > ucr.first && r.last >= ucr.last puts " in if #4, returning #{ucr.first..r.first}" ucr.first..r.first else puts " in else, returning #{[ucr.first..r.first, r.last..ucr.last]}" [ucr.first..r.first, r.last..ucr.last] end end.tap { |u| puts "ucrs after processing range #{r}=#{u}" }. compact. tap { |u| puts "ucrs after compact=#{u}" } end ucrs end uncovered_ranges 1..20, [3..4, 6..8, 10..12, 8..14, 16..17, 20..20]
prints the following.
ucrs initially=[1..20]
covering range r=3..4 range uncovered so far ucr=1..20 in else, returning [1..3, 4..20] ucrs after processing range 3..4=[1..3, 4..20] ucrs after compact=[1..3, 4..20]
covering range r=6..8 range uncovered so far ucr=1..3 in if
covering range r=10..12 range uncovered so far ucr=1..3 in if
covering range r=8..14 range uncovered so far ucr=1..3 in if
covering range r=16..17 range uncovered so far ucr=1..3 in if
covering range r=20..20 range uncovered so far ucr=1..3 in if