jkbrzt / rrule

JavaScript library for working with recurrence rules for calendar dates as defined in the iCalendar RFC and more.
https://jkbrzt.github.io/rrule
Other
3.28k stars 510 forks source link

Timezones do not seem to be respected correctly #452

Open christianbrueggemann opened 3 years ago

christianbrueggemann commented 3 years ago

I am trying to use the rrule library for a recurrence every day at midnight in my local timezone.

Version of rrule: 2.6.4 (since the new one does not work with timezones anymore, see: https://github.com/jakubroztocil/rrule/issues/427) Operating System: MacOS Catalina Local Timezone: Europe/Berlin (which is currently UTC+01:00, in summertime UTC+02:00)

Input:

import RRule from 'rrule' 
var r = RRule.fromString("FREQ=DAILY;DTSTART;TZID=Europe/Berlin:19760101T000000;INTERVAL=1;BYHOUR=0;BYMINUTE=0;BYSECOND=0")

var x = r.after(new Date("2021-02-01T00:00:00+01:00"), true)
console.log(x)

x = r.after(new Date("2021-07-01T00:00:00+02:00"), true)
console.log(x)

Expected Output:

2021-02-01T00:00:00.000+01:00
2021-07-01T00:00:00.000+02:00

or

2021-01-31T23:00:00.000Z
2021-06-30T22:00:00.000Z

both would be fine.

Actual Output:

2021-02-01T00:00:00.000Z
2021-07-01T00:00:00.000Z

I have tried this with the America/New_York timezone, which yields even worse results:

Input String: "FREQ=DAILY;DTSTART;TZID=America/New_York:19760101T000000;INTERVAL=1;BYHOUR=0;BYMINUTE=0;BYSECOND=0"

Actual Output:

2021-02-01T06:00:00.000Z
2021-07-01T06:00:00.000Z

This indicates that it actually does some timezone-stuff, but obviously the wrong thing, since new-york is not utc-6!!

Expected Output:

2021-02-01T00:00:00.000-05:00
2021-07-01T00:00:00.000-04:00

or

2021-02-01T05:00:00.000Z
2021-07-01T04:00:00.000Z

I have used the golang implementation of rrule (https://github.com/teambition/rrule-go), which is pretty much the same api, and it gives the expected result.

Has anyone experienced similar problems? Did I install stuff wrong? Or is this a bug?

yannickcare commented 3 years ago

Same here from EST

davidgoli commented 3 years ago

I agree this is confusing, but it is by design, and JavaScript native dates unfortunately do not yield a better alternative. Make sure to read the documentation: https://github.com/jakubroztocil/rrule#timezone-support

christianbrueggemann commented 3 years ago

this cannot be by design - how is New York ever UTC-6 ? I think you have quite a serious bug there. If there is no way to do this you should not offer the feature, instead of giving wrong results

lxcid commented 3 years ago

I believe DTStart is converted to UTC but DTStart is sort of a floating date time store in new Date() which is local.

when reading back, instead of considering it as UTC time, it consider it as local time thus the offset got mess up, if you machine is in UTC, you shouldn't have any issue.

lxcid commented 3 years ago

The problem lies in here

https://github.com/jakubroztocil/rrule/blob/286422ddff0700f1beb2e65cebff3421cc698aac/src/parsestring.ts#L25

https://github.com/jakubroztocil/rrule/blob/286422ddff0700f1beb2e65cebff3421cc698aac/src/dateutil.ts#L184-L193

It store the date as in UTC time, but when calculating recurrence, it consider the local time, thus your offset is likely a sum of the actual tzid + local timezone.

lxcid commented 3 years ago

Sorry for being spammy because I'm continuously trying to validate my hypothesis. I ran the test with my proposed fix and it seems to broke a lot of tests.

So I found this thread:

https://www.drupal.org/project/ics_field/issues/3051353

I believe that dtstart expect local time and not UTC time. meaning if I am in Asia/Singapore and my dtstart is in America/New_York and the time is in New York time. It expect the Date the same YYYY-MM-DD'T'HH:MM:ss value but with the timezone in local.

e.g.

for 2021-03-14 00:00:00 America/New_York the only date that help us translate correctly, is setting DTStart to 2021-03-14 00:00:00 Asia/Singapore which is my local machine time.

I also think that the unit test didn't caught these because it did not cover such cases.

DTSTART;TZID=America/New_York:20210314T000000

which based on spec, it should be handled:

This parameter MUST be specified on the "DTSTART", "DTEND", "DUE", "EXDATE", and "RDATE" properties when either a DATE-TIME or TIME value type is specified and when the value is neither a UTC or a "floating" time.

The "TZID" property parameter MUST NOT be applied to DATE properties and DATE-TIME or TIME properties whose time values are specified in UTC.

If you are try fixing this, I suggest, overriding it dtstart.

lxcid commented 3 years ago
const str = `DTSTART;TZID=America/New_York:20210224T080000\nRRULE:FREQ=DAILY`;
const rrule = rrulestr(str);
if (rrule.origOptions.tzid != null && rrule.origOptions.dtstart != null) {
  const dtstart = rrule.origOptions.dtstart;
  console.log({ dtstart });
  rrule.origOptions.dtstart = new Date(
    dtstart.getUTCFullYear(),
    dtstart.getUTCMonth(),
    dtstart.getUTCDate(),
    dtstart.getUTCHours(),
    dtstart.getUTCMinutes(),
    dtstart.getUTCSeconds(),
    dtstart.getUTCMilliseconds(),
  );
}

Its not pretty, but it works for us.

davidgoli commented 3 years ago

@lxcid is correct here.

This is a limitation of the problematic/nonexistent time zone support in JavaScript. Printing a "time-zoned" date will deceive you, because dates are always converted into your local time regardless of what time zone it's "supposed" to be in. And that's because:

There is no concept of "time zone" in JavaScript.

Consider that there are 3 zones at play:

  1. The target time zone of the RRule, as specified using the tzid option
  2. UTC
  3. Your machine's local time, which may not be the same as the tzid time zone

This library does two things to work around this:

  1. For dates that are NOT time-zoned, we consider them "floating" dates and render in UTC.
  2. For dates that ARE time-zoned, we render in the local time of the specified target zone, NOT in the user's local time zone, BUT the date you are given is zoned as though it were a UTC date. For example, to represent 7pm in America/New_York, rrule will give you a time 1900Z - in UTC - along with the zone information that it is in America/New_York. Converting this into the user's local time will require you to implement extra logic, perhaps in Luxon, from the given zoned time to the target time zone.

In both cases what you will get from this library will be the correct time, but represented as a UTC as it's the only reliable zone. Note that these are not actually UTC times (unless the zone is UTC) - they are local times in the zone - but this is the only way to represent a time in a zone that is not the local machine zone.

When you see the JS interpreter telling you the TZ offset of a date from RRule is -0600, that is most likely your local machine's time offset not the time offset of the target time zone.

This workaround is admittedly confusing and difficult to explain, but I have yet to see a proposal that improves it using existing JS Date primitives. I have high hopes, however, for the upcoming Temporal proposal!

braveheart-star commented 2 years ago

this issue is still opened?