Detecting DateTime timespan overlap in Ruby
The Scenario
Let’s say you have an instance of DateTime that represents a starting time, and you have a duration that, when added to the starting time, represents an ending time.
Let’s say you have two such things and you want to know if they overlap (or intersect, or collide). For instance:
- March 29, 2009 @ 10:00am; 120 minutes
- March 29, 2009 @ 11:00am; 90 minutes
In this example there’s one hour of overlap in which #1 stretches from 10:00am – 12:00pm and #2 stretches from 11:00am – 12:30pm.
How to detect this? I’m sure there’s many clever ways, this way is mine.
We’re Gonna Need More Monkeys
First, we need to monkey-patch some core classes. Monkey-patching is fun, it makes us feel 31337.
As an aside: it’s patently absurd that DateTime doesn’t have a built-in function for converting to a Time instance (which is layered atop the patently absurd need for Ruby to have three unique Date/Time classes… but that’s another rant)….
Anyhow, let’s monkey-patch DateTime so it can return itself as an instance of Time:
class DateTime
def to_time
return Time.mktime( year, month, day, hour, min, sec )
end
end
Now we need to alter Range to detect intersections with other ranges the way Array does with its & operator. We do this with Bill Siggelkow’s clever intersection() method:
class Range
def intersection(range)
res = self.to_a & range.to_a
res.empty? ? nil : (res.first..res.last)
end
alias_method :&, :intersection
end
Ta-da; monkey-patching complete.
DateTime is the VB.Net of Date/Time Classes
You’ve likely inferred from the code above that we won’t be using DateTime for this jaunty action, but rather instances of Time. You’d be correct. DateTime is remarkably unsuited for this sort of thing. But since DateTime is so popular and prevalent that’s where we’ll start.
Our first moment of comparison will start right now to 60 minutes in the future:
a = DateTime.now a_start = a.to_time a_end = a_start + ( 60 * 60 )
Our second moment in time will start 30 minutes from now to 120 minutes in the future:
b = DateTime.now b_start = b.to_time b_start = b_start + ( 30 * 60 ) b_end = b_start + ( 120 * 60 )
Free-range Comparison
Now that we have our times, let’s convert them into ranges:
# Turn these into ranges a_range = (a_start.to_i..a_end.to_i) b_range = (b_start.to_i..b_end.to_i)
and compare them:
puts "Result A-B: #{a_range.intersection( b_range )}"
As of this writing the output is:
Result A-B: 1238340860..1238342660
which indicates an overlap/intersection/collision between the two timespans, displayed as seconds. Had there been no collision, nil would have been returned instead.
2,262 views
By chris on March 29th 2009 in /dev/random
