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.34k stars 513 forks source link

Recurring Events using byweekday shift by one day #391

Closed spurreiter closed 3 years ago

spurreiter commented 4 years ago

I'm trying to get the next Tuesdays after a given date (1st Feb 2020 in Europe/Paris). Nonetheless the events start at Wednesday if still being in that timezone. Calculating the events for timezone America/New_York the events are as expected.

Please check the below snippet. My findings are:

// problem: events should be on Tuesday but are shifted by one day:
// Tue Feb 04 2020 00:00:00 GMT+0100 (Central European Standard Time) ...
$ TZ=Europe/Paris node zone.js Europe/Paris
rule:
DTSTART;TZID=Europe/Paris:20200131T230000
RRULE:FREQ=WEEKLY;BYDAY=TU;COUNT=2
events:
Wed Feb 05 2020 00:00:00 GMT+0100 (Central European Standard Time)
Wed Feb 12 2020 00:00:00 GMT+0100 (Central European Standard Time)
luxon:
Sat Feb 01 2020 00:00:00 GMT+0100 (Central European Standard Time)

// timezone America/New_York shows correct result
$ TZ=America/New_York node zone.js America/New_York
rule:
DTSTART;TZID=America/New_York:20200201T050000
RRULE:FREQ=WEEKLY;BYDAY=TU;COUNT=2
events:
Tue Feb 04 2020 00:00:00 GMT-0500 (Eastern Standard Time)
Tue Feb 11 2020 00:00:00 GMT-0500 (Eastern Standard Time)
luxon:
Sat Feb 01 2020 00:00:00 GMT-0500 (Eastern Standard Time)

// crossing timezones show correct result
$ TZ=America/New_York node zone.js Europe/Paris
rule:
DTSTART;TZID=Europe/Paris:20200201T050000
RRULE:FREQ=WEEKLY;BYDAY=TU;COUNT=2
events:
Mon Feb 03 2020 18:00:00 GMT-0500 (Eastern Standard Time)
Mon Feb 10 2020 18:00:00 GMT-0500 (Eastern Standard Time)
luxon:
Sat Feb 01 2020 00:00:00 GMT-0500 (Eastern Standard Time)

// UTC shows correct result
$ TZ=UTC node zone.js UTC
rule:
DTSTART:20200201T000000Z
RRULE:FREQ=WEEKLY;BYDAY=TU;COUNT=2
events:
Tue Feb 04 2020 00:00:00 GMT+0000 (Coordinated Universal Time)
Tue Feb 11 2020 00:00:00 GMT+0000 (Coordinated Universal Time)
luxon:
Sat Feb 01 2020 00:00:00 GMT+0000 (Coordinated Universal Time)
const { DateTime } = require('luxon')
const { RRule } = require('rrule/dist/es5/rrule-tz.js')

const dtstart = new Date(2020, 1, 1, 0)
const tzid = process.argv[2] || 'Europe/Paris'

const rule = new RRule({
  freq: RRule.WEEKLY,
  dtstart,
  tzid,
  byweekday: [RRule.TU],
  count: 2
})

console.log('rule:\n' + rule.toString())
console.log('events:\n' + rule.all().map(d => d.toString()).join('\n'))

const datetime = DateTime.fromJSDate(dtstart).setZone(tzid)
console.log('luxon:\n' + datetime.toJSDate().toString())
proficiat commented 4 years ago

Hello, the same issue here For recurrence: "FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR;UNTIL=20200430T040000Z"

rule.all() shows TU,WE,TH,FR,SA

cmditch commented 4 years ago

Update: I think this was purely my confusion around rrule.js's default behavior regarding UTC inputs as well as JS Date objects.

As gleaned from other comments, this confusion seems quite common: (1) Working with TZID (2) Inputting DTSTART in the correct format (local to the timezone, rather than UTC or local to the computer) (3) Inputting the right dates into rrule.between(). If declaring a TZID, much like you have to massage the dates when they come out - using DateTime.fromJSDate(date).toUTC().setZone('local', { keepLocalTime: true }), you have to massage the dates going in to between() (likely after/before as well) using DateTime.fromMillis(posixTime).setZone('utc', { keepLocalTime: true }).

Despite my troubles with the documentation, and frustrations around undocumented behavior, I'm very grateful this library exists. Thank you to the creator and all the maintainers and contributors. Much like luxon grew out of lessons learned from moment, I think the next iteration of an rrule library will build on the shoulders of rrule.js.

Original posted "issue", disregard what I've said below. Also experiencing what appears to be this issue as well.

import { rrulestr } from 'rrule';

const twoWeeks = 1209600000;
const myrrule = ["DTSTART:20200104T000000Z", "RRULE:FREQ=WEEKLY;BYDAY=FR"]
rrulestr(myrrule.join('\n'))
    .between(new Date(Date.now() - twoWeeks), new Date(Date.now() + twoWeeks))

This returns a list of dates that occur on Thursday. I've made sure that the DTSTART is UTC.

The event in question occurs on Friday at 18:00-0600 (mountain time), which is the same as Saturday 00:00-UTC, but rrule.between() oddly returns times @ Thursday 18:00-0600.

I'll gladly attempt to dissect the code to see where/how the issue is occurring. If anyone more familiar with the codebase has some tips to point me in the right direction, let me know, thanks!

jessicaelee commented 4 years ago

@davidgoli Hey David! I've been working with rrule, for the past 5 days and cannot figure out how to work with timezones. If I put the time in at 9pm my time, it gets converted to 4am UTC time. This causes an issue when I want to set recurring events on certain days, lets say Fridays. So then it repeats Friday 4am, and when it gets translated back to my timezone, then it is Thursday 9pm when I wanted it on Friday. Do you have any suggestions for this? I tried with tzid but I cannot get that to work either. Not sure how I'm supposed to incorporate Luxon? I have downloaded the package.

davidgoli commented 3 years ago

I think this issue arises from a misunderstanding of how this library is using UTC: not to represent dates in UTC, but rather as a sort of a hack to get around JavaScript's broken Date math. @cmditch's comment here is correct.

The times you get back from RRule are not in UTC (unless you explicitly say they should be, by using Z at the end of ISO strings or providing tzid: 'utc'), but rather in your local time but then shifted into UTC. If you want a "proper" Date in your local time, you'll have to do:

const date = rule.all()[0] // for whatever rule you have

// this is probably what you expect
const local new Date(
        date.getUTCFullYear(),
        date.getUTCMonth(),
        date.getUTCDate(),
        date.getUTCHours(),
        date.getUTCMinutes(),
        date.getUTCSeconds(),
        date.valueOf() % 1000
      )

I apologize for leaving this library in a state of neglect for the past couple of years. I moved on to other projects, and the code in this codebase is so tortured and convoluted that it's not really worth attempting to add new features to. I plan to rewrite this library from the ground up soon, stay tuned!

egarkavy commented 3 years ago

@davidgoli Thank you for the reply. Could you help me to understand. Is what @spurreiter proposed in PR change a valid fix for the library? Why then you propose doing it in other way? First generate an occurrences and only do that UTC hack afterwards?