Skip to content

Commit

Permalink
Merge pull request #660 from phil-davis/dst-leap-648-4.5
Browse files Browse the repository at this point in the history
[4.5] Handle summer time jumps in event recurrences
  • Loading branch information
phil-davis authored Jul 2, 2024
2 parents 2783cc8 + b7f45de commit 2aeb4fc
Show file tree
Hide file tree
Showing 2 changed files with 568 additions and 9 deletions.
83 changes: 74 additions & 9 deletions lib/Recur/RRuleIterator.php
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,14 @@ public function fastForward(DateTimeInterface $dt)
*/
protected $currentDate;

/**
* The number of hours that the next occurrence of an event
* jumped forward, usually because summer time started and
* the requested time-of-day like 0230 did not exist on that
* day. And so the event was scheduled 1 hour later at 0330.
*/
protected $hourJump = 0;

/**
* Frequency is one of: secondly, minutely, hourly, daily, weekly, monthly,
* yearly.
Expand Down Expand Up @@ -319,12 +327,65 @@ public function fastForward(DateTimeInterface $dt)

/* Functions that advance the iterator {{{ */

/**
* Gets the original start time of the RRULE.
*
* The value is formatted as a string with 24-hour:minute:second
*/
protected function startTime(): string
{
return $this->startDate->format('H:i:s');
}

/**
* Advances currentDate by the interval.
* The time is set from the original startDate.
* If the recurrence is on a day when summer time started, then the
* time on that day may have jumped forward, for example, from 0230 to 0330.
* Using the original time means that the next recurrence will be calculated
* based on the original start time and the day/week/month/year interval.
* So the start time of the next occurrence can correctly revert to 0230.
*/
protected function advanceTheDate(string $interval): void
{
$this->currentDate = $this->currentDate->modify($interval.' '.$this->startTime());
}

/**
* Does the processing for adjusting the time of multi-hourly events when summer time starts.
*/
protected function adjustForTimeJumpsOfHourlyEvent(DateTimeInterface $previousEventDateTime): void
{
if (0 === $this->hourJump) {
// Remember if the clock time jumped forward on the next occurrence.
// That happens if the next event time is on a day when summer time starts
// and the event time is in the non-existent hour of the day.
// For example, an event that normally starts at 02:30 will
// have to start at 03:30 on that day.
// If the interval is just 1 hour, then there is no "jumping back" to do.
// The events that day will happen, for example, at 0030 0130 0330 0430 0530...
if ($this->interval > 1) {
$expectedHourOfNextDate = ((int) $previousEventDateTime->format('G') + $this->interval) % 24;
$actualHourOfNextDate = (int) $this->currentDate->format('G');
$this->hourJump = $actualHourOfNextDate - $expectedHourOfNextDate;
}
} else {
// The hour "jumped" for the previous occurrence, to avoid the non-existent time.
// currentDate got set ahead by (usually) 1 hour on that day.
// Adjust it back for this next occurrence.
$this->currentDate = $this->currentDate->sub(new \DateInterval('PT'.$this->hourJump.'H'));
$this->hourJump = 0;
}
}

/**
* Does the processing for advancing the iterator for hourly frequency.
*/
protected function nextHourly()
{
$previousEventDateTime = clone $this->currentDate;
$this->currentDate = $this->currentDate->modify('+'.$this->interval.' hours');
$this->adjustForTimeJumpsOfHourlyEvent($previousEventDateTime);
}

/**
Expand All @@ -333,7 +394,7 @@ protected function nextHourly()
protected function nextDaily()
{
if (!$this->byHour && !$this->byDay) {
$this->currentDate = $this->currentDate->modify('+'.$this->interval.' days');
$this->advanceTheDate('+'.$this->interval.' days');

return;
}
Expand Down Expand Up @@ -392,7 +453,7 @@ protected function nextDaily()
protected function nextWeekly()
{
if (!$this->byHour && !$this->byDay) {
$this->currentDate = $this->currentDate->modify('+'.$this->interval.' weeks');
$this->advanceTheDate('+'.$this->interval.' weeks');

return;
}
Expand All @@ -414,7 +475,7 @@ protected function nextWeekly()
if ($this->byHour) {
$this->currentDate = $this->currentDate->modify('+1 hours');
} else {
$this->currentDate = $this->currentDate->modify('+1 days');
$this->advanceTheDate('+1 days');
}

// Current day of the week
Expand Down Expand Up @@ -449,13 +510,13 @@ protected function nextMonthly()
// occur to the next month. We Must skip these invalid
// entries.
if ($currentDayOfMonth < 29) {
$this->currentDate = $this->currentDate->modify('+'.$this->interval.' months');
$this->advanceTheDate('+'.$this->interval.' months');
} else {
$increase = 0;
do {
++$increase;
$tempDate = clone $this->currentDate;
$tempDate = $tempDate->modify('+ '.($this->interval * $increase).' months');
$tempDate = $tempDate->modify('+ '.($this->interval * $increase).' months '.$this->startTime());
} while ($tempDate->format('j') != $currentDayOfMonth);
$this->currentDate = $tempDate;
}
Expand Down Expand Up @@ -506,11 +567,15 @@ protected function nextMonthly()
}
}

// Set the currentDate to the year and month that we are in, and the day of the month that we have selected.
// That day could be a day when summer time starts, and if the time of the event is, for example, 0230,
// then 0230 will not be a valid time on that day. So always apply the start time from the original startDate.
// The "modify" method will set the time forward to 0330, for example, if needed.
$this->currentDate = $this->currentDate->setDate(
(int) $this->currentDate->format('Y'),
(int) $this->currentDate->format('n'),
(int) $occurrence
);
)->modify($this->startTime());
}

/**
Expand Down Expand Up @@ -627,7 +692,7 @@ protected function nextYearly()
}

// The easiest form
$this->currentDate = $this->currentDate->modify('+'.$this->interval.' years');
$this->advanceTheDate('+'.$this->interval.' years');

return;
}
Expand Down Expand Up @@ -691,7 +756,7 @@ protected function nextYearly()
(int) $currentYear,
(int) $currentMonth,
(int) $occurrence
);
)->modify($this->startTime());

return;
} else {
Expand All @@ -708,7 +773,7 @@ protected function nextYearly()
(int) $currentYear,
(int) $currentMonth,
(int) $currentDayOfMonth
);
)->modify($this->startTime());

return;
}
Expand Down
Loading

0 comments on commit 2aeb4fc

Please sign in to comment.