@@ -159,6 +159,14 @@ public function fastForward(\DateTimeInterface $dt): void
159
159
*/
160
160
protected ?\DateTimeInterface $ currentDate ;
161
161
162
+ /**
163
+ * The number of hours that the next occurrence of an event
164
+ * jumped forward, usually because summer time started and
165
+ * the requested time-of-day like 0230 did not exist on that
166
+ * day. And so the event was scheduled 1 hour later at 0330.
167
+ */
168
+ protected int $ hourJump = 0 ;
169
+
162
170
/**
163
171
* Frequency is one of: secondly, minutely, hourly, daily, weekly, monthly,
164
172
* yearly.
@@ -276,12 +284,65 @@ public function fastForward(\DateTimeInterface $dt): void
276
284
277
285
/* Functions that advance the iterator {{{ */
278
286
287
+ /**
288
+ * Gets the original start time of the RRULE.
289
+ *
290
+ * The value is formatted as a string with 24-hour:minute:second
291
+ */
292
+ protected function startTime (): string
293
+ {
294
+ return $ this ->startDate ->format ('H:i:s ' );
295
+ }
296
+
297
+ /**
298
+ * Advances currentDate by the interval.
299
+ * The time is set from the original startDate.
300
+ * If the recurrence is on a day when summer time started, then the
301
+ * time on that day may have jumped forward, for example, from 0230 to 0330.
302
+ * Using the original time means that the next recurrence will be calculated
303
+ * based on the original start time and the day/week/month/year interval.
304
+ * So the start time of the next occurrence can correctly revert to 0230.
305
+ */
306
+ protected function advanceTheDate (string $ interval ): void
307
+ {
308
+ $ this ->currentDate = $ this ->currentDate ->modify ($ interval .' ' .$ this ->startTime ());
309
+ }
310
+
311
+ /**
312
+ * Does the processing for adjusting the time of multi-hourly events when summer time starts.
313
+ */
314
+ protected function adjustForTimeJumpsOfHourlyEvent (\DateTimeInterface $ previousEventDateTime ): void
315
+ {
316
+ if (0 === $ this ->hourJump ) {
317
+ // Remember if the clock time jumped forward on the next occurrence.
318
+ // That happens if the next event time is on a day when summer time starts
319
+ // and the event time is in the non-existent hour of the day.
320
+ // For example, an event that normally starts at 02:30 will
321
+ // have to start at 03:30 on that day.
322
+ // If the interval is just 1 hour, then there is no "jumping back" to do.
323
+ // The events that day will happen, for example, at 0030 0130 0330 0430 0530...
324
+ if ($ this ->interval > 1 ) {
325
+ $ expectedHourOfNextDate = ((int ) $ previousEventDateTime ->format ('G ' ) + $ this ->interval ) % 24 ;
326
+ $ actualHourOfNextDate = (int ) $ this ->currentDate ->format ('G ' );
327
+ $ this ->hourJump = $ actualHourOfNextDate - $ expectedHourOfNextDate ;
328
+ }
329
+ } else {
330
+ // The hour "jumped" for the previous occurrence, to avoid the non-existent time.
331
+ // currentDate got set ahead by (usually) 1 hour on that day.
332
+ // Adjust it back for this next occurrence.
333
+ $ this ->currentDate = $ this ->currentDate ->sub (new \DateInterval ('PT ' .$ this ->hourJump .'H ' ));
334
+ $ this ->hourJump = 0 ;
335
+ }
336
+ }
337
+
279
338
/**
280
339
* Does the processing for advancing the iterator for hourly frequency.
281
340
*/
282
341
protected function nextHourly (): void
283
342
{
343
+ $ previousEventDateTime = clone $ this ->currentDate ;
284
344
$ this ->currentDate = $ this ->currentDate ->modify ('+ ' .$ this ->interval .' hours ' );
345
+ $ this ->adjustForTimeJumpsOfHourlyEvent ($ previousEventDateTime );
285
346
}
286
347
287
348
/**
@@ -290,7 +351,7 @@ protected function nextHourly(): void
290
351
protected function nextDaily (): void
291
352
{
292
353
if (!$ this ->byHour && !$ this ->byDay ) {
293
- $ this ->currentDate = $ this -> currentDate -> modify ('+ ' .$ this ->interval .' days ' );
354
+ $ this ->advanceTheDate ('+ ' .$ this ->interval .' days ' );
294
355
295
356
return ;
296
357
}
@@ -349,7 +410,7 @@ protected function nextDaily(): void
349
410
protected function nextWeekly (): void
350
411
{
351
412
if (!$ this ->byHour && !$ this ->byDay ) {
352
- $ this ->currentDate = $ this -> currentDate -> modify ('+ ' .$ this ->interval .' weeks ' );
413
+ $ this ->advanceTheDate ('+ ' .$ this ->interval .' weeks ' );
353
414
354
415
return ;
355
416
}
@@ -371,7 +432,7 @@ protected function nextWeekly(): void
371
432
if ($ this ->byHour ) {
372
433
$ this ->currentDate = $ this ->currentDate ->modify ('+1 hours ' );
373
434
} else {
374
- $ this ->currentDate = $ this -> currentDate -> modify ('+1 days ' );
435
+ $ this ->advanceTheDate ('+1 days ' );
375
436
}
376
437
377
438
// Current day of the week
@@ -408,13 +469,13 @@ protected function nextMonthly(): void
408
469
// occur to the next month. We Must skip these invalid
409
470
// entries.
410
471
if ($ currentDayOfMonth < 29 ) {
411
- $ this ->currentDate = $ this -> currentDate -> modify ('+ ' .$ this ->interval .' months ' );
472
+ $ this ->advanceTheDate ('+ ' .$ this ->interval .' months ' );
412
473
} else {
413
474
$ increase = 0 ;
414
475
do {
415
476
++$ increase ;
416
477
$ tempDate = clone $ this ->currentDate ;
417
- $ tempDate = $ tempDate ->modify ('+ ' .($ this ->interval * $ increase ).' months ' );
478
+ $ tempDate = $ tempDate ->modify ('+ ' .($ this ->interval * $ increase ).' months ' . $ this -> startTime () );
418
479
} while ($ tempDate ->format ('j ' ) != $ currentDayOfMonth );
419
480
$ this ->currentDate = $ tempDate ;
420
481
}
@@ -465,11 +526,15 @@ protected function nextMonthly(): void
465
526
}
466
527
}
467
528
529
+ // Set the currentDate to the year and month that we are in, and the day of the month that we have selected.
530
+ // That day could be a day when summer time starts, and if the time of the event is, for example, 0230,
531
+ // then 0230 will not be a valid time on that day. So always apply the start time from the original startDate.
532
+ // The "modify" method will set the time forward to 0330, for example, if needed.
468
533
$ this ->currentDate = $ this ->currentDate ->setDate (
469
534
(int ) $ this ->currentDate ->format ('Y ' ),
470
535
(int ) $ this ->currentDate ->format ('n ' ),
471
536
(int ) $ occurrence
472
- );
537
+ )-> modify ( $ this -> startTime ()) ;
473
538
}
474
539
475
540
/**
@@ -586,7 +651,7 @@ protected function nextYearly(): void
586
651
}
587
652
588
653
// The easiest form
589
- $ this ->currentDate = $ this -> currentDate -> modify ('+ ' .$ this ->interval .' years ' );
654
+ $ this ->advanceTheDate ('+ ' .$ this ->interval .' years ' );
590
655
591
656
return ;
592
657
}
@@ -650,7 +715,7 @@ protected function nextYearly(): void
650
715
(int ) $ currentYear ,
651
716
(int ) $ currentMonth ,
652
717
(int ) $ occurrence
653
- );
718
+ )-> modify ( $ this -> startTime ()) ;
654
719
655
720
return ;
656
721
} else {
@@ -667,7 +732,7 @@ protected function nextYearly(): void
667
732
(int ) $ currentYear ,
668
733
(int ) $ currentMonth ,
669
734
(int ) $ currentDayOfMonth
670
- );
735
+ )-> modify ( $ this -> startTime ()) ;
671
736
672
737
return ;
673
738
}
0 commit comments