How to store repeating dates keeping in mind Daylight Savings Time
Asked Answered
C

1

48

I'm storing events in my database. I have 'start' and 'end' date times, 'tickets_start' and 'tickets_end' (for when ticket sales actually start/end - as opposed to the start/end of the actual event).

So far, I've built methods that do all the fun stuff like converting the date/times to GMT before saving, then back to their respective timezone for display.

I'm storing the timezone in a varchar field with values like "America/New_York".

But - now I need to start dealing with if the user wants to allow repeating events. I've done it before, and it's not that big a deal, but never across multiple timezones.

At first, I thought it'd be no big deal, but then realized that - if the initial start date was in July (as example), and it repeats every month for a year, at some point, Daylight Savings Time will make it so that the conversion from GMT will change a time differently. One month, when converting 12:00, it would change it to -5, and the next, it would change it to -4 because of DST.

My current thought is that I'll store a 'dst' tinyint(1) for whether the start/end dates were entered during DST, and then make a method to alter the time by an hour if/when necessary.

But - figured I'd ask here in hopes maybe there's a "normal" for this or an easy something that I'm not thinking of.

(cakephp 2.4.x)

Cushing answered 28/10, 2013 at 2:7 Comment(1)
possible duplicate of Daylight saving time and time zone best practicesSakovich
E
145

First, please recognize that in modern terminology you should say UTC instead of GMT. They are mostly equivalent, except that UTC is more precisely defined. Reserve the term GMT to refer to the portion of the time zone in the United Kingdom that is in effect during the winter months having the offset UTC+0.

Now to your question. UTC is not necessarily the best way to store all date and time values. It works particularly well for past events, or for future absolute events, but it doesn't work so great for future local events - especially future recurring events.

I wrote about this on another answer recently, and is one of the few exception cases where local time makes more sense than UTC. The main argument is the "alarm clock problem". If you set your alarm clock by UTC, you'll be waking up an hour early or late on the day of the DST transitions. This is why most folks set their alarm clocks by local time.

Of course, you can't just store a local time if you are working with data from all over the world. You should store a few different things:

  • The local time of the recurring event, such as "08:00"
  • The time zone that the local time is expressed in, such as "America/New_York"
  • The recurrence pattern, in whatever format makes sense for your application, such as daily, bi-weekly, or the third Thursday of the month, etc.
  • The next immediate UTC date and time equivalent, to the best that you can project it.
  • Perhaps, but not always, a list of future event UTC date and times, projected out some predefined period into the future (perhaps a week, perhaps 6 months, perhaps a year or two, depending on your needs).

For the last two, understand that the UTC equivalent of any local date/time can change if the government responsible for that time zone decides to change anything. Since there are multiple time zone database updates every year, you will want to have a plan to subscribe to announcements of updates and update your timezone database regularly. Whenever you update your timezone data, you'll need to recalculate the UTC equivalent times of all future events.

Having the UTC equivalents is important if you plan to show any kind of list of events spanning more than one time zone. Those are the values you'll query to build that list.

Another point to consider is that if an event is scheduled for a local time that occurs during a DST fall-back transition, you will have to decide whether the event occurs on the first instance (usually), or the second instance (sometimes), or both (rarely), and build into your application a mechanism to ensure the event doesn't fire twice unless you want it to.

If you were looking for a simple answer - sorry, but there isn't one. Scheduling future events across time zones is a complicated task.

Alternative Approach

I've had a few people show me a technique where they do use UTC time for scheduling, which is that they pick a starting date in local time, convert that to UTC to be stored, and store the time zone ID as well. Then at runtime, they apply the time zone to convert the original UTC time back to local time, then use that local time to compute the other recurrences, as if it were the one originally stored as above.

While this technique will work, the drawbacks are:

  • If there's a time zone update that changes the local time before the first instance is run, it will throw the whole schedule off. This can be mitigated by choosing a time in the past for the "first" instance, such that the second instance is really the first one.

  • If the time is really a "floating time" that should follow the user around (such as in an alarm clock on a cell phone), you'd still have to store time zone information for the zone it was originally created in - even if that's not the zone you want to run in.

  • It adds additional complexity without any benefit. I'd reserve this technique for situations where you may have had a UTC-only scheduler that you're trying to retrofit time zone support into.

Ephialtes answered 28/10, 2013 at 4:47 Comment(10)
Is there a good way to deal with checking to see if the date passed, or other simple 'checks' like that? Previously, if stored all dates in UTC, it was simple because I could check against server time. But now, it seems to require additional steps... any advice?Cushing
If you store the next immediate projected UTC time, then you can use this to query if an event is upcoming or has passed. If passed, then you might want to write a new record for the past event in a different table and then update your UTC time to the next recurring instance.Ephialtes
You can have the recurring event scheduled with local time, and past instances of each occurrence in UTC. But try to think of these as separate entities in your domain.Ephialtes
Of the five items, the last two can always be calculated from the first three, so if you decide to store them, I would consider them as cached values.Antrorse
@Antrorse - Exactly. And the cache always expires if you update the tz rules. :)Ephialtes
Great Answer @MattJohnson, Would have given you +1000, if allowed. Thanks for clear explanation.Blister
@MattJohnson Everything is clear, but what do you mean when you say "The next immediate UTC date and time equivalent,...." ?Hardworking
@Hardworking - as in, if the task is running at 12:00:00 PM Pacific daily, and today is 2017-11-07, then the next run would be 2017-11-07T20:00:00Z.Ephialtes
This is a very informative and well-structured post. Thank you @MattJohnson-PintRifleman
I think, sometimes it depends on the language/libraries you use. Some are quite smart, for example Luxon. I live in Switzerland, our DST will end on 2024-03-31 02:00:00. Running DateTime.fromISO('2024-03-30T19:00').plus({ hours: 24 }) gives 2024-03-31T20:00:00 whereas DateTime.fromISO('2024-03-30T19:00').plus({ days: 1 }) returns 2024-03-31T19:00:00. In Oracle Calendaring Syntax you define schedules for example like 'FREQ=DAILY;BYHOUR=08'Errancy

© 2022 - 2024 — McMap. All rights reserved.