@@ -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,22 @@ 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+ } else if ($ occurrence [0 ] < $ currentDayOfMonth ) {
451+ continue ;
452+ }
453+ if ($ occurrence [1 ] > $ currentHourOfMonth ) {
454+ break 2 ;
455+ } else if ($ occurrence [1 ] < $ currentHourOfMonth ) {
456+ continue ;
457+ }
458+ if ($ occurrence [2 ] > $ currentMinuteOfMonth ) {
459+ break 2 ;
460+ } else if ($ occurrence [2 ] < $ currentMinuteOfMonth ) {
461+ continue ;
462+ }
463+ if ($ occurrence [3 ] > $ currentSecondOfMonth ) {
446464 break 2 ;
447465 }
448466 }
@@ -461,13 +479,16 @@ protected function nextMonthly()
461479 // This goes to 0 because we need to start counting at the
462480 // beginning.
463481 $ currentDayOfMonth = 0 ;
482+ $ currentHourOfMonth = 0 ;
483+ $ currentMinuteOfMonth = 0 ;
484+ $ currentSecondOfMonth = 0 ;
464485 }
465486
466487 $ this ->currentDate = $ this ->currentDate ->setDate (
467488 (int ) $ this ->currentDate ->format ('Y ' ),
468489 (int ) $ this ->currentDate ->format ('n ' ),
469- ( int ) $ occurrence
470- );
490+ $ occurrence[ 0 ]
491+ )-> setTime ( $ occurrence [ 1 ], $ occurrence [ 2 ], $ occurrence [ 3 ]) ;
471492 }
472493
473494 /**
@@ -478,6 +499,9 @@ protected function nextYearly()
478499 $ currentMonth = $ this ->currentDate ->format ('n ' );
479500 $ currentYear = $ this ->currentDate ->format ('Y ' );
480501 $ currentDayOfMonth = $ this ->currentDate ->format ('j ' );
502+ $ currentHourOfMonth = $ this ->currentDate ->format ('G ' );
503+ $ currentMinuteOfMonth = $ this ->currentDate ->format ('i ' );
504+ $ currentSecondOfMonth = $ this ->currentDate ->format ('s ' );
481505
482506 // No sub-rules, so we just advance by year
483507 if (empty ($ this ->byMonth )) {
@@ -588,25 +612,39 @@ protected function nextYearly()
588612 return ;
589613 }
590614
591- $ currentMonth = $ this ->currentDate ->format ('n ' );
592- $ currentYear = $ this ->currentDate ->format ('Y ' );
593- $ currentDayOfMonth = $ this ->currentDate ->format ('j ' );
594-
595615 $ advancedToNewMonth = false ;
596616
597617 // If we got a byDay or getMonthDay filter, we must first expand
598618 // further.
599619 if ($ this ->byDay || $ this ->byMonthDay ) {
600620 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 ;
621+
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+ } else if ($ occurrence [0 ] < $ currentDayOfMonth ) {
633+ continue ;
634+ }
635+ if ($ occurrence [1 ] > $ currentHourOfMonth ) {
636+ break 2 ;
637+ } else if ($ occurrence [1 ] < $ currentHourOfMonth ) {
638+ continue ;
639+ }
640+ if ($ occurrence [2 ] > $ currentMinuteOfMonth ) {
641+ break 2 ;
642+ } else if ($ occurrence [2 ] < $ currentMinuteOfMonth ) {
643+ continue ;
644+ }
645+ if ($ occurrence [3 ] > $ currentSecondOfMonth ) {
646+ break 2 ;
647+ }
610648 }
611649 }
612650
@@ -633,9 +671,8 @@ protected function nextYearly()
633671 $ this ->currentDate = $ this ->currentDate ->setDate (
634672 (int ) $ currentYear ,
635673 (int ) $ currentMonth ,
636- (int ) $ occurrence
637- );
638-
674+ (int ) $ occurrence [0 ]
675+ )->setTime ($ occurrence [1 ], $ occurrence [2 ], $ occurrence [3 ]);
639676 return ;
640677 } else {
641678 // These are the 'byMonth' rules, if there are no byDay or
@@ -798,7 +835,8 @@ protected function parseRRule($rrule)
798835 * Returns all the occurrences for a monthly frequency with a 'byDay' or
799836 * 'byMonthDay' expansion for the current month.
800837 *
801- * The returned list is an array of integers with the day of month (1-31).
838+ * The returned list is an array of arrays with as first element the day of month (1-31);
839+ * the hour; the minute and second of the occurence
802840 *
803841 * @return array
804842 */
@@ -884,8 +922,22 @@ protected function getMonthlyOccurrences()
884922 } else {
885923 $ result = $ byDayResults ;
886924 }
887- $ result = array_unique ($ result );
888- sort ($ result , SORT_NUMERIC );
925+
926+ $ result = $ this ->addDailyOccurences ($ result );
927+ $ result = array_unique ($ result , SORT_REGULAR );
928+ $ sortLex = function ($ a , $ b ) {
929+ if ($ a [0 ] != $ b [0 ]) {
930+ return $ a [0 ] - $ b [0 ];
931+ }
932+ if ($ a [1 ] != $ b [1 ]) {
933+ return $ a [1 ] - $ b [1 ];
934+ }
935+ if ($ a [2 ] != $ b [2 ]) {
936+ return $ a [2 ] - $ b [2 ];
937+ }
938+ return $ a [3 ] - $ b [3 ];
939+ };
940+ usort ($ result , $ sortLex );
889941
890942 // The last thing that needs checking is the BYSETPOS. If it's set, it
891943 // means only certain items in the set survive the filter.
@@ -903,11 +955,37 @@ protected function getMonthlyOccurrences()
903955 }
904956 }
905957
906- sort ( $ filteredResult , SORT_NUMERIC );
958+ usort ( $ result , $ sortLex );
907959
908960 return $ filteredResult ;
909961 }
910962
963+ /**
964+ * Expends daily occurrences to an array of days that an event occurs on
965+ * @param array $result an array of integers with the day of month (1-31);
966+ * @return array an array of arrays with the day of the month, hours, minute and seconds of the occurence
967+ */
968+ protected function addDailyOccurences (array $ result ) {
969+ $ output = [];
970+ $ hour = (int ) $ this ->currentDate ->format ('G ' );
971+ $ minute = (int ) $ this ->currentDate ->format ('i ' );
972+ $ second = (int ) $ this ->currentDate ->format ('s ' );
973+ foreach ($ result as $ day )
974+ {
975+ $ seconds = $ this ->bySecond ? $ this ->bySecond : [ $ second ];
976+ $ minutes = $ this ->byMinute ? $ this ->byMinute : [ $ minute ];
977+ $ hours = $ this ->byHour ? $ this ->byHour : [ $ hour ];
978+ foreach ($ hours as $ h ) {
979+ foreach ($ minutes as $ m ) {
980+ foreach ($ seconds as $ s ) {
981+ $ output [] = [(int ) $ day , (int ) $ h , (int ) $ m , (int ) $ s ];
982+ }
983+ }
984+ }
985+ }
986+ return $ output ;
987+ }
988+
911989 /**
912990 * Simple mapping from iCalendar day names to day numbers.
913991 *
0 commit comments