From ee94da360858e7c574f54bbf3d4f2d7ac0914659 Mon Sep 17 00:00:00 2001 From: Cyril van Schreven Date: Mon, 22 Apr 2024 14:48:32 +0200 Subject: [PATCH 01/19] Reproduce bug where dst leap is passed on to subsequent occurences --- tests/VObject/Recur/RRuleIteratorTest.php | 58 +++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/tests/VObject/Recur/RRuleIteratorTest.php b/tests/VObject/Recur/RRuleIteratorTest.php index 3a6cbfe2..cb99915a 100644 --- a/tests/VObject/Recur/RRuleIteratorTest.php +++ b/tests/VObject/Recur/RRuleIteratorTest.php @@ -165,6 +165,64 @@ public function testDailyBySetPosLoop() ); } + /** + * @dataProvider dstTransitionProvider + */ + public function testDailyOnDstTransition(string $start, array $expected): void + { + $this->parse( + 'FREQ=DAILY;INTERVAL=1;COUNT=5', + $start, + $expected, + null, + 'Europe/Zurich', + ); + } + + public function dstTransitionProvider(): iterable + { + yield 'On transition start' => [ + 'Start' => '2023-03-24 02:00:00', + 'Expected' => [ + '2023-03-24 02:00:00', + '2023-03-25 02:00:00', + '2023-03-26 03:00:00', + '2023-03-27 02:00:00', + '2023-03-28 02:00:00', + ], + ]; + yield 'During transition' => [ + 'Start' => '2023-03-24 02:15:00', + 'Expected' => [ + '2023-03-24 02:15:00', + '2023-03-25 02:15:00', + '2023-03-26 03:15:00', + '2023-03-27 02:15:00', + '2023-03-28 02:15:00', + ], + ]; + yield 'On transition end' => [ + 'Start' => '2023-03-24 03:00:00', + 'Expected' => [ + '2023-03-24 03:00:00', + '2023-03-25 03:00:00', + '2023-03-26 03:00:00', + '2023-03-27 03:00:00', + '2023-03-28 03:00:00', + ], + ]; + yield 'After transition end' => [ + 'Start' => '2023-03-24 03:15:00', + 'Expected' => [ + '2023-03-24 03:15:00', + '2023-03-25 03:15:00', + '2023-03-26 03:15:00', + '2023-03-27 03:15:00', + '2023-03-28 03:15:00', + ], + ]; + } + public function testWeekly() { $this->parse( From 104c1e111a84d6550ae3c9cb964a636aaa6573ac Mon Sep 17 00:00:00 2001 From: Phil Davis Date: Thu, 9 May 2024 15:13:10 +0545 Subject: [PATCH 02/19] Handle summer time start for daily recurrences --- lib/Recur/RRuleIterator.php | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/lib/Recur/RRuleIterator.php b/lib/Recur/RRuleIterator.php index 8cf575e0..6f656c98 100644 --- a/lib/Recur/RRuleIterator.php +++ b/lib/Recur/RRuleIterator.php @@ -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 int $hourJump = 0; + /** * Frequency is one of: secondly, minutely, hourly, daily, weekly, monthly, * yearly. @@ -333,7 +341,23 @@ protected function nextHourly() protected function nextDaily() { if (!$this->byHour && !$this->byDay) { + $hourOfCurrentDate = (int) $this->currentDate->format('G'); $this->currentDate = $this->currentDate->modify('+'.$this->interval.' days'); + $hourOfNextDate = (int) $this->currentDate->format('G'); + if (0 === $this->hourJump) { + // Remember if the clock time jumped forward on the nextDate. + // That happens if nextDate is 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. + $this->hourJump = $hourOfNextDate - $hourOfCurrentDate; + } else { + // The hour "jumped" for the previous date, to avoid the non-existent time. + // currentDate got set ahead by (usually) one 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; + } return; } From 939daab6a801f9de73fb0f39c1ba5066e2e21214 Mon Sep 17 00:00:00 2001 From: Phil Davis Date: Thu, 9 May 2024 15:37:29 +0545 Subject: [PATCH 03/19] Handle summer time start for weekly recurrences --- lib/Recur/RRuleIterator.php | 16 ++++++ tests/VObject/Recur/RRuleIteratorTest.php | 62 ++++++++++++++++++++++- 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/lib/Recur/RRuleIterator.php b/lib/Recur/RRuleIterator.php index 6f656c98..bf2eb989 100644 --- a/lib/Recur/RRuleIterator.php +++ b/lib/Recur/RRuleIterator.php @@ -416,7 +416,23 @@ protected function nextDaily() protected function nextWeekly() { if (!$this->byHour && !$this->byDay) { + $hourOfCurrentDate = (int) $this->currentDate->format('G'); $this->currentDate = $this->currentDate->modify('+'.$this->interval.' weeks'); + $hourOfNextDate = (int) $this->currentDate->format('G'); + if (0 === $this->hourJump) { + // Remember if the clock time jumped forward on the nextDate. + // That happens if nextDate is 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. + $this->hourJump = $hourOfNextDate - $hourOfCurrentDate; + } else { + // The hour "jumped" for the previous date, to avoid the non-existent time. + // currentDate got set ahead by (usually) one 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; + } return; } diff --git a/tests/VObject/Recur/RRuleIteratorTest.php b/tests/VObject/Recur/RRuleIteratorTest.php index cb99915a..e561e79d 100644 --- a/tests/VObject/Recur/RRuleIteratorTest.php +++ b/tests/VObject/Recur/RRuleIteratorTest.php @@ -166,7 +166,7 @@ public function testDailyBySetPosLoop() } /** - * @dataProvider dstTransitionProvider + * @dataProvider dstDailyTransitionProvider */ public function testDailyOnDstTransition(string $start, array $expected): void { @@ -179,7 +179,7 @@ public function testDailyOnDstTransition(string $start, array $expected): void ); } - public function dstTransitionProvider(): iterable + public function dstDailyTransitionProvider(): iterable { yield 'On transition start' => [ 'Start' => '2023-03-24 02:00:00', @@ -326,6 +326,64 @@ public function testWeeklyByDaySpecificHour() ); } + /** + * @dataProvider dstWeeklyTransitionProvider + */ + public function testWeeklyOnDstTransition(string $start, array $expected): void + { + $this->parse( + 'FREQ=WEEKLY;INTERVAL=1;COUNT=5', + $start, + $expected, + null, + 'Europe/Zurich', + ); + } + + public function dstWeeklyTransitionProvider(): iterable + { + yield 'On transition start' => [ + 'Start' => '2023-03-12 02:00:00', + 'Expected' => [ + '2023-03-12 02:00:00', + '2023-03-19 02:00:00', + '2023-03-26 03:00:00', + '2023-04-02 02:00:00', + '2023-04-09 02:00:00', + ], + ]; + yield 'During transition' => [ + 'Start' => '2023-03-12 02:15:00', + 'Expected' => [ + '2023-03-12 02:15:00', + '2023-03-19 02:15:00', + '2023-03-26 03:15:00', + '2023-04-02 02:15:00', + '2023-04-09 02:15:00', + ], + ]; + yield 'On transition end' => [ + 'Start' => '2023-03-12 03:00:00', + 'Expected' => [ + '2023-03-12 03:00:00', + '2023-03-19 03:00:00', + '2023-03-26 03:00:00', + '2023-04-02 03:00:00', + '2023-04-09 03:00:00', + ], + ]; + yield 'After transition end' => [ + 'Start' => '2023-03-12 03:15:00', + 'Expected' => [ + '2023-03-12 03:15:00', + '2023-03-19 03:15:00', + '2023-03-26 03:15:00', + '2023-04-02 03:15:00', + '2023-04-09 03:15:00', + ], + ]; + } + public function testMonthly() { $this->parse( From f7423aa6a4c8f9b7aaf128e02e31968a1a7f31e6 Mon Sep 17 00:00:00 2001 From: Phil Davis Date: Thu, 9 May 2024 15:45:34 +0545 Subject: [PATCH 04/19] Handle summer time start for monthly recurrences --- lib/Recur/RRuleIterator.php | 16 +++++++ tests/VObject/Recur/RRuleIteratorTest.php | 58 +++++++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/lib/Recur/RRuleIterator.php b/lib/Recur/RRuleIterator.php index bf2eb989..312f6289 100644 --- a/lib/Recur/RRuleIterator.php +++ b/lib/Recur/RRuleIterator.php @@ -489,7 +489,23 @@ protected function nextMonthly() // occur to the next month. We Must skip these invalid // entries. if ($currentDayOfMonth < 29) { + $hourOfCurrentDate = (int) $this->currentDate->format('G'); $this->currentDate = $this->currentDate->modify('+'.$this->interval.' months'); + $hourOfNextDate = (int) $this->currentDate->format('G'); + if (0 === $this->hourJump) { + // Remember if the clock time jumped forward on the nextDate. + // That happens if nextDate is 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. + $this->hourJump = $hourOfNextDate - $hourOfCurrentDate; + } else { + // The hour "jumped" for the previous date, to avoid the non-existent time. + // currentDate got set ahead by (usually) one 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; + } } else { $increase = 0; do { diff --git a/tests/VObject/Recur/RRuleIteratorTest.php b/tests/VObject/Recur/RRuleIteratorTest.php index e561e79d..2bd8217e 100644 --- a/tests/VObject/Recur/RRuleIteratorTest.php +++ b/tests/VObject/Recur/RRuleIteratorTest.php @@ -533,6 +533,64 @@ public function testMonthlyByDayBySetPos() ); } + /** + * @dataProvider dstMonthlyTransitionProvider + */ + public function testMonthlyOnDstTransition(string $start, array $expected): void + { + $this->parse( + 'FREQ=MONTHLY;INTERVAL=1;COUNT=5', + $start, + $expected, + null, + 'Europe/Zurich', + ); + } + + public function dstMonthlyTransitionProvider(): iterable + { + yield 'On transition start' => [ + 'Start' => '2023-01-26 02:00:00', + 'Expected' => [ + '2023-01-26 02:00:00', + '2023-02-26 02:00:00', + '2023-03-26 03:00:00', + '2023-04-26 02:00:00', + '2023-05-26 02:00:00', + ], + ]; + yield 'During transition' => [ + 'Start' => '2023-01-26 02:15:00', + 'Expected' => [ + '2023-01-26 02:15:00', + '2023-02-26 02:15:00', + '2023-03-26 03:15:00', + '2023-04-26 02:15:00', + '2023-05-26 02:15:00', + ], + ]; + yield 'On transition end' => [ + 'Start' => '2023-01-26 03:00:00', + 'Expected' => [ + '2023-01-26 03:00:00', + '2023-02-26 03:00:00', + '2023-03-26 03:00:00', + '2023-04-26 03:00:00', + '2023-05-26 03:00:00', + ], + ]; + yield 'After transition end' => [ + 'Start' => '2023-01-26 03:15:00', + 'Expected' => [ + '2023-01-26 03:15:00', + '2023-02-26 03:15:00', + '2023-03-26 03:15:00', + '2023-04-26 03:15:00', + '2023-05-26 03:15:00', + ], + ]; + } + public function testYearly() { $this->parse( From fb5766ceb974c1b913f79bdafcc4b217e16b4ca3 Mon Sep 17 00:00:00 2001 From: Phil Davis Date: Thu, 9 May 2024 15:53:12 +0545 Subject: [PATCH 05/19] Handle summer time start for yearly recurrences --- lib/Recur/RRuleIterator.php | 16 +++++++ tests/VObject/Recur/RRuleIteratorTest.php | 58 +++++++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/lib/Recur/RRuleIterator.php b/lib/Recur/RRuleIterator.php index 312f6289..e0fd8922 100644 --- a/lib/Recur/RRuleIterator.php +++ b/lib/Recur/RRuleIterator.php @@ -683,7 +683,23 @@ protected function nextYearly() } // The easiest form + $hourOfCurrentDate = (int) $this->currentDate->format('G'); $this->currentDate = $this->currentDate->modify('+'.$this->interval.' years'); + $hourOfNextDate = (int) $this->currentDate->format('G'); + if (0 === $this->hourJump) { + // Remember if the clock time jumped forward on the nextDate. + // That happens if nextDate is 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. + $this->hourJump = $hourOfNextDate - $hourOfCurrentDate; + } else { + // The hour "jumped" for the previous date, to avoid the non-existent time. + // currentDate got set ahead by (usually) one 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; + } return; } diff --git a/tests/VObject/Recur/RRuleIteratorTest.php b/tests/VObject/Recur/RRuleIteratorTest.php index 2bd8217e..6e6dcc1c 100644 --- a/tests/VObject/Recur/RRuleIteratorTest.php +++ b/tests/VObject/Recur/RRuleIteratorTest.php @@ -887,6 +887,64 @@ public function testYearlyByDayByWeekNo() ); } + /** + * @dataProvider dstYearlyTransitionProvider + */ + public function testYearlyOnDstTransition(string $start, array $expected): void + { + $this->parse( + 'FREQ=YEARLY;INTERVAL=1;COUNT=5', + $start, + $expected, + null, + 'Europe/Zurich', + ); + } + + public function dstYearlyTransitionProvider(): iterable + { + yield 'On transition start' => [ + 'Start' => '2021-03-26 02:00:00', + 'Expected' => [ + '2021-03-26 02:00:00', + '2022-03-26 02:00:00', + '2023-03-26 03:00:00', + '2024-03-26 02:00:00', + '2025-03-26 02:00:00', + ], + ]; + yield 'During transition' => [ + 'Start' => '2021-03-26 02:15:00', + 'Expected' => [ + '2021-03-26 02:15:00', + '2022-03-26 02:15:00', + '2023-03-26 03:15:00', + '2024-03-26 02:15:00', + '2025-03-26 02:15:00', + ], + ]; + yield 'On transition end' => [ + 'Start' => '2021-03-26 03:00:00', + 'Expected' => [ + '2021-03-26 03:00:00', + '2022-03-26 03:00:00', + '2023-03-26 03:00:00', + '2024-03-26 03:00:00', + '2025-03-26 03:00:00', + ], + ]; + yield 'After transition end' => [ + 'Start' => '2021-03-26 03:15:00', + 'Expected' => [ + '2021-03-26 03:15:00', + '2022-03-26 03:15:00', + '2023-03-26 03:15:00', + '2024-03-26 03:15:00', + '2025-03-26 03:15:00', + ], + ]; + } + public function testFastForward() { // The idea is that we're fast-forwarding too far in the future, so From e1e6e55ad5ae9228cff3bfdaf8f7b5dd910808c0 Mon Sep 17 00:00:00 2001 From: Phil Davis Date: Thu, 9 May 2024 16:07:36 +0545 Subject: [PATCH 06/19] Refactor summer time start logic into advanceTheDate function --- lib/Recur/RRuleIterator.php | 99 ++++++++++++------------------------- 1 file changed, 31 insertions(+), 68 deletions(-) diff --git a/lib/Recur/RRuleIterator.php b/lib/Recur/RRuleIterator.php index e0fd8922..fce7199e 100644 --- a/lib/Recur/RRuleIterator.php +++ b/lib/Recur/RRuleIterator.php @@ -327,6 +327,33 @@ public function fastForward(DateTimeInterface $dt) /* Functions that advance the iterator {{{ */ + /** + * Advances currentDate by the interval. + * Takes into account the case where summer time starts and + * the event time on that day may have had to be advanced, + * usually by 1 hour. + */ + protected function advanceTheDate(string $interval): void + { + $hourOfCurrentDate = (int) $this->currentDate->format('G'); + $this->currentDate = $this->currentDate->modify($interval); + $hourOfNextDate = (int) $this->currentDate->format('G'); + if (0 === $this->hourJump) { + // Remember if the clock time jumped forward on the next date. + // That happens if the next date is 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. + $this->hourJump = $hourOfNextDate - $hourOfCurrentDate; + } else { + // The hour "jumped" for the previous date, 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. */ @@ -341,23 +368,7 @@ protected function nextHourly() protected function nextDaily() { if (!$this->byHour && !$this->byDay) { - $hourOfCurrentDate = (int) $this->currentDate->format('G'); - $this->currentDate = $this->currentDate->modify('+'.$this->interval.' days'); - $hourOfNextDate = (int) $this->currentDate->format('G'); - if (0 === $this->hourJump) { - // Remember if the clock time jumped forward on the nextDate. - // That happens if nextDate is 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. - $this->hourJump = $hourOfNextDate - $hourOfCurrentDate; - } else { - // The hour "jumped" for the previous date, to avoid the non-existent time. - // currentDate got set ahead by (usually) one 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; - } + $this->advanceTheDate('+'.$this->interval.' days'); return; } @@ -416,23 +427,7 @@ protected function nextDaily() protected function nextWeekly() { if (!$this->byHour && !$this->byDay) { - $hourOfCurrentDate = (int) $this->currentDate->format('G'); - $this->currentDate = $this->currentDate->modify('+'.$this->interval.' weeks'); - $hourOfNextDate = (int) $this->currentDate->format('G'); - if (0 === $this->hourJump) { - // Remember if the clock time jumped forward on the nextDate. - // That happens if nextDate is 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. - $this->hourJump = $hourOfNextDate - $hourOfCurrentDate; - } else { - // The hour "jumped" for the previous date, to avoid the non-existent time. - // currentDate got set ahead by (usually) one 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; - } + $this->advanceTheDate('+'.$this->interval.' weeks'); return; } @@ -489,23 +484,7 @@ protected function nextMonthly() // occur to the next month. We Must skip these invalid // entries. if ($currentDayOfMonth < 29) { - $hourOfCurrentDate = (int) $this->currentDate->format('G'); - $this->currentDate = $this->currentDate->modify('+'.$this->interval.' months'); - $hourOfNextDate = (int) $this->currentDate->format('G'); - if (0 === $this->hourJump) { - // Remember if the clock time jumped forward on the nextDate. - // That happens if nextDate is 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. - $this->hourJump = $hourOfNextDate - $hourOfCurrentDate; - } else { - // The hour "jumped" for the previous date, to avoid the non-existent time. - // currentDate got set ahead by (usually) one 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; - } + $this->advanceTheDate('+'.$this->interval.' months'); } else { $increase = 0; do { @@ -683,23 +662,7 @@ protected function nextYearly() } // The easiest form - $hourOfCurrentDate = (int) $this->currentDate->format('G'); - $this->currentDate = $this->currentDate->modify('+'.$this->interval.' years'); - $hourOfNextDate = (int) $this->currentDate->format('G'); - if (0 === $this->hourJump) { - // Remember if the clock time jumped forward on the nextDate. - // That happens if nextDate is 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. - $this->hourJump = $hourOfNextDate - $hourOfCurrentDate; - } else { - // The hour "jumped" for the previous date, to avoid the non-existent time. - // currentDate got set ahead by (usually) one 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; - } + $this->advanceTheDate('+'.$this->interval.' years'); return; } From a8b8e88cda723a918aaa09e51a8ea561b768bd8d Mon Sep 17 00:00:00 2001 From: Phil Davis Date: Thu, 9 May 2024 17:16:55 +0545 Subject: [PATCH 07/19] Handle summer time start for hourly recurrences --- lib/Recur/RRuleIterator.php | 21 ++++ tests/VObject/Recur/RRuleIteratorTest.php | 116 ++++++++++++++++++++++ 2 files changed, 137 insertions(+) diff --git a/lib/Recur/RRuleIterator.php b/lib/Recur/RRuleIterator.php index fce7199e..fb1755e4 100644 --- a/lib/Recur/RRuleIterator.php +++ b/lib/Recur/RRuleIterator.php @@ -359,7 +359,28 @@ protected function advanceTheDate(string $interval): void */ protected function nextHourly() { + $hourOfCurrentDate = (int) $this->currentDate->format('G'); $this->currentDate = $this->currentDate->modify('+'.$this->interval.' hours'); + 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 0130 0330 0430 0530... + if ($this->interval > 1) { + $expectedHourOfNextDate = ($hourOfCurrentDate + $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; + } } /** diff --git a/tests/VObject/Recur/RRuleIteratorTest.php b/tests/VObject/Recur/RRuleIteratorTest.php index 6e6dcc1c..5448829f 100644 --- a/tests/VObject/Recur/RRuleIteratorTest.php +++ b/tests/VObject/Recur/RRuleIteratorTest.php @@ -32,6 +32,122 @@ public function testHourly() ); } + /** + * @dataProvider dst2HourlyTransitionProvider + */ + public function test2HourlyOnDstTransition(string $start, array $expected): void + { + $this->parse( + 'FREQ=HOURLY;INTERVAL=2;COUNT=5', + $start, + $expected, + null, + 'Europe/Zurich', + ); + } + + public function dst2HourlyTransitionProvider(): iterable + { + yield 'On transition start' => [ + 'Start' => '2023-03-26 00:00:00', + 'Expected' => [ + '2023-03-26 00:00:00', + '2023-03-26 03:00:00', + '2023-03-26 04:00:00', + '2023-03-26 06:00:00', + '2023-03-26 08:00:00', + ], + ]; + yield 'During transition' => [ + 'Start' => '2023-03-26 00:15:00', + 'Expected' => [ + '2023-03-26 00:15:00', + '2023-03-26 03:15:00', + '2023-03-26 04:15:00', + '2023-03-26 06:15:00', + '2023-03-26 08:15:00', + ], + ]; + yield 'On transition end' => [ + 'Start' => '2023-03-26 01:00:00', + 'Expected' => [ + '2023-03-26 01:00:00', + '2023-03-26 03:00:00', + '2023-03-26 05:00:00', + '2023-03-26 07:00:00', + '2023-03-26 09:00:00', + ], + ]; + yield 'After transition end' => [ + 'Start' => '2023-03-26 01:15:00', + 'Expected' => [ + '2023-03-26 01:15:00', + '2023-03-26 03:15:00', + '2023-03-26 05:15:00', + '2023-03-26 07:15:00', + '2023-03-26 09:15:00', + ], + ]; + } + + /** + * @dataProvider dst6HourlyTransitionProvider + */ + public function testHourlyOnDstTransition(string $start, array $expected): void + { + $this->parse( + 'FREQ=HOURLY;INTERVAL=6;COUNT=5', + $start, + $expected, + null, + 'Europe/Zurich', + ); + } + + public function dst6HourlyTransitionProvider(): iterable + { + yield 'On transition start' => [ + 'Start' => '2023-03-25 20:00:00', + 'Expected' => [ + '2023-03-25 20:00:00', + '2023-03-26 03:00:00', + '2023-03-26 08:00:00', + '2023-03-26 14:00:00', + '2023-03-26 20:00:00', + ], + ]; + yield 'During transition' => [ + 'Start' => '2023-03-25 20:15:00', + 'Expected' => [ + '2023-03-25 20:15:00', + '2023-03-26 03:15:00', + '2023-03-26 08:15:00', + '2023-03-26 14:15:00', + '2023-03-26 20:15:00', + ], + ]; + yield 'On transition end' => [ + 'Start' => '2023-03-25 21:00:00', + 'Expected' => [ + '2023-03-25 21:00:00', + '2023-03-26 03:00:00', + '2023-03-26 09:00:00', + '2023-03-26 15:00:00', + '2023-03-26 21:00:00', + ], + ]; + yield 'After transition end' => [ + 'Start' => '2023-03-25 21:15:00', + 'Expected' => [ + '2023-03-25 21:15:00', + '2023-03-26 03:15:00', + '2023-03-26 09:15:00', + '2023-03-26 15:15:00', + '2023-03-26 21:15:00', + ], + ]; + } + public function testDaily() { $this->parse( From 54811755aad2e2921fe7d159bbb40f09a465f9bf Mon Sep 17 00:00:00 2001 From: Phil Davis Date: Thu, 9 May 2024 17:21:14 +0545 Subject: [PATCH 08/19] Refactor advanceTheDate --- lib/Recur/RRuleIterator.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/Recur/RRuleIterator.php b/lib/Recur/RRuleIterator.php index fb1755e4..617a88c2 100644 --- a/lib/Recur/RRuleIterator.php +++ b/lib/Recur/RRuleIterator.php @@ -335,16 +335,16 @@ public function fastForward(DateTimeInterface $dt) */ protected function advanceTheDate(string $interval): void { - $hourOfCurrentDate = (int) $this->currentDate->format('G'); + $hourOfPreviousDate = (int) $this->currentDate->format('G'); $this->currentDate = $this->currentDate->modify($interval); - $hourOfNextDate = (int) $this->currentDate->format('G'); if (0 === $this->hourJump) { // Remember if the clock time jumped forward on the next date. // That happens if the next date is 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. - $this->hourJump = $hourOfNextDate - $hourOfCurrentDate; + $hourOfNextDate = (int) $this->currentDate->format('G'); + $this->hourJump = $hourOfNextDate - $hourOfPreviousDate; } else { // The hour "jumped" for the previous date, to avoid the non-existent time. // currentDate got set ahead by (usually) 1 hour on that day. From 92de1083f0f17ce4940f6e843737522395a7f65f Mon Sep 17 00:00:00 2001 From: Phil Davis Date: Fri, 17 May 2024 14:23:41 +0545 Subject: [PATCH 09/19] fix: refactor advanceTheDate --- lib/Recur/RRuleIterator.php | 27 +++++++-------------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/lib/Recur/RRuleIterator.php b/lib/Recur/RRuleIterator.php index 617a88c2..4a817f34 100644 --- a/lib/Recur/RRuleIterator.php +++ b/lib/Recur/RRuleIterator.php @@ -329,29 +329,16 @@ public function fastForward(DateTimeInterface $dt) /** * Advances currentDate by the interval. - * Takes into account the case where summer time starts and - * the event time on that day may have had to be advanced, - * usually by 1 hour. + * 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 { - $hourOfPreviousDate = (int) $this->currentDate->format('G'); - $this->currentDate = $this->currentDate->modify($interval); - if (0 === $this->hourJump) { - // Remember if the clock time jumped forward on the next date. - // That happens if the next date is 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. - $hourOfNextDate = (int) $this->currentDate->format('G'); - $this->hourJump = $hourOfNextDate - $hourOfPreviousDate; - } else { - // The hour "jumped" for the previous date, 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; - } + $this->currentDate = $this->currentDate->modify($interval.' '.$this->startDate->format('H:i:s')); } /** From 71f14119cc3aa8d57c14321730889d993589cb35 Mon Sep 17 00:00:00 2001 From: Phil Davis Date: Fri, 17 May 2024 15:28:30 +0545 Subject: [PATCH 10/19] Handle case when BYMONTHDAY falls on summer time start --- lib/Recur/RRuleIterator.php | 6 +++++- tests/VObject/Recur/RRuleIteratorTest.php | 20 ++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/lib/Recur/RRuleIterator.php b/lib/Recur/RRuleIterator.php index 4a817f34..be23b90c 100644 --- a/lib/Recur/RRuleIterator.php +++ b/lib/Recur/RRuleIterator.php @@ -549,11 +549,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->startDate->format('H:i:s')); } /** diff --git a/tests/VObject/Recur/RRuleIteratorTest.php b/tests/VObject/Recur/RRuleIteratorTest.php index 5448829f..4843bf1e 100644 --- a/tests/VObject/Recur/RRuleIteratorTest.php +++ b/tests/VObject/Recur/RRuleIteratorTest.php @@ -556,6 +556,26 @@ public function testMonthlyByMonthDay() ); } + public function testMonthlyByMonthDayDstTransition(): void + { + $this->parse( + 'FREQ=MONTHLY;INTERVAL=1;COUNT=8;BYMONTHDAY=1,26', + '2023-01-01 02:15:00', + [ + '2023-01-01 02:15:00', + '2023-01-26 02:15:00', + '2023-02-01 02:15:00', + '2023-02-26 02:15:00', + '2023-03-01 02:15:00', + '2023-03-26 03:15:00', + '2023-04-01 02:15:00', + '2023-04-26 02:15:00', + ], + null, + 'Europe/Zurich', + ); + } + public function testMonthlyByDay() { $this->parse( From 057bbceb36662fdac2a06d828e19fa352569aa8b Mon Sep 17 00:00:00 2001 From: Phil Davis Date: Fri, 17 May 2024 15:50:44 +0545 Subject: [PATCH 11/19] Handle case when day at or near end of month falls on summer time start --- lib/Recur/RRuleIterator.php | 2 +- tests/VObject/Recur/RRuleIteratorTest.php | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/Recur/RRuleIterator.php b/lib/Recur/RRuleIterator.php index be23b90c..3fb4e4df 100644 --- a/lib/Recur/RRuleIterator.php +++ b/lib/Recur/RRuleIterator.php @@ -498,7 +498,7 @@ protected function nextMonthly() do { ++$increase; $tempDate = clone $this->currentDate; - $tempDate = $tempDate->modify('+ '.($this->interval * $increase).' months'); + $tempDate = $tempDate->modify('+ '.($this->interval * $increase).' months '.$this->startDate->format('H:i:s')); } while ($tempDate->format('j') != $currentDayOfMonth); $this->currentDate = $tempDate; } diff --git a/tests/VObject/Recur/RRuleIteratorTest.php b/tests/VObject/Recur/RRuleIteratorTest.php index 4843bf1e..3d36f540 100644 --- a/tests/VObject/Recur/RRuleIteratorTest.php +++ b/tests/VObject/Recur/RRuleIteratorTest.php @@ -725,6 +725,16 @@ public function dstMonthlyTransitionProvider(): iterable '2023-05-26 03:15:00', ], ]; + yield 'During transition on 31st day of month' => [ + 'Start' => '2024-01-31 02:15:00', + 'Expected' => [ + '2024-01-31 02:15:00', + '2024-03-31 03:15:00', + '2024-05-31 02:15:00', + '2024-07-31 02:15:00', + '2024-08-31 02:15:00', + ], + ]; } public function testYearly() From a7150a94a5b7e14f7b3efda0a629b749806a5b81 Mon Sep 17 00:00:00 2001 From: Phil Davis Date: Mon, 27 May 2024 14:08:12 +0545 Subject: [PATCH 12/19] refactor hourly time jump logic into adjustForTimeJumpsOfHourlyEvent private method --- lib/Recur/RRuleIterator.php | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/lib/Recur/RRuleIterator.php b/lib/Recur/RRuleIterator.php index 3fb4e4df..d7c0e0e0 100644 --- a/lib/Recur/RRuleIterator.php +++ b/lib/Recur/RRuleIterator.php @@ -342,12 +342,10 @@ protected function advanceTheDate(string $interval): void } /** - * Does the processing for advancing the iterator for hourly frequency. + * Does the processing for adjusting the time of multi-hourly events when summer time starts. */ - protected function nextHourly() + private function adjustForTimeJumpsOfHourlyEvent(DateTimeInterface $previousEventDateTime): void { - $hourOfCurrentDate = (int) $this->currentDate->format('G'); - $this->currentDate = $this->currentDate->modify('+'.$this->interval.' hours'); 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 @@ -355,9 +353,9 @@ protected function nextHourly() // 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 0130 0330 0430 0530... + // The events that day will happen, for example, at 0030 0130 0330 0430 0530... if ($this->interval > 1) { - $expectedHourOfNextDate = ($hourOfCurrentDate + $this->interval) % 24; + $expectedHourOfNextDate = ((int) $previousEventDateTime->format('G') + $this->interval) % 24; $actualHourOfNextDate = (int) $this->currentDate->format('G'); $this->hourJump = $actualHourOfNextDate - $expectedHourOfNextDate; } @@ -370,6 +368,16 @@ protected function nextHourly() } } + /** + * 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); + } + /** * Does the processing for advancing the iterator for daily frequency. */ From 3ebd1fdb68dcb320df63d017a4d5b25be4488177 Mon Sep 17 00:00:00 2001 From: Phil Davis Date: Mon, 27 May 2024 14:31:48 +0545 Subject: [PATCH 13/19] refactor original start time calculation into startTime method --- lib/Recur/RRuleIterator.php | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/Recur/RRuleIterator.php b/lib/Recur/RRuleIterator.php index d7c0e0e0..0757cebc 100644 --- a/lib/Recur/RRuleIterator.php +++ b/lib/Recur/RRuleIterator.php @@ -327,6 +327,16 @@ 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. @@ -338,7 +348,7 @@ public function fastForward(DateTimeInterface $dt) */ protected function advanceTheDate(string $interval): void { - $this->currentDate = $this->currentDate->modify($interval.' '.$this->startDate->format('H:i:s')); + $this->currentDate = $this->currentDate->modify($interval.' '.$this->startTime()); } /** @@ -506,7 +516,7 @@ protected function nextMonthly() do { ++$increase; $tempDate = clone $this->currentDate; - $tempDate = $tempDate->modify('+ '.($this->interval * $increase).' months '.$this->startDate->format('H:i:s')); + $tempDate = $tempDate->modify('+ '.($this->interval * $increase).' months '.$this->startTime()); } while ($tempDate->format('j') != $currentDayOfMonth); $this->currentDate = $tempDate; } @@ -565,7 +575,7 @@ protected function nextMonthly() (int) $this->currentDate->format('Y'), (int) $this->currentDate->format('n'), (int) $occurrence - )->modify($this->startDate->format('H:i:s')); + )->modify($this->startTime()); } /** From 01ec1ba32dd7ae2026b8dee1b5f30b1ac71c49e1 Mon Sep 17 00:00:00 2001 From: Phil Davis Date: Mon, 27 May 2024 14:33:03 +0545 Subject: [PATCH 14/19] refactor adjustForTimeJumpsOfHourlyEvent to be protected --- lib/Recur/RRuleIterator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Recur/RRuleIterator.php b/lib/Recur/RRuleIterator.php index 0757cebc..2849e32a 100644 --- a/lib/Recur/RRuleIterator.php +++ b/lib/Recur/RRuleIterator.php @@ -354,7 +354,7 @@ protected function advanceTheDate(string $interval): void /** * Does the processing for adjusting the time of multi-hourly events when summer time starts. */ - private function adjustForTimeJumpsOfHourlyEvent(DateTimeInterface $previousEventDateTime): void + protected function adjustForTimeJumpsOfHourlyEvent(DateTimeInterface $previousEventDateTime): void { if (0 === $this->hourJump) { // Remember if the clock time jumped forward on the next occurrence. From 9c4e5a851240b87b2b3a4f38f3b4decc6f216804 Mon Sep 17 00:00:00 2001 From: Phil Davis Date: Thu, 30 May 2024 12:31:10 +0545 Subject: [PATCH 15/19] Handle summer time start for weekly BYDAY recurrences --- lib/Recur/RRuleIterator.php | 2 +- tests/VObject/Recur/RRuleIteratorTest.php | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/lib/Recur/RRuleIterator.php b/lib/Recur/RRuleIterator.php index 2849e32a..4a07382d 100644 --- a/lib/Recur/RRuleIterator.php +++ b/lib/Recur/RRuleIterator.php @@ -475,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 diff --git a/tests/VObject/Recur/RRuleIteratorTest.php b/tests/VObject/Recur/RRuleIteratorTest.php index 3d36f540..687ffbcd 100644 --- a/tests/VObject/Recur/RRuleIteratorTest.php +++ b/tests/VObject/Recur/RRuleIteratorTest.php @@ -442,6 +442,24 @@ public function testWeeklyByDaySpecificHour() ); } + public function testWeeklyByDaySpecificHourOnDstTransition(): void + { + $this->parse( + 'FREQ=WEEKLY;INTERVAL=2;BYDAY=SA,SU', + '2023-03-11 02:30:00', + [ + '2023-03-11 02:30:00', + '2023-03-12 02:30:00', + '2023-03-25 02:30:00', + '2023-03-26 03:30:00', + '2023-04-08 02:30:00', + '2023-04-09 02:30:00', + ], + null, + 'Europe/Zurich', + ); + } + /** * @dataProvider dstWeeklyTransitionProvider */ From 7d3b632b656625d3cb82d3a592374b87c12e8e93 Mon Sep 17 00:00:00 2001 From: Phil Davis Date: Thu, 30 May 2024 12:52:42 +0545 Subject: [PATCH 16/19] Add test case for Weekly BYDAY with BYHOUR on summer-time --- tests/VObject/Recur/RRuleIteratorTest.php | 28 +++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/VObject/Recur/RRuleIteratorTest.php b/tests/VObject/Recur/RRuleIteratorTest.php index 687ffbcd..82e1496a 100644 --- a/tests/VObject/Recur/RRuleIteratorTest.php +++ b/tests/VObject/Recur/RRuleIteratorTest.php @@ -460,6 +460,34 @@ public function testWeeklyByDaySpecificHourOnDstTransition(): void ); } + public function testWeeklyByDayByHourOnDstTransition(): void + { + $this->parse( + 'FREQ=WEEKLY;INTERVAL=2;BYDAY=SA,SU;WKST=MO;BYHOUR=2,14', + '2023-03-11 02:00:00', + [ + '2023-03-11 02:00:00', + '2023-03-11 14:00:00', + '2023-03-12 02:00:00', + '2023-03-12 14:00:00', + '2023-03-25 02:00:00', + '2023-03-25 14:00:00', + // 02:00:00 does not exist on 2023-03-26 because of summer-time start. + // The current implementation logic does not schedule a recurrence on + // the morning of 2023-03-26. But maybe it should schedule one at 03:00:00. + // The RFC is silent about the required behavior in this case. + // '2023-03-26 03:00:00', + '2023-03-26 14:00:00', + '2023-04-08 02:00:00', + '2023-04-08 14:00:00', + '2023-04-09 02:00:00', + '2023-04-09 14:00:00', + ], + null, + 'Europe/Zurich', + ); + } + /** * @dataProvider dstWeeklyTransitionProvider */ From f4c64dd7131284021a0b34828fa110be5c2ed2bb Mon Sep 17 00:00:00 2001 From: Phil Davis Date: Thu, 30 May 2024 13:49:01 +0545 Subject: [PATCH 17/19] Add test cases and fix YEARLY with BYMONTH on summer-time transition --- lib/Recur/RRuleIterator.php | 2 +- tests/VObject/Recur/RRuleIteratorTest.php | 45 +++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/lib/Recur/RRuleIterator.php b/lib/Recur/RRuleIterator.php index 4a07382d..1743b7eb 100644 --- a/lib/Recur/RRuleIterator.php +++ b/lib/Recur/RRuleIterator.php @@ -773,7 +773,7 @@ protected function nextYearly() (int) $currentYear, (int) $currentMonth, (int) $currentDayOfMonth - ); + )->modify($this->startTime()); return; } diff --git a/tests/VObject/Recur/RRuleIteratorTest.php b/tests/VObject/Recur/RRuleIteratorTest.php index 82e1496a..7a7a068f 100644 --- a/tests/VObject/Recur/RRuleIteratorTest.php +++ b/tests/VObject/Recur/RRuleIteratorTest.php @@ -664,6 +664,31 @@ public function testMonthlyByDayUntil() ); } + public function testMonthlyByDayOnDstTransition(): void + { + $this->parse( + 'FREQ=MONTHLY;INTERVAL=2;COUNT=13;BYDAY=SU', + '2023-01-01 02:30:00', + [ + '2023-01-01 02:30:00', + '2023-01-08 02:30:00', + '2023-01-15 02:30:00', + '2023-01-22 02:30:00', + '2023-01-29 02:30:00', + '2023-03-05 02:30:00', + '2023-03-12 02:30:00', + '2023-03-19 02:30:00', + '2023-03-26 03:30:00', + '2023-05-07 02:30:00', + '2023-05-14 02:30:00', + '2023-05-21 02:30:00', + '2023-05-28 02:30:00', + ], + null, + 'Europe/Zurich', + ); + } + public function testMonthlyByDayUntilWithImpossibleNextOccurrence() { $this->parse( @@ -834,6 +859,26 @@ public function testYearlyByMonth() ); } + public function testYearlyByMonthOnDstTransition(): void + { + $this->parse( + 'FREQ=YEARLY;COUNT=8;INTERVAL=2;BYMONTH=3,9', + '2019-03-26 02:30:00', + [ + '2019-03-26 02:30:00', + '2019-09-26 02:30:00', + '2021-03-26 02:30:00', + '2021-09-26 02:30:00', + '2023-03-26 03:30:00', + '2023-09-26 02:30:00', + '2025-03-26 02:30:00', + '2025-09-26 02:30:00', + ], + null, + 'Europe/Zurich', + ); + } + public function testYearlyByMonthInvalidValue1() { $this->expectException(InvalidDataException::class); From 5c17d30d4629c8e3239167b23303febe714e5ba5 Mon Sep 17 00:00:00 2001 From: Phil Davis Date: Thu, 30 May 2024 14:01:25 +0545 Subject: [PATCH 18/19] Add test cases and fix YEARLY with BYMONTH BYDAY on summer-time transition --- lib/Recur/RRuleIterator.php | 2 +- tests/VObject/Recur/RRuleIteratorTest.php | 25 +++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/lib/Recur/RRuleIterator.php b/lib/Recur/RRuleIterator.php index 1743b7eb..dd80d9cb 100644 --- a/lib/Recur/RRuleIterator.php +++ b/lib/Recur/RRuleIterator.php @@ -756,7 +756,7 @@ protected function nextYearly() (int) $currentYear, (int) $currentMonth, (int) $occurrence - ); + )->modify($this->startTime()); return; } else { diff --git a/tests/VObject/Recur/RRuleIteratorTest.php b/tests/VObject/Recur/RRuleIteratorTest.php index 7a7a068f..e5dbab14 100644 --- a/tests/VObject/Recur/RRuleIteratorTest.php +++ b/tests/VObject/Recur/RRuleIteratorTest.php @@ -937,6 +937,31 @@ public function testYearlyByMonthByDay() ); } + public function testYearlyByMonthByDayOnDstTransition(): void + { + $this->parse( + 'FREQ=YEARLY;COUNT=13;INTERVAL=2;BYMONTH=3;BYDAY=SU', + '2021-03-07 02:30:00', + [ + '2021-03-07 02:30:00', + '2021-03-14 02:30:00', + '2021-03-21 02:30:00', + '2021-03-28 03:30:00', + '2023-03-05 02:30:00', + '2023-03-12 02:30:00', + '2023-03-19 02:30:00', + '2023-03-26 03:30:00', + '2025-03-02 02:30:00', + '2025-03-09 02:30:00', + '2025-03-16 02:30:00', + '2025-03-23 02:30:00', + '2025-03-30 03:30:00', + ], + null, + 'Europe/Zurich', + ); + } + public function testYearlyNewYearsDay() { $this->parse( From b7f45deedec54e047435e0e7bd6aa312b2f24835 Mon Sep 17 00:00:00 2001 From: Phil Davis Date: Thu, 6 Jun 2024 17:17:55 +0545 Subject: [PATCH 19/19] Remove PHP features not supported for 7.1 --- lib/Recur/RRuleIterator.php | 2 +- tests/VObject/Recur/RRuleIteratorTest.php | 24 +++++++++++------------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/lib/Recur/RRuleIterator.php b/lib/Recur/RRuleIterator.php index dd80d9cb..ca53b63e 100644 --- a/lib/Recur/RRuleIterator.php +++ b/lib/Recur/RRuleIterator.php @@ -178,7 +178,7 @@ public function fastForward(DateTimeInterface $dt) * 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 int $hourJump = 0; + protected $hourJump = 0; /** * Frequency is one of: secondly, minutely, hourly, daily, weekly, monthly, diff --git a/tests/VObject/Recur/RRuleIteratorTest.php b/tests/VObject/Recur/RRuleIteratorTest.php index e5dbab14..d148e27d 100644 --- a/tests/VObject/Recur/RRuleIteratorTest.php +++ b/tests/VObject/Recur/RRuleIteratorTest.php @@ -42,7 +42,7 @@ public function test2HourlyOnDstTransition(string $start, array $expected): void $start, $expected, null, - 'Europe/Zurich', + 'Europe/Zurich' ); } @@ -100,7 +100,7 @@ public function testHourlyOnDstTransition(string $start, array $expected): void $start, $expected, null, - 'Europe/Zurich', + 'Europe/Zurich' ); } @@ -291,7 +291,7 @@ public function testDailyOnDstTransition(string $start, array $expected): void $start, $expected, null, - 'Europe/Zurich', + 'Europe/Zurich' ); } @@ -456,7 +456,7 @@ public function testWeeklyByDaySpecificHourOnDstTransition(): void '2023-04-09 02:30:00', ], null, - 'Europe/Zurich', + 'Europe/Zurich' ); } @@ -484,7 +484,7 @@ public function testWeeklyByDayByHourOnDstTransition(): void '2023-04-09 14:00:00', ], null, - 'Europe/Zurich', + 'Europe/Zurich' ); } @@ -498,7 +498,7 @@ public function testWeeklyOnDstTransition(string $start, array $expected): void $start, $expected, null, - 'Europe/Zurich', + 'Europe/Zurich' ); } @@ -618,7 +618,7 @@ public function testMonthlyByMonthDayDstTransition(): void '2023-04-26 02:15:00', ], null, - 'Europe/Zurich', + 'Europe/Zurich' ); } @@ -685,7 +685,7 @@ public function testMonthlyByDayOnDstTransition(): void '2023-05-28 02:30:00', ], null, - 'Europe/Zurich', + 'Europe/Zurich' ); } @@ -750,7 +750,7 @@ public function testMonthlyOnDstTransition(string $start, array $expected): void $start, $expected, null, - 'Europe/Zurich', + 'Europe/Zurich' ); } @@ -875,7 +875,7 @@ public function testYearlyByMonthOnDstTransition(): void '2025-09-26 02:30:00', ], null, - 'Europe/Zurich', + 'Europe/Zurich' ); } @@ -958,7 +958,7 @@ public function testYearlyByMonthByDayOnDstTransition(): void '2025-03-30 03:30:00', ], null, - 'Europe/Zurich', + 'Europe/Zurich' ); } @@ -1159,7 +1159,7 @@ public function testYearlyOnDstTransition(string $start, array $expected): void $start, $expected, null, - 'Europe/Zurich', + 'Europe/Zurich' ); }