@@ -417,6 +417,9 @@ protected function nextWeekly()
417417 protected function nextMonthly ()
418418 {
419419 $ currentDayOfMonth = $ this ->currentDate ->format ('j ' );
420+ $ currentHourOfMonth = $ this ->currentDate ->format ('G ' );
421+ $ currentMinuteOfMonth = $ this ->currentDate ->format ('i ' );
422+ $ currentSecondOfMonth = $ this ->currentDate ->format ('s ' );
420423 if (!$ this ->byMonthDay && !$ this ->byDay ) {
421424 // If the current day is higher than the 28th, rollover can
422425 // occur to the next month. We Must skip these invalid
@@ -442,7 +445,23 @@ protected function nextMonthly()
442445 foreach ($ occurrences as $ occurrence ) {
443446 // The first occurrence thats higher than the current
444447 // day of the month wins.
445- if ($ occurrence > $ currentDayOfMonth ) {
448+ if ($ occurrence [0 ] > $ currentDayOfMonth ) {
449+ break 2 ;
450+ } elseif ($ occurrence [0 ] < $ currentDayOfMonth ) {
451+ continue ;
452+ }
453+ if ($ occurrence [1 ] > $ currentHourOfMonth ) {
454+ break 2 ;
455+ } elseif ($ occurrence [1 ] < $ currentHourOfMonth ) {
456+ continue ;
457+ }
458+
459+ if ($ occurrence [2 ] > $ currentMinuteOfMonth ) {
460+ break 2 ;
461+ } elseif ($ occurrence [2 ] < $ currentMinuteOfMonth ) {
462+ continue ;
463+ }
464+ if ($ occurrence [3 ] > $ currentSecondOfMonth ) {
446465 break 2 ;
447466 }
448467 }
@@ -461,13 +480,16 @@ protected function nextMonthly()
461480 // This goes to 0 because we need to start counting at the
462481 // beginning.
463482 $ currentDayOfMonth = 0 ;
483+ $ currentHourOfMonth = 0 ;
484+ $ currentMinuteOfMonth = 0 ;
485+ $ currentSecondOfMonth = 0 ;
464486 }
465487
466488 $ this ->currentDate = $ this ->currentDate ->setDate (
467489 (int ) $ this ->currentDate ->format ('Y ' ),
468490 (int ) $ this ->currentDate ->format ('n ' ),
469- ( int ) $ occurrence
470- );
491+ $ occurrence[ 0 ]
492+ )-> setTime ( $ occurrence [ 1 ], $ occurrence [ 2 ], $ occurrence [ 3 ]) ;
471493 }
472494
473495 /**
@@ -478,6 +500,9 @@ protected function nextYearly()
478500 $ currentMonth = $ this ->currentDate ->format ('n ' );
479501 $ currentYear = $ this ->currentDate ->format ('Y ' );
480502 $ currentDayOfMonth = $ this ->currentDate ->format ('j ' );
503+ $ currentHourOfMonth = $ this ->currentDate ->format ('G ' );
504+ $ currentMinuteOfMonth = $ this ->currentDate ->format ('i ' );
505+ $ currentSecondOfMonth = $ this ->currentDate ->format ('s ' );
481506
482507 // No sub-rules, so we just advance by year
483508 if (empty ($ this ->byMonth )) {
@@ -588,25 +613,38 @@ protected function nextYearly()
588613 return ;
589614 }
590615
591- $ currentMonth = $ this ->currentDate ->format ('n ' );
592- $ currentYear = $ this ->currentDate ->format ('Y ' );
593- $ currentDayOfMonth = $ this ->currentDate ->format ('j ' );
594-
595616 $ advancedToNewMonth = false ;
596617
597618 // If we got a byDay or getMonthDay filter, we must first expand
598619 // further.
599620 if ($ this ->byDay || $ this ->byMonthDay ) {
600621 while (true ) {
601- $ occurrences = $ this ->getMonthlyOccurrences ();
602-
603- foreach ($ occurrences as $ occurrence ) {
604- // The first occurrence that's higher than the current
605- // day of the month wins.
606- // If we advanced to the next month or year, the first
607- // occurrence is always correct.
608- if ($ occurrence > $ currentDayOfMonth || $ advancedToNewMonth ) {
609- break 2 ;
622+ // If the start date is incorrect we must directly jump to the next value
623+ if (in_array ($ currentMonth , $ this ->byMonth )) {
624+ $ occurrences = $ this ->getMonthlyOccurrences ();
625+ foreach ($ occurrences as $ occurrence ) {
626+ // The first occurrence that's higher than the current
627+ // day of the month wins.
628+ // If we advanced to the next month or year, the first
629+ // occurrence is always correct.
630+ if ($ occurrence [0 ] > $ currentDayOfMonth || $ advancedToNewMonth ) {
631+ break 2 ;
632+ } elseif ($ occurrence [0 ] < $ currentDayOfMonth ) {
633+ continue ;
634+ }
635+ if ($ occurrence [1 ] > $ currentHourOfMonth ) {
636+ break 2 ;
637+ } elseif ($ occurrence [1 ] < $ currentHourOfMonth ) {
638+ continue ;
639+ }
640+ if ($ occurrence [2 ] > $ currentMinuteOfMonth ) {
641+ break 2 ;
642+ } elseif ($ occurrence [2 ] < $ currentMinuteOfMonth ) {
643+ continue ;
644+ }
645+ if ($ occurrence [3 ] > $ currentSecondOfMonth ) {
646+ break 2 ;
647+ }
610648 }
611649 }
612650
@@ -633,8 +671,8 @@ protected function nextYearly()
633671 $ this ->currentDate = $ this ->currentDate ->setDate (
634672 (int ) $ currentYear ,
635673 (int ) $ currentMonth ,
636- (int ) $ occurrence
637- );
674+ (int ) $ occurrence[ 0 ]
675+ )-> setTime ( $ occurrence [ 1 ], $ occurrence [ 2 ], $ occurrence [ 3 ]) ;
638676
639677 return ;
640678 } else {
@@ -798,7 +836,8 @@ protected function parseRRule($rrule)
798836 * Returns all the occurrences for a monthly frequency with a 'byDay' or
799837 * 'byMonthDay' expansion for the current month.
800838 *
801- * The returned list is an array of integers with the day of month (1-31).
839+ * The returned list is an array of arrays with as first element the day of month (1-31);
840+ * the hour; the minute and second of the occurence
802841 *
803842 * @return array
804843 */
@@ -884,8 +923,23 @@ protected function getMonthlyOccurrences()
884923 } else {
885924 $ result = $ byDayResults ;
886925 }
887- $ result = array_unique ($ result );
888- sort ($ result , SORT_NUMERIC );
926+
927+ $ result = $ this ->addDailyOccurences ($ result );
928+ $ result = array_unique ($ result , SORT_REGULAR );
929+ $ sortLex = function ($ a , $ b ) {
930+ if ($ a [0 ] != $ b [0 ]) {
931+ return $ a [0 ] - $ b [0 ];
932+ }
933+ if ($ a [1 ] != $ b [1 ]) {
934+ return $ a [1 ] - $ b [1 ];
935+ }
936+ if ($ a [2 ] != $ b [2 ]) {
937+ return $ a [2 ] - $ b [2 ];
938+ }
939+
940+ return $ a [3 ] - $ b [3 ];
941+ };
942+ usort ($ result , $ sortLex );
889943
890944 // The last thing that needs checking is the BYSETPOS. If it's set, it
891945 // means only certain items in the set survive the filter.
@@ -903,11 +957,40 @@ protected function getMonthlyOccurrences()
903957 }
904958 }
905959
906- sort ( $ filteredResult , SORT_NUMERIC );
960+ usort ( $ result , $ sortLex );
907961
908962 return $ filteredResult ;
909963 }
910964
965+ /**
966+ * Expends daily occurrences to an array of days that an event occurs on.
967+ *
968+ * @param array $result an array of integers with the day of month (1-31);
969+ *
970+ * @return array an array of arrays with the day of the month, hours, minute and seconds of the occurence
971+ */
972+ protected function addDailyOccurences (array $ result )
973+ {
974+ $ output = [];
975+ $ hour = (int ) $ this ->currentDate ->format ('G ' );
976+ $ minute = (int ) $ this ->currentDate ->format ('i ' );
977+ $ second = (int ) $ this ->currentDate ->format ('s ' );
978+ foreach ($ result as $ day ) {
979+ $ seconds = $ this ->bySecond ? $ this ->bySecond : [$ second ];
980+ $ minutes = $ this ->byMinute ? $ this ->byMinute : [$ minute ];
981+ $ hours = $ this ->byHour ? $ this ->byHour : [$ hour ];
982+ foreach ($ hours as $ h ) {
983+ foreach ($ minutes as $ m ) {
984+ foreach ($ seconds as $ s ) {
985+ $ output [] = [(int ) $ day , (int ) $ h , (int ) $ m , (int ) $ s ];
986+ }
987+ }
988+ }
989+ }
990+
991+ return $ output ;
992+ }
993+
911994 /**
912995 * Simple mapping from iCalendar day names to day numbers.
913996 *
0 commit comments