views:

211

answers:

3

Example

business_hours['monday'] = [800..1200, 1300..1700]
business_hours['tuesday'] = [900..1100, 1300..1700]

...

I then have a bunch of events which occupy some of these intervals, for example

event = { start_at: somedatetime, end_at: somedatetime }

Iterating over events from a certain date to a certain date, I create another array

busy_hours['monday'] = [800..830, 1400..1415]

...

Now my challenges are

  • Creating an available_hours array that contains business_hours minus busy_hours

available_hours = business_hours - busy_hours

  • Given a certain duration say 30 minutes, find which time slots are available in available_hours. In the examples above, such a method would return

available_slots['monday'] = [830..900, 845..915, 900..930, and so on]

Not that it checks available_hours in increments of 15 minutes for slots of specified duration.

Thanks for the help!

+1  A: 

To answer your question's title, find if a range of arrays contains a range:

ary = [800..1200, 1300..1700]

test = 800..830
p ary.any? {|rng| rng.include?(test.first) and rng.include?(test.last)}
# => true

test = 1245..1330
p ary.any? {|rng| rng.include?(test.first) and rng.include?(test.last)}
# => false

which could be written as

class Range
  def include_range?(r)
    self.include?(r.first) and self.include?(r.last)
  end
end
glenn jackman
If the range in the Array has overlap, we have to merger first. My 2 cents. http://stackoverflow.com/questions/1233292/whats-a-good-generic-algorithm-for-collapsing-a-set-of-potentially-overlapping/1249209#1249209
pierr
+1  A: 

Okay, I don't have time to write up a full solution, but the problem does not seem too difficult to me. I hacked together the following primitive methods you can use to help in constructing your solution (You may want to subclass Range rather than monkey patching, but this will give you the idea):

class Range
  def contains(range)
    first <= range.first || last >= range.last
  end

  def -(range)
    out = []
    unless range.first <= first && range.last >= last
      out << Range.new(first, range.first) if range.first > first
      out << Range.new(range.last, last) if range.last < last
    end
    out
  end
end

You can iterate over business hours and find the one that contains the event like so:

event_range = event.start_time..event.end_time
matching_range = business_hours.find{|r| r.contains(event_range)}

You can construct the new array like this (pseudocode, not tested):

available_hours = business_hours.dup
available_hours.delete(matching_range)
available_hours += matching_range - event_range

That should be a pretty reusable approach. Of course you'll need something totally different for the next part of your question, but this is all I have time for :)

dasil003
Hi dasil003. Thanks for the reply. Those Range methods look outstanding. Gonna put them in my standard library.Indeed the second part is tricky. Have a look at EmFi's solution though. Really outstanding.
Alexandre
EmFi
+1  A: 

I think this is a job for bit fields. Unfortunately this solution will rely on magic numbers, conversions helpers and a fair bit of binary logic, so it won't be pretty. But it will work and be very efficient.

This is how I'd approach the problem:

Atomize your days into reasonable time intervals. I'll follow your example and treat each 15 minute block of time as considered one time chunk (mostly because it keeps the example simple). Then represent your availability per hour as a hex digit.

Example:

  • 0xF = 0x1111 => available for the whole hour.
  • 0xC = 0x1100 => available for the first half of the hour.

String 24 of these together together to represent a day. Or fewer if you can be sure that no events will occur outside of the range. The example continues assuming 24 hours.

From this point on I've split long Hex numbers into words for legibility Assuming the day goes from 00:00 to 23:59 business_hours['monday'] = 0x0000 0000 FFFF 0FFF F000 0000

To get busy_hours you store events in a similar format, and just & them all together.

Exmample:

event_a = 0x0000 0000 00F0 0000 0000 0000 # 10:00 - 11:00 
event_b = 0x0000 0000 0000 07F8 0000 0000 # 13:15 - 15:15

busy_hours = event_a & event_b

From busy_hours and business_hours you can get available hours:

available_hours = business_hours & (busy_hours ^ 0xFFFF FFFF FFFF FFFF FFFF FFFF)

The xor(^) essentialy translates busy_hours into not_busy_hours. Anding (&) not_busy_hours with business_hours gives us the available times for the day.

This scheme also makes it simple to compare available hours for many people.

all_available_hours = person_a_available_hours & person_b_available_hours & person_c_available_hours

Then to find a time slot that fits into available hours. You need to do something like this: Convert your length of time into a similar hex digit to the an hour where the ones represent all time chunks of that hour the time slot will cover. Next right shift the digit so there's no trailing 0's.

Examples are better than explanations: 0x1 => 15 minutes, 0x3 => half hour, 0x7 => 45 minutes, 0xF => full hour, ... 0xFF => 2 hours, etc.

Once you've done that you do this:

acceptable_times =[]
(0 .. 24 * 4 - (#of time chunks time slot)).each do |i|
  acceptable_times.unshift(time_slot_in_hex) if available_hours & (time_slot_in_hex << i) == time_slot_in_hex << i
end

The high end of the range is a bit of a mess. So lets look a bit more at it. We don't want to shift too many times or else we'll could start getting false positives at the early end of the spectrum.

24 * 4 24 hours in the day, with each represented by 4 bits. - (#of time chunks in time slot) Subtract 1 check for each 15 minutes in the time slot we're looking for. This value can be found by (Math.log(time_slot_in_hex)/Math.log(2)).floor + 1

Which starts at the end of the day, checking each time slot, moving earlier by a time chunk (15 minutes in this example) on each iteration. If the time slot is available it's added to the start of acceptable times. So when the process finishes acceptable_times is sorted in order of occurrence.

The cool thing is this implementation allows for time slots that incorporate so that your attendee can have a busy period in their day that bisects the time slot you're looking for with a break, where they might be otherwise busy.

It's up to you to write helper functions that translate between an array of ranges (ie: [800..1200, 1300..1700]) and the hex representation. The best way to do that is to encapsulate the behaviour in an object and use custom accessor methods. And then use the same objects to represent days, events, busy hours, etc. The only thing that's not built into this scheme is how to schedule events so that they can span the boundary of days.

EmFi
Thanks a bunch for the help. Do you have a background in Assembler? ;) Those folks love bit logic! I'm gonna try to implement this solution. What's your email? I'll get back to you if I get this working. Will also try to make this available on github.
Alexandre
No background in assembler. I just know enough about algorithms binary logic to know when it's useful to drop down to such a basic level.
EmFi