@@ -420,6 +420,9 @@ protected function nextWeekly()
420420 protected function nextMonthly ()
421421 {
422422 $ currentDayOfMonth = $ this ->currentDate ->format ('j ' );
423+ $ currentHourOfMonth = $ this ->currentDate ->format ('G ' );
424+ $ currentMinuteOfMonth = $ this ->currentDate ->format ('i ' );
425+ $ currentSecondOfMonth = $ this ->currentDate ->format ('s ' );
423426 if (!$ this ->byMonthDay && !$ this ->byDay ) {
424427 // If the current day is higher than the 28th, rollover can
425428 // occur to the next month. We Must skip these invalid
@@ -445,7 +448,23 @@ protected function nextMonthly()
445448 foreach ($ occurrences as $ occurrence ) {
446449 // The first occurrence thats higher than the current
447450 // day of the month wins.
448- if ($ occurrence > $ currentDayOfMonth ) {
451+ if ($ occurrence [0 ] > $ currentDayOfMonth ) {
452+ break 2 ;
453+ } elseif ($ occurrence [0 ] < $ currentDayOfMonth ) {
454+ continue ;
455+ }
456+ if ($ occurrence [1 ] > $ currentHourOfMonth ) {
457+ break 2 ;
458+ } elseif ($ occurrence [1 ] < $ currentHourOfMonth ) {
459+ continue ;
460+ }
461+
462+ if ($ occurrence [2 ] > $ currentMinuteOfMonth ) {
463+ break 2 ;
464+ } elseif ($ occurrence [2 ] < $ currentMinuteOfMonth ) {
465+ continue ;
466+ }
467+ if ($ occurrence [3 ] > $ currentSecondOfMonth ) {
449468 break 2 ;
450469 }
451470 }
@@ -464,6 +483,9 @@ protected function nextMonthly()
464483 // This goes to 0 because we need to start counting at the
465484 // beginning.
466485 $ currentDayOfMonth = 0 ;
486+ $ currentHourOfMonth = 0 ;
487+ $ currentMinuteOfMonth = 0 ;
488+ $ currentSecondOfMonth = 0 ;
467489
468490 // To prevent running this forever (better: until we hit the max date of DateTimeImmutable) we simply
469491 // stop at 9999-12-31. Looks like the year 10000 problem is not solved in php ....
@@ -477,8 +499,8 @@ protected function nextMonthly()
477499 $ this ->currentDate = $ this ->currentDate ->setDate (
478500 (int ) $ this ->currentDate ->format ('Y ' ),
479501 (int ) $ this ->currentDate ->format ('n ' ),
480- ( int ) $ occurrence
481- );
502+ $ occurrence[ 0 ]
503+ )-> setTime ( $ occurrence [ 1 ], $ occurrence [ 2 ], $ occurrence [ 3 ]) ;
482504 }
483505
484506 /**
@@ -489,6 +511,9 @@ protected function nextYearly()
489511 $ currentMonth = $ this ->currentDate ->format ('n ' );
490512 $ currentYear = $ this ->currentDate ->format ('Y ' );
491513 $ currentDayOfMonth = $ this ->currentDate ->format ('j ' );
514+ $ currentHourOfMonth = $ this ->currentDate ->format ('G ' );
515+ $ currentMinuteOfMonth = $ this ->currentDate ->format ('i ' );
516+ $ currentSecondOfMonth = $ this ->currentDate ->format ('s ' );
492517
493518 // No sub-rules, so we just advance by year
494519 if (empty ($ this ->byMonth )) {
@@ -599,25 +624,38 @@ protected function nextYearly()
599624 return ;
600625 }
601626
602- $ currentMonth = $ this ->currentDate ->format ('n ' );
603- $ currentYear = $ this ->currentDate ->format ('Y ' );
604- $ currentDayOfMonth = $ this ->currentDate ->format ('j ' );
605-
606627 $ advancedToNewMonth = false ;
607628
608629 // If we got a byDay or getMonthDay filter, we must first expand
609630 // further.
610631 if ($ this ->byDay || $ this ->byMonthDay ) {
611632 while (true ) {
612- $ occurrences = $ this ->getMonthlyOccurrences ();
613-
614- foreach ($ occurrences as $ occurrence ) {
615- // The first occurrence that's higher than the current
616- // day of the month wins.
617- // If we advanced to the next month or year, the first
618- // occurrence is always correct.
619- if ($ occurrence > $ currentDayOfMonth || $ advancedToNewMonth ) {
620- break 2 ;
633+ // If the start date is incorrect we must directly jump to the next value
634+ if (in_array ($ currentMonth , $ this ->byMonth )) {
635+ $ occurrences = $ this ->getMonthlyOccurrences ();
636+ foreach ($ occurrences as $ occurrence ) {
637+ // The first occurrence that's higher than the current
638+ // day of the month wins.
639+ // If we advanced to the next month or year, the first
640+ // occurrence is always correct.
641+ if ($ occurrence [0 ] > $ currentDayOfMonth || $ advancedToNewMonth ) {
642+ break 2 ;
643+ } elseif ($ occurrence [0 ] < $ currentDayOfMonth ) {
644+ continue ;
645+ }
646+ if ($ occurrence [1 ] > $ currentHourOfMonth ) {
647+ break 2 ;
648+ } elseif ($ occurrence [1 ] < $ currentHourOfMonth ) {
649+ continue ;
650+ }
651+ if ($ occurrence [2 ] > $ currentMinuteOfMonth ) {
652+ break 2 ;
653+ } elseif ($ occurrence [2 ] < $ currentMinuteOfMonth ) {
654+ continue ;
655+ }
656+ if ($ occurrence [3 ] > $ currentSecondOfMonth ) {
657+ break 2 ;
658+ }
621659 }
622660 }
623661
@@ -644,8 +682,8 @@ protected function nextYearly()
644682 $ this ->currentDate = $ this ->currentDate ->setDate (
645683 (int ) $ currentYear ,
646684 (int ) $ currentMonth ,
647- (int ) $ occurrence
648- );
685+ (int ) $ occurrence[ 0 ]
686+ )-> setTime ( $ occurrence [ 1 ], $ occurrence [ 2 ], $ occurrence [ 3 ]) ;
649687
650688 return ;
651689 } else {
@@ -809,7 +847,8 @@ protected function parseRRule($rrule)
809847 * Returns all the occurrences for a monthly frequency with a 'byDay' or
810848 * 'byMonthDay' expansion for the current month.
811849 *
812- * The returned list is an array of integers with the day of month (1-31).
850+ * The returned list is an array of arrays with as first element the day of month (1-31);
851+ * the hour; the minute and second of the occurence
813852 *
814853 * @return array
815854 */
@@ -895,8 +934,23 @@ protected function getMonthlyOccurrences()
895934 } else {
896935 $ result = $ byDayResults ;
897936 }
898- $ result = array_unique ($ result );
899- sort ($ result , SORT_NUMERIC );
937+
938+ $ result = $ this ->addDailyOccurences ($ result );
939+ $ result = array_unique ($ result , SORT_REGULAR );
940+ $ sortLex = function ($ a , $ b ) {
941+ if ($ a [0 ] != $ b [0 ]) {
942+ return $ a [0 ] - $ b [0 ];
943+ }
944+ if ($ a [1 ] != $ b [1 ]) {
945+ return $ a [1 ] - $ b [1 ];
946+ }
947+ if ($ a [2 ] != $ b [2 ]) {
948+ return $ a [2 ] - $ b [2 ];
949+ }
950+
951+ return $ a [3 ] - $ b [3 ];
952+ };
953+ usort ($ result , $ sortLex );
900954
901955 // The last thing that needs checking is the BYSETPOS. If it's set, it
902956 // means only certain items in the set survive the filter.
@@ -914,11 +968,40 @@ protected function getMonthlyOccurrences()
914968 }
915969 }
916970
917- sort ( $ filteredResult , SORT_NUMERIC );
971+ usort ( $ result , $ sortLex );
918972
919973 return $ filteredResult ;
920974 }
921975
976+ /**
977+ * Expends daily occurrences to an array of days that an event occurs on.
978+ *
979+ * @param array $result an array of integers with the day of month (1-31);
980+ *
981+ * @return array an array of arrays with the day of the month, hours, minute and seconds of the occurence
982+ */
983+ protected function addDailyOccurences (array $ result )
984+ {
985+ $ output = [];
986+ $ hour = (int ) $ this ->currentDate ->format ('G ' );
987+ $ minute = (int ) $ this ->currentDate ->format ('i ' );
988+ $ second = (int ) $ this ->currentDate ->format ('s ' );
989+ foreach ($ result as $ day ) {
990+ $ seconds = $ this ->bySecond ? $ this ->bySecond : [$ second ];
991+ $ minutes = $ this ->byMinute ? $ this ->byMinute : [$ minute ];
992+ $ hours = $ this ->byHour ? $ this ->byHour : [$ hour ];
993+ foreach ($ hours as $ h ) {
994+ foreach ($ minutes as $ m ) {
995+ foreach ($ seconds as $ s ) {
996+ $ output [] = [(int ) $ day , (int ) $ h , (int ) $ m , (int ) $ s ];
997+ }
998+ }
999+ }
1000+ }
1001+
1002+ return $ output ;
1003+ }
1004+
9221005 /**
9231006 * Simple mapping from iCalendar day names to day numbers.
9241007 *
0 commit comments