2

I have a bunch of rrules (implemented in rrule.js) that gives me an array of event start times (see the demo). rrule.js doesn't actually provide the concept of an event duration or endtime... So it can tell me the precise date when the millionth occurrence of a repeating event will start but not when it will end. Turns out I actually want to know when an event ends so I'll have to get creative. As far as I see it I've got two options

  1. DB SIDE: Store an rrule string + an event duration.

    CLIENT SIDE: Reconstitute events start date array from rrule string. Only start times would be known and end times would be calculated by adding the duration as an offset to each start time in the array.

  2. DB SIDE: Store a modified rrule string which encodes an endtime.

    CLIENT SIDE: A special wrapper function reads the modified rrule string and reconstitutes it as two date arrays; one representing event start times and the other end times.

Option 1 seems easier but I suspect it will run into problems with daylight savings. For example, say I've an event that is every Tuesday from 6pm to 2 am Wednesday. In that case I'd store a duration of 8 hours in my database alongside that stringified rrule. Now let's fast forward to any 6pm Tuesday in the future. Does my event always end on Wednesday at 2am (or does that 8 hour duration sometimes make my event end at 1am or 3am)? How do I get it to always end at 2am?

... If you know the answer then just stop reading here.

How I've seen others handle duration offset

According to Kip in How to add 30 minutes to a JavaScript Date object? the smart way to offset a date time is to use a fancy library like moment.js.

He emphasizes that point by showing how easily things go wrong using non fancy date time libraries (showing how a naive minute offset function fails due to daylight savings)

function addMinutes(date, minutes) {
    return new Date(date.getTime() + minutes*60000);
}
addMinutes(new Date('2014-11-02'), 60*24) //In USA, prints 11pm on Nov 2, not 12am Nov 3!

But something weird happens for me. The function above was supposed to output 11pm on Nov 2 - which is the wrong answer i.e. it was supposed to fail because of daylight savings. When I run it, it actually outputs the right time 12am on Nov 3 (note: I'm in Chicago/Central time).

When I compare the output of his naive function to the output of moment.js and luxon.js, I get the same answer as you can see in this observable notebook.

Scratching my head

What's more, if using luxon or moment, when you add a days worth of minutes to 2014-11-02 you get2014-11-03T00:00:00.000Z but if you just directly add a day to 2014-11-02 you get 2014-11-03T01:00:00.000Z - it's an hour off.

So am I better off pursuing option 2?

zelusp
  • 3,500
  • 3
  • 31
  • 65
  • 1
    Your naive minute offset example is a poor one. '2014-11-02' should be parsed as UTC, so even on the east coast of USA (UTC-4) it will be 2014-11-01T20:00:00-0400 (and earlier in more western timezones). Adding one day of minutes will make it 2014-11-02T20:00:00-0400. Best to just do everything in UTC and leave conversion to local times for presentation only. That way, days are always 24 hours long. – RobG Jun 16 '19 at 01:15
  • Yes :) I appreciate that pointer - that's also what rrule recommends (and what I follow [here](https://observablehq.com/@aagostini/rrule-js)). So assuming that UTC is the reference point, how does one add a fixed amount of time to a recurrence rule's start time so that it's future event end times are always the same? – zelusp Jun 16 '19 at 02:13

2 Answers2

2

Now let's fast forward to any 6pm Tuesday in the future. Does my event always end on Wednesday at 2am (or does that 8 hour duration sometimes make my event end at 1am or 3am)? How do I get it to always end at 2am?

The standard Javascript Date object automatically handles the daylight savings shift for you. Even if you add 8 hours to a date at 6pm the day before daylight savings, the new date will still end at 2am the next day.

Incidently, I implemented duration support in rSchedule and since it supports both the standard javascript Date as well as moment/luxon dates, you can test a recurring event with a duration using either library and see that they both produce the same result.

This example can be seen on stackblitz.

import { Schedule } from '@rschedule/rschedule';
import { StandardDateAdapter } from '@rschedule/standard-date-adapter';

// This example will also work with `moment`, `moment-timezone`, and `luxon`
// (assuming you import the proper date adapter -- see rSchedule docs)

const schedule = new Schedule({
  rrules: [
    {
      start: new Date(2019,9,10,18),
      frequency: "DAILY",
      duration: 1000 * 60 * 60 * 8,
      count: 30
    }
  ],
  dateAdapter: StandardDateAdapter,
});

schedule.occurrences().toArray().forEach(adapter => {
  console.log(
      {
        start: adapter.date.toLocaleString(),
        end: adapter.end.toLocaleString(),
      }
    )
})

Turns out I actually want to know when an event ends

To find out when this event ends, you could do:

const iterator = schedule.occurrences({ reverse: true })

const { end } = iterator.next().value

This trick would only work with an event that actually has an end date (so not an event with infinite occurrences).

John
  • 9,249
  • 5
  • 44
  • 76
  • So are you saying [this](https://observablehq.com/@aagostini/rrule-js) would always give the right event end times?.. sure looks like it. – zelusp Jun 16 '19 at 16:05
  • 1
    @zelusp I'm not going to comment on the validity of all the code, but as far as `.plus({hours: durationHours})` ya. That should perform as you expect/want it to. – John Jun 16 '19 at 21:39
  • That's swell John. Out of curiosity, what was the motivation for writing rSchedule? Aside from being date type agnostic and having duration support, why write a whole new thing instead of extending rrule.js? – zelusp Jun 17 '19 at 15:26
  • 1
    @zelusp a bigger answer than I can fit in 550 characters. I looked into extending rrule and concluded that it's internals were too messy. Some highlights: being date library agnostic is not something that can be added to rrule. rSchedule is immutable which eliminates many usage bugs. rSchedule supports manipulating schedules with occurrence stream operators (e.g. add two schedules together). Because of the occurrence stream operators, rSchedule supports whole calendars, rrule does not. Other stuff. You can [check out the docs](https://gitlab.com/john.carroll.p/rschedule#docs). – John Jun 17 '19 at 17:22
  • 1
    Am awarding the "right" answer to you for pointing me towards rSchedule. Thanks mate – zelusp Jul 16 '19 at 01:20
  • @John, I need your help on another [rscheduler problem](https://stackoverflow.com/questions/58773446/how-to-produce-recurrence-rschedule-durations-with-timeshifts). – Mehmet Aydoğdu Nov 08 '19 at 20:40
1

I wrote the original answer you are referring to about a decade ago. Seven years later, I made an edit, changing new Date(2014, 10, 2) to new Date('2014-11-02'). I thought this would be easier to read (because you don't have to explain that the months in that version of the constructor start at 0 instead of 1). But as @RobG pointed out, formatting in this way causes it to be parsed as UTC. I've gone back and fixed this now (thanks for pointing it out).

To get to your "scratching my head" part of your question:

What's more, if using luxon or moment, when you add a days worth of minutes to 2014-11-02 you get 2014-11-03T00:00:00.000Z

The Z at the end of that timestamp means it is in UTC, and UTC does not observe daylight savings time. So if you start with 2014-11-02T00:00:00.000Z, and add 24 hours, you get 2014-11-03T00:00:00.000Z. When you add hours/minutes/seconds, there's no need to worry about daylight saving time.

but if you just directly add a day to 2014-11-02 you get 2014-11-03T01:00:00.000Z - it's an hour off.

In this case what is happening is you are starting with 2014-11-02T00:00:00.000Z, but when you tell the library to add one day, and you don't specify a time zone, the library is assuming you are in your local time zone, so it adds one local day. Because you cross a DST boundary, that day is 25 hours long, and when you print it as an ISO timestamp in UTC, you end up with 2014-11-03T01:00:00.000Z (25 hours later).

Time zone stuff is hard, even if you are using a library. Most people can get by for a long time not knowing or caring that for many users one day a year is 25 hours long. But if these edge cases will matter to you, the best approach is to play around with them like you're doing, and make sure you really understand what is happening and why.

Kip
  • 107,154
  • 87
  • 232
  • 265