Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Recurring hourly times DST mismatch #1

Open
holmberd opened this issue Feb 24, 2025 · 3 comments
Open

Recurring hourly times DST mismatch #1

holmberd opened this issue Feb 24, 2025 · 3 comments

Comments

@holmberd
Copy link

This is very much an edge-case. Rather than explaining I'll illustrate with two examples below.

Daily Frequence:

loc, _ := time.LoadLocation("America/Vancouver")
r := RRule{
	Frequency: Daily,
	ByHours:   []int{2},
	Count:     3,
	Dtstart:   time.Date(2025, 3, 8, 0, 0, 0, 0, loc), // Before DST change: 2025-03-08 00:00:00 -0800 PST
}
for _, t := range All(r.Iterator(), 0) {
	fmt.Println(t)
}

// Prints:
// 2025-03-08 02:00:00 -0800 PST
// 2025-03-09 03:00:00 -0700 PDT
// 2025-03-10 02:00:00 -0700 PDT

That looks good. DST is respected during 2025-03-09 02:00:00.

Hourly Frequence:

loc, _ := time.LoadLocation("America/Vancouver")
r := RRule{
	Frequency: Hourly,
	ByHours:   []int{2},
	Count:     3,
	Dtstart:   time.Date(2025, 3, 8, 0, 0, 0, 0, loc), // Before DST change: 2025-03-08 00:00:00 -0800 PST
}
for _, t := range All(r.Iterator(), 0) {
	fmt.Println(t)
}

// Prints:
// 2025-03-08 02:00:00 -0800 PST
// 2025-03-10 02:00:00 -0700 PDT
// 2025-03-11 02:00:00 -0700 PDT

Problem, since 2025-03-09 02:00:00 -0700 PDT technically doesn't exist. Instead of returning 2025-03-09 03:00:00 -0700 PDT, it skips the day and returns the next date 2025-03-10 02:00:00 -0700 PDT instead.

However, expected behaviour is for it to return the same as daily, i.e. 2025-03-09 03:00:00 -0700 PDT.

@stephens2424
Copy link
Owner

I'm tempted to agree with you, however, the spec this library is trying to follow is actually pretty clear about the case:

Recurrence rules may generate recurrence instances with an invalid
date (e.g., February 30) or nonexistent local time (e.g., 1:30 AM
on a day where the local time is moved forward by an hour at 1:00
AM). Such recurrence instances MUST be ignored and MUST NOT be
counted as part of the recurrence set.

I suspect the expectation is that an extra RDATE could be included to cover such missed instances, allowing implementers to decide what a fallback mechanism would do.

I imagine this library or another could grow a helper function to "validate and correct" generated recurrences, to apply some fallback mechanism for these cases. I don't personally use this library much, but if you have motivation for that, by all means. I'm going to preemptively close this issue, but feel free to continue on here if you'd like.

@stephens2424
Copy link
Owner

Oh I see, it's inconsistent. It should probably not emit that case on the 9th in the first example, going by the spec. Reopened the issue. But again, I still don't have the time to dig into this. I may someday, so I'll leave this here. But if you're motivated to do so, feel free.

@holmberd
Copy link
Author

holmberd commented Feb 25, 2025

Thank you for the response.

I agree that the spec is somewhat contradictory regarding these rules. My interpretation is as follows:

Rule 1: Invalid recurrence instances must be ignored

Recurrence rules may generate recurrence instances with an invalid
date (e.g., February 30) or nonexistent local time (e.g., 1:30 AM
on a day where the local time is moved forward by an hour at 1:00
AM). Such recurrence instances MUST be ignored and MUST NOT be
counted as part of the recurrence set.

Rule 2: Nonexistent/Ambiguous local times are interpreted as if they were explicitly specified DATE-TIME values

If the computed local start time of a recurrence instance does not
exist, or occurs more than once, for the specified time zone, the
time of the recurrence instance is interpreted in the same manner
as an explicit DATE-TIME value describing that date and time, as
specified in Section 3.3.5.

Rule 3: DATE-TIME with timezone reference interpretation

If, based on the definition of the referenced time zone, the local
time described occurs more than once (when changing from daylight
to standard time), the DATE-TIME value refers to the first
occurrence of the referenced time. Thus, TZID=America/
New_York:20071104T013000 indicates November 4, 2007 at 1:30 A.M.
EDT (UTC-04:00). If the local time described does not occur (when
changing from standard to daylight time), the DATE-TIME value is
interpreted using the UTC offset before the gap in local times.
Thus, TZID=America/New_York:20070311T023000 indicates March 11,
2007 at 3:30 A.M. EDT (UTC-04:00), one hour after 1:30 A.M. EST
(UTC-05:00).

Since nonexistent local times with a timezone reference are interpreted as DATE-TIME values, we can infer:

  1. When a time occurs twice (DST fall back), we use the first occurrence (Rule 3).
  2. When a time does not exist (DST spring forward), it should be interpreted using the last valid UTC offset before the shift (Rule 3).

Applying this logic:

  • February 30 is ignored completely because it doesn't exist and is not a timezone issue.
  • In rule FREQ=DAILY;BYHOUR=2, 2025-03-09 02:00:00 -0800 PST is invalid, because 2 AM does not exist on that day in America/Vancouver.
    • Since it does not exist, it is interpreted as an explicit DATE-TIME value using the last valid UTC offset before the gap.
      In this case, 2025-03-09 02:00:00 -0800 (UTC-8) is adjusted to 2025-03-09 03:00:00 -0700 PDT (UTC-7).
    • As a result, 2025-03-09 02:00:00 -0800 PST is ignored in the set, but the adjusted time is not. This behavior aligns with what you would expect from a calendar (e.g. Google Calendar) when scheduling a daily event at 2 AM.

Edit: However, whether the rule FREQ=HOURLY;BYHOUR=2 should behave the same as FREQ=DAILY;BYHOUR2 or not, is not very clear. I don't see any specific rules in the spec that say they should behave differently, and based on that I have to assume they should behave the same.

I still don't have the time to dig into this. I may someday,

I completely understand that there’s only so much time in a day, and despite this edge cases (which might not be an edge-case at all), this package is still more consistent than the others. If I need to make adjustments later, I’ll push an update.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants