Daylight Ravings
Posted on November 3rd, 2008 3 Comments »
Any developer who's had to deal with Date/Time calculations knows there are myriad issues to navigate, particularly around time zone and Daylight Saving handling. Today (Mon 2008.11.03), I ran our tests and stumbled upon the following code (simplified for this post) that was breaking:
# relapse_test.rb
def test_middle_time
t = Time.now
r = Relapse.new(:start_time => t-5.days, :end_time => t-1.day)
middle = t-3.days
assert_equal middle, r.middle_time # Breaking on Mon 2008.11.03
end
# Relapse class in relapse.rb
def middle_time
return start_time + (end_time - start_time)/2
end
Results of the test:
<Fri Oct 31 11:27:18 -0400 2008> expected but was
<Fri Oct 31 11:57:18 -0400 2008>.
The purpose of middle_time() is to return the time halfway between the start and end times. The code seemed straightforward, but for some reason, the expected and returned values differed by 30 minutes.
As I stared at the code, there was another nagging question - why was this happening now when it wasn't prior, before the weekend. Hmmm ... a Halloween trick? To get more insight, I added some print statements to echo the times used by the middle_time calculation and got the following:
Start: Wed Oct 29 11:27:18 -0400 2008
End: Sun Nov 02 11:27:18 -0500 2008
Bingo. Check out the time zone offset: -0400 for start time and -0500 for end. What happened over the weekend? Halloween in Salem was a good guess but the culprit was the switch from Daylight Saving back to Standard Time. Because of the time zone disparity, there is an extra hour between the start time, taking place in Daylight Saving time, and the end time in Standard Time. So (end_time - start time) in middle_time() adds an extra hour (4 days plus one hour) and, when divided by 2, produces the 30 minute discrepancy between expected and actual time.
Proper handling depends on your situation. In ours, we were interested in the middle DATE rather than the time, so the implementation was changed to use dates v. times. For situations where time does matter, the 30 minute discrepancy may, in fact, be desired as it factors in the number of hours that have elapsed. But if you want the calculation to be time zone-consistent (or agnostic), you can use the UTC (GMT) time zone; Time.now.utc returns the current UTC time. Calculations can be performed in UTC, which does not shift for Daylight Saving (or any other reason), and converted back to a zone-specific time as needed.