7

I have what seems like a very common use case: I want to have a recurring event that occurs at the same time each day in a specific time zone (in the example below, 6:00 AM in the America/Denver time zone). I want this to recur at the same time of day after a change in Daylight Savings as before. Right now, it is changing by one hour after Daylight Savings, which seems to indicate that Daylight Savings is not being accounted for when the recurring datetimes are generated.

I have tried various configurations for the rrule as indicated in the documentation here and here. It says the time of day should be the same across Daylight Savings, but that is not what I am seeing.

Code sample

const rrule = new RRule({
  freq: RRule.DAILY,
  dtstart: new Date(Date.UTC(2022, 7, 18, 12, 0, 0)),
  // tzid: 'America/Denver', // output is the same whether this is included or not
})
const datetimes = rrule.between(
  new Date('2022-10-31'),
  new Date('2022-11-10')
)

Try out the CodeSandbox. Should get similar results as long as you are in a time zone that has Daylight Savings and the between range includes a change in Daylight Savings.

Expected output

The time of day in America/Denver time zone should not change after Daylight Savings (i.e. recurrence should account for Daylight Savings):

Mon Oct 31 2022 06:00:00 GMT-0600 (Mountain Daylight Time)
Tue Nov 01 2022 06:00:00 GMT-0600 (Mountain Daylight Time)
Wed Nov 02 2022 06:00:00 GMT-0600 (Mountain Daylight Time)
Thu Nov 03 2022 06:00:00 GMT-0600 (Mountain Daylight Time)
Fri Nov 04 2022 06:00:00 GMT-0600 (Mountain Daylight Time)
Sat Nov 05 2022 06:00:00 GMT-0600 (Mountain Daylight Time)
Sun Nov 06 2022 06:00:00 GMT-0700 (Mountain Standard Time) <-- Daylight savings change
Mon Nov 07 2022 06:00:00 GMT-0700 (Mountain Standard Time)
Tue Nov 08 2022 06:00:00 GMT-0700 (Mountain Standard Time)
Wed Nov 09 2022 06:00:00 GMT-0700 (Mountain Standard Time)
                ^^

Actual output

The time of day in America/Denver time zone is changing from 6:00 to 5:00:

Mon Oct 31 2022 06:00:00 GMT-0600 (Mountain Daylight Time)
Tue Nov 01 2022 06:00:00 GMT-0600 (Mountain Daylight Time)
Wed Nov 02 2022 06:00:00 GMT-0600 (Mountain Daylight Time)
Thu Nov 03 2022 06:00:00 GMT-0600 (Mountain Daylight Time)
Fri Nov 04 2022 06:00:00 GMT-0600 (Mountain Daylight Time)
Sat Nov 05 2022 06:00:00 GMT-0600 (Mountain Daylight Time)
Sun Nov 06 2022 05:00:00 GMT-0700 (Mountain Standard Time) <-- Daylight savings change
Mon Nov 07 2022 05:00:00 GMT-0700 (Mountain Standard Time)
Tue Nov 08 2022 05:00:00 GMT-0700 (Mountain Standard Time)
Wed Nov 09 2022 05:00:00 GMT-0700 (Mountain Standard Time)
                ^^

I've opened an issue for this on GitHub, but I'm wondering if I'm just missing something. It seems like a common use case, so I would think I'd be able to find something out there about it. I did find a couple of SO questions about it here and here, but I'm already applying the solutions suggested.

Is this an actual bug in rrule or am I just missing something?

fenix.shadow
  • 402
  • 4
  • 9

1 Answers1

2

I had to dig very deep and surprisingly it seems like there is no bug. If you change your CodeSandbox to the following then it should work:

import { RRule } from "rrule";
import "./styles.css";

const rrule = new RRule({
  freq: RRule.DAILY,
  dtstart: new Date(Date.UTC(2022, 7 - 1, 18, 12, 0)), // See note 1
  tzid: 'America/Denver',
})
const datetimes = rrule.between(
  new Date('2022-10-31'),
  new Date('2022-11-10')
)
const output = datetimes.map((d) => new Date( // See note 2
  d.getUTCFullYear(),
  d.getUTCMonth(),
  d.getUTCDate(),
  d.getUTCHours(),
  d.getUTCMinutes(),
)).join('<br/>')
document.getElementById("app").innerHTML = output;

Notes:

  1. The docs at https://github.com/jakubroztocil/rrule#timezone-support state that using new Date(2022, 7, ...) will produce unexpected timezone offsets. Instead use rrule's datetime function. Since this function currently is not working (see https://github.com/jakubroztocil/rrule/issues/551) we have to use new Date(Date.UTC(2022, 7 - 1, ...)) (which is excatly what the datetime function is doing).
  2. The result will be a date in UTC which you have to interpret as a date in the timezone you specified in tzid. It seems very weird but it is stated in the docs at https://github.com/jakubroztocil/rrule#important-use-utc-dates that: Even though the given offset is Z (UTC), these are local times, not UTC times. Just to emphasize this my code example transforms the UTC dates to America/Denver dates by creating a new instance of Date using the UTC values (only works if your operating's system timezone is America/Denver).
Michael Käfer
  • 1,597
  • 2
  • 19
  • 37
  • 2
    Finally, I understand how it's supposed to work! There are bits and pieces of it in the documentation, but they never really put it all together. It's a little weird, but at least I can get it to work now. Thank you! – fenix.shadow Nov 30 '22 at 19:51
  • Why do we need to use `m -1` instead of `m`? Thanks – Usama Tahir Dec 12 '22 at 03:58
  • 1
    @UsamaTahir Because in the native JS `Date` class the first month January is `0` (and not `1`). – Michael Käfer Dec 12 '22 at 08:46
  • Thanks. @MichaelKäfer. I am facing some issues with the `dtstart` value. When I use `JS Date Object (Mon Dec 12 2022 00:00:00 GMT+0500 (Pakistan Standard Time))`, I get wrong results. ``` const rule = new RRule({ freq: RRule.MONTHLY, dtstart: new Date("2023-1-1"), until: null, count: 2, byMonthDay: 19, }); const dates = rule.all() // OUTPUT Fri Jan 20 2023 00:00:00 GMT+0500 (Pakistan Standard Time) Mon Feb 20 2023 00:00:00 GMT+0500 (Pakistan Standard Time) ``` You see rule.all returns 20th of each date. Ideally it should return 19th of each month. – Usama Tahir Dec 12 '22 at 15:47
  • When I set dtstart = null, things work alright. Another thing I did was to set `dtstart = new Date(Date.UTC(startDate.getFullYear(),startDate.getMonth(),startDate.getDate(),0,0,0)` where startDate = new Date("2023-1-1"). This made things work for me but my colleague started facing this issue (He is in san francisco ). I was getting results one day in future, he started getting results one day in past. I think it has to do with timezone but I can't figure out the value that I need to pass to `dtstart` to make it work in all timezones. Any ideas what I am doing wrong here? – Usama Tahir Dec 12 '22 at 15:47
  • @UsamaTahir Sorry, I'm not sure, I think this gets off topic, maybe you can create a CodeSandbox and ask a new Stackoverflow question. – Michael Käfer Dec 12 '22 at 16:20