Monday, May 18, 2015

More Fun with DateTime: Scheduling Items with DateTime or DateTimeOffset?

Last time, we looked at the DateTimeKind property of DateTime. What we saw is that there are some challenges when DateTimeKind is "Unspecified".

Matt Johnson (expert in all things DateTime related), left a possible solution: converting the output of the sunset provider to DateTimeOffset.
"Another way to handle this would be to change the methods of your ISunsetProvider interface to return DateTimeOffset instead of DateTime. In the implementation, you would return new DateTimeOffset(solarTimes.Sunset). The constructor will treat Unspecified as Local, and assign the correct offset."
Matt often recommends using DateTimeOffset, so I looked into it further and swapped the code over to use DateTimeOffset. This has pros and cons (and some unexpected behavior).

I wish this were easy.

DateTimeOffset: What is it Good For?
The primary reason why we want to use DateTimeOffset is that it represents a point in time. It does this by storing the time with an offset from UTC (this is a number like "-07:00", not a time zone like "PST"). This means that DateTimeOffset is not affected by Daylight Saving Time.

Let's compare DateTime and DateTimeOffset by looking at some times. The fun part about this is that we get the "same" time twice when using local time as DateTime.
Before Daylight Saving Time Ends (11/01/2015 09:25 UTC)
11/01/2015 01:25:00 (Local Time - Pacific)
11/01/2015 01:25:00 (DateTimeOffset -07:00)

After Daylight Saving Time Ends (11/01/2015 10:25 UTC)
11/01/2015 01:25:00 (Local Time - Pacific)
11/01/2015 02:25:00 (DateTimeOffset -07:00)
The fun part of using local time is that when someone says it is "01:25:00" on November 1st, we get to ask "which one?"

But if we use DateTimeOffset, we don't have to worry about that. The time is not representative of any time zone, it simply shows how many hours it differs from UTC.

This is why DateTimeOffset is often recommended. It represents a specific point in time unambiguously.

For more information, see the MSDN article: Choosing Between DateTime, DateTimeOffset, and TimeZoneInfo. Which includes the following:

Too bad real life is a bit trickier.

Problems with Scheduling
So, I did go through the sunset provider library and swap things out for DateTimeOffset. And I also set up the schedule items to use DateTimeOffset.

To see the updates, check out the following commit on GitHub: Switched to use DateTimeOffset.

This makes perfect sense for the sunset and sunrise information. The sunset for a particular date represents a particular point in time. But the other schedule items are a bit trickier.

Application Behavior
To see the problem, let's look at a schedule item. Here's the JSON representation from our file:

This shows the scheduled time as May 1, 2015 at 5:00 p.m. (local time). When the application starts, it rolls this forward until it is in the future. So, for example, if it is currently May 18th at 9:47 p.m. (which is the time I'm writing this), this schedule item will end up as May 19th at 5:00 p.m. -- the next "5:00 p.m."

At the same time, as the application runs, when we get to 5:00 p.m., this item will be executed (in this case a lamp will be turned off) and then the item is rolled to the next day.

And this all works fine until we get to Daylight Saving Time. But before we get there, let's enjoy this comic from XKCD:

The Problem with Daylight Saving Time
To see the problem, let's put together a little bit of code (just a console application). We'll pretend that it's the day before Daylight Saving Time ends this year.

Rather than picking the end of Daylight Saving Time ourselves, we'll ask the TimeZone information about that. This block of code gives us a UTC time that represents when DST ends this year (which happens to be November 1st at 2:00 a.m.)

Now we'll create two variables, one DateTime and one DateTimeOffset:

These represent the same time. And when we look at the output, we get no surprises:

It's 5:00 p.m. on October 31st.

But now we'll add 1 day to these (and cross over the DST boundary):

Here's the output:

Looks okay. But it's a bit deceptive.
These times are actually different!
Don't believe me? Let's change the output format from "s" (sortable format) to "o" (round-trip format):

Now we see what we really have:

The original times are fine, but the updated times are an hour apart! The first represents 5:00 p.m. local time, and the second represents 4:00 p.m. local time.

This is a problem for our application. The application regularly rolls the times forward by one day. That means we'll run into this problem every time Daylight Saving Time starts or ends.

What's the Answer?
We've got a problem. How do we solve it? Well, I haven't figured that out yet. The best solution is to eliminate Daylight Saving Time (or move to Arizona). But that's not practical at the moment.

A simple (yet kludgy) solution is to reload the schedule items from file whenever DST starts or ends. This would force the times into the appropriate offset (since there is no offset specified in the JSON, it is treated as "local" time). This would work, but it does smell a bit funny.

Maybe some items need to be DateTime while others are DateTimeOffset. I really don't like the idea of mixing these two. DateTimeOffset makes perfect sense when we're talking about the sunset and sunrise times -- these are discrete points in time. But DateTime makes sense for other times that are rescheduled each day at the same time.

I'll need to put a bit more thought into this. What's important is that we've identified the issue. I still have a few more months to fix it (until October 31st actually), so there's no rush. Feel free to leave any suggestions in the comments.

Happy Coding!

No comments:

Post a Comment