Skip to content

fix: between() optimization may cause earlier event occurrences to be skipped#103

Merged
ggaabe merged 4 commits intoggaabe:mainfrom
nathanhleung:nathanhleung/fix-optimization-edge-case-with-earlier-occurrences
Jan 28, 2026
Merged

fix: between() optimization may cause earlier event occurrences to be skipped#103
ggaabe merged 4 commits intoggaabe:mainfrom
nathanhleung:nathanhleung/fix-optimization-edge-case-with-earlier-occurrences

Conversation

@nathanhleung
Copy link
Contributor

Background

Currently, to reduce the amount of iteration done in between() (the method which gets recurrences of an event after a specified date/time and before another), the original RRuleTemporal instance is re-created with an optimized dtstart that is closer to the provided after date, and iteration is done with this re-created RRuleTemporal instance starting from the new dtstart instead.

Problem

The calculation to determine the new, optimized dtstart may jump too far ahead, resulting in some event recurrences potentially being skipped.

Specifically, to calculate the optimized dtstart, the library takes the original dtstart date and jumps forward by the event frequency interval (e.g., every 1 day) until the new dtstart lies within the same frequency interval (e.g., the same day) as after.

The problem lies in the fact that dtstart might actually occur relatively late within the frequency interval (specifically, I ran into this issue with daily recurring events — e.g., dtstart on a daily event could have a very late time attached to it), but the actual event might recur sometime earlier within the frequency interval (e.g., in the morning). If the passed after parameter is also on the earlier side of the frequency window, using the later, optimized dtstart for calculations will result in missed recurrences — specifically, those that are scheduled to occur between the time of the after parameter and the time of the optimized dtstart.

Example / Bug Walkthrough

Let's say dtstart is 7:00pm on January 25th, 2026 (rather late in the day), and our event recurs DAILY, every day (interval is 1) at 10am, 2pm, and 8pm (byHour: [10, 14, 20]). Now we call the between method with the after parameter set to 9:00am on January 27th, 2026, about two days after dtstart but relatively earlier in the day.

First (see the original between() code for variable references), the between method converts after to aligned, which is after with the same date as it had originally, but with the time updated to match that of dtstart. So now aligned is 7:00pm (the time of dtstart) on January 27th (date is unchanged). Then the method calculates the difference between dtstart and aligned, which is now 2 full days (unitsBetween = 2).

So then Math.floor(unitsBetween / interval) is 2, which means to calculate the optimized dtstart, the method jumps forward two whole days from the original dtstart to 7:00pm on January 27th. But after was set to 9:00am on that day! This means that the iteration misses the 10am and 2pm event occurrences, even though they are actually after the after parameter, too.

Fix

We need to subtract 1 more from the number of steps we jump forward from the original dtstart, because the relative time of the original dtstart within the frequency interval may not actually be before earlier occurrences of the event in the interval, even though the after parameter could be before those occurrences. By jumping forward one less step (e.g. to the day before the after date for a daily event), we run through the entire frequency interval in which the after parameter is contained, ensuring we catch the earlier occurrences.

@ggaabe ggaabe merged commit e68055e into ggaabe:main Jan 28, 2026
1 check passed
@nathanhleung nathanhleung deleted the nathanhleung/fix-optimization-edge-case-with-earlier-occurrences branch January 28, 2026 04:35
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

Successfully merging this pull request may close these issues.

2 participants