Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

preventing DST shift with moment.js for recurring events

The system I'm building has an events component and part of that is the ability to create recurring events. In my database, I'm storing all events in UTC. When a recurring event is displayed on the user's calendar, it should always be displayed in "wall time." So for example, if I create a recurring event every Wednesday at 1:00PM, regardless of the daylight savings time shift, it should always be at 1:00PM.

The issue that I'm running into is that whenever I attempt to format this date using Moment.js, Moment is always accounting for the DST shift and updating the event accordingly. Taking my previous example, in 2016, the DST shift occurs on March 13th, so if my appointment was booked in February of 2016 then every appointment until March 13th is correctly output as 1:00PM. After March 13th, the DST shift is applied and all my events are now an hour ahead.

Is there a recommended way to handle this? I can't seem to find a way to have Moment "ignore" DST nor have I found a proper solution when searching.

Not sure how important this is, but I am using Moment Timezone to convert to the user's local timezone when outputting the date.

Thanks!

like image 893
jdixon04 Avatar asked May 17 '16 18:05

jdixon04


2 Answers

As a matter of best practice, future dates should be stored in local time. This is because, as pointed out in the comments, time zone rules can and do change. As such, you cannot correctly compute the right UTC date for a local time ahead of time.

When you store the future date, you should store it with the IANA time zone that the user is expecting the event time to be in. Then you would actually do the reverse of the conversion you are doing now. You would convert from local time to UTC if you needed to know the event's exact point on the global timeline.

Be aware that the IANA timezone database updates frequently. It is on version 2016d right now, which means that it has already updated four times this year. If you have an application that is going to be used in multiple countries, you will need to be very diligent about continually updating moment timezone to keep up with the changes.

As far as why you are seeing your UTC dates not change correctly right now - I think you are taking your scheduled date, converting to UTC, and then calculating the time of your recurrence. When you do your date math in UTC, it will not correctly account for the DST shifts because it has no knowledge of them. As such, when moment converts that date back to local, it will be off by an hour. If you were to do your date math in local time, and then convert to UTC to store, your conversion back would be correct PROVIDED that the timezone rules have not changed. Don't do this though - store the dates in local time.

like image 56
Maggie Pint Avatar answered Sep 29 '22 06:09

Maggie Pint


Alternative (partly) solution

There is an alternative solution which addresses the DST-shift issue. Note: When the time zone offset of some timezone changes in the future, this does not work as expected! It does not use moment.js but I think it might still help someone.

Explanation

The trick is to adjust the UTC dates when creating the entity by using the current UTC-to-local-time offset, and then storing the updated UTC date.

Let's say the local time zone is 'Europe/Berlin', and we are creating a recurrent event starting on '2020-03-28 10:00:00Z' (UTC). This is our reference date. The time zone has an offset of +01:00 at this time, so the user's intention was to let the event start at 11:00.

Now this event is repeated until '2020-03-29' (which is the day after the DST shift in this time zone, on which the time zone has an offset of +02:00). Hence, if we didn't change anything, the event would be displayed at 12:00 on that day (and not 11:00, as the user intended).

Thus, if we store the date as '2020-03-29 09:00:00Z' (UTC) on that day instead, it is correctly shown at 11:00 in the local time zone.

Code

This is a bit hacky and the part where we apply the timezone offset may certainbly be improved, but for our use case it was sufficient. It uses date-fns and date-fns-timezone in a Node.js environment.

  • referenceDate: the UTC date which to use as the reference for the offset to be applied (e.g., the first date of the series; '2020-03-28 10:00:00Z' in the example above
  • date: the UTC date of the current repetition; '2020-03-29 10:00:00Z' in the example above
  • timeZone: the IANA time zone name of the local time zone; 'Europe/Berlin' in the example above
  • adjustedUTCDate: the adjusted UTC date

Conversion code:

// offset == '+01:00' in the example
let offset = dateFnsTimezone.formatToTimeZone(referenceDate, 'Z', { timeZone });
if (offset[0] === '+') {
    offset = offset.replace('+', '-');
} else {
    offset = offset.replace('-', '+');
}

const dateString = dateFns.format(date, 'YYYY-MM-DDTHH:mm:ss[Z]');

// applies the offset to change the time (intended local time); '2020-03-29 10:00:00+01:00' in the example
const localDate = dateString.replace('Z', offset);

// convert the date back to UTC, from the intended local time; '2020-03-29 09:00:00' in the example
const adjustedUTCDate = dateFnsTimezone.parseFromTimeZone(localDate, { timeZone });
like image 24
Florian Pfisterer Avatar answered Sep 29 '22 05:09

Florian Pfisterer