0

For a Ruby on Rails planning application I am running into an algorithm / combination problem that I have trouble solving efficiently.

In my application I have 2 types of records:

  • Availabilities (when is someone freely available, on stand-by or explicitly unavailable (sick, vacation))
  • Plan records (when is someone actually scheduled in).

Both types of records are defined by a start and end time, and availabilities have an additional type (available, stand-by, unavailable).

Now I would like to get a flat list of non-overlapping periods that show me when someone has plan records first, but additionally has availabilities

To give an example:

Time:  0-----------6-----------12-----------18-----------24
Avail:     |-----available-----||--standby--|
Plans:             |------------------|

Result:    |------||------------------||----|

The desired result is 3 non-overlapping periods:

  • 3-6: Available
  • 6-15: Planned
  • 15-18: Standby

Another example, where an Availability needs to be split:

Time:  0-----------6-----------12-----------18-----------24
Avail:     |-----available-----||--standby--|
Plans:             |-----|

Result:    |------||-----||----||-----------|

The desired result is 3 non-overlapping periods:

  • 3-6: Available
  • 6-9: Planned
  • 9-12: Available
  • 12-18: Standby

I already have all (overlapping) periods in an array. What is the best way to achieve what I want efficiently?

ChrisDekker
  • 1,584
  • 18
  • 39
  • Please read "[ask]" including the linked pages, "[mcve]" and "[How much research effort is expected of Stack Overflow users?](http://meta.stackoverflow.com/questions/261592)". We'd like to see evidence of your effort. What did you try? Did you search and not find anything? Did you find stuff but it didn't help? Did you try writing code? If not, why? If so, what is the smallest code example that shows what you tried and why didn't it work? Without that it looks like you didn't try and want us to write it for you. – the Tin Man Apr 17 '17 at 17:42

1 Answers1

0

I assume that we are given hour ranges for 'plan', 'available' and 'coverage', where 'coverage' is the 'available` range preceded and followed by 'stand-by' ranges. Moreover, 'coverage' contains 'plan' and either or both of its 'stand-by' ranges may be of zero duration.

Code

def categories(avail, plan)
  adj_avail = adj_avail(avail)
  adj_plan  = adj_plan(avail, plan)
  arr = []
  finish = [adj_avail[:available][:start], adj_plan[:start]].min
  add_block(arr, :standby, adj_avail[:coverage][:start], finish)
  start = finish
  finish = [adj_avail[:available][:finish], adj_plan[:start]].min
  add_block(arr, :available, start, finish)
  start = finish
  add_block(arr, :standby, finish, adj_plan[:start])
  arr << [:plan, adj_plan]
  finish = [adj_plan[:finish], adj_avail[:available][:finish]].max
  add_block(arr, :available, adj_plan[:finish], finish)
  add_block(arr, :standby, finish, adj_avail[:coverage][:finish])
  restore_times(arr)
end

def adj_avail(avail)
  avail.each_with_object({}) do |(k,g),h|
    start, finish = g[:start], g[:finish]
    h[k] = case k
    when :coverage
      { start:  start, finish: finish + (finish < start ? 24 : 0) }
    else # when :available
      { start:  start + (start < h[:coverage][:start] ? 24 : 0), 
        finish: finish + (finish < start ? 24 : 0) }
    end
  end
end

def adj_plan(avail, plan)
  { start:  plan[:start]  + (plan[:start]  < avail[:coverage][:start] ? 24 : 0),
    finish: plan[:finish] + (plan[:finish] < plan[:start] ? 24 : 0) }
end

def add_block(arr, value, curr_epoch, nxt_epoch)  
  arr << [value, { start: curr_epoch, finish: nxt_epoch }] if nxt_epoch > curr_epoch
end

def restore_times(arr)
  arr.map! do |k,g|
    start, finish = g.values_at(:start, :finish)
    start  -= 24 if start  > 24
    finish -= 24 if finish > 24
    [k, { start: start, finish: finish }]
  end
end

Examples

avail = { coverage:  { start: 3, finish: 18 },
          available: { start: 3, finish: 12 } }
plan  = { start: 6, finish: 15 }
categories(avail, plan)
  #=> [[:available, {:start=>3, :finish=>6}  ],
  #    [:plan,      {:start=>6, :finish=>15} ],
  #    [:standby,   {:start=>15, :finish=>18}]] 

avail = { coverage:  { start: 22, finish: 11 },
          available: { start: 23, finish: 10 } }
plan  = { start: 24, finish: 9 }
categories(avail, plan)
  #=> [[:standby,   {:start=>22, :finish=>23}],
  #    [:available, {:start=>23, :finish=>24}],
  #    [:plan,      {:start=>24, :finish=>9 }],
  #    [:available, {:start=>9,  :finish=>10}],
  #    [:standby,   {:start=>10, :finish=>11}]] 

avail = { coverage:  { start: 1, finish: 13 },
          available: { start: 2, finish: 3 } }
plan  = { start: 4, finish: 12 }
categories(avail, plan)
  #=> [[:standby,   {:start=>1,  :finish=>2 }],
  #    [:available, {:start=>2,  :finish=>3 }],
  #    [:standby,   {:start=>3,  :finish=>4 }],
  #    [:plan,      {:start=>4,  :finish=>12}],
  #    [:standby,   {:start=>12, :finish=>13}]]

Explanation

The main complication here is when the finish hour for 'coverage' is less than the start hour, meaning that the 'coverage' range contains midnight. When this occurs, the 'available' and 'plan' ranges may also contain midnight. I've dealt with this by adding 24 hours to hours after midnight before computing the ranges and then subtracting 24 hours from all hour values that exceed 24 after computing the ranges.

Consider the second example above.

avail = { coverage:  { start: 22, finish: 11 },
          available: { start: 23, finish: 10 } }
adj_avail(avail)
  #=>   {:coverage=> {:start=>22, :finish=>35},
  #      :available=>{:start=>23, :finish=>34}} 

plan  = { start: 24, finish: 9 }
adj_plan(avail, plan)    
  #=> {:start=>24, :finish=>33}

If I execute categories for these values of avail and plan, with the last line commented out, I obtain

a = categories(avail, plan)    
  #=> [[:standby,   {:start=>22, :finish=>23}],
  #    [:available, {:start=>23, :finish=>24}],
  #    [:plan,      {:start=>24, :finish=>33}],
  #    [:available, {:start=>33, :finish=>34}],
  #    [:standby,   {:start=>34, :finish=>35}]]

and

restore_times(a)
  #=> the return value shown above
Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100