@@ -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+ } elseif ($ occurrence [0 ] < $ currentDayOfMonth ) {
451+ continue ;
452+ }
453+ if ($ occurrence [1 ] > $ currentHourOfMonth ) {
454+ break 2 ;
455+ } elseif ($ occurrence [1 ] < $ currentHourOfMonth ) {
456+ continue ;
457+ }
458+ if ($ occurrence [2 ] > $ currentMinuteOfMonth ) {
459+ break 2 ;
460+ } elseif ($ 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,38 @@ 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+ // If the start date is incorrect we must directly jump to the next value
622+ if (in_array ($ currentMonth , $ this ->byMonth )) {
623+ $ occurrences = $ this ->getMonthlyOccurrences ();
624+ foreach ($ occurrences as $ occurrence ) {
625+ // The first occurrence that's higher than the current
626+ // day of the month wins.
627+ // If we advanced to the next month or year, the first
628+ // occurrence is always correct.
629+ if ($ occurrence [0 ] > $ currentDayOfMonth || $ advancedToNewMonth ) {
630+ break 2 ;
631+ } elseif ($ occurrence [0 ] < $ currentDayOfMonth ) {
632+ continue ;
633+ }
634+ if ($ occurrence [1 ] > $ currentHourOfMonth ) {
635+ break 2 ;
636+ } elseif ($ occurrence [1 ] < $ currentHourOfMonth ) {
637+ continue ;
638+ }
639+ if ($ occurrence [2 ] > $ currentMinuteOfMonth ) {
640+ break 2 ;
641+ } elseif ($ occurrence [2 ] < $ currentMinuteOfMonth ) {
642+ continue ;
643+ }
644+ if ($ occurrence [3 ] > $ currentSecondOfMonth ) {
645+ break 2 ;
646+ }
610647 }
611648 }
612649
@@ -633,8 +670,8 @@ protected function nextYearly()
633670 $ this ->currentDate = $ this ->currentDate ->setDate (
634671 (int ) $ currentYear ,
635672 (int ) $ currentMonth ,
636- (int ) $ occurrence
637- );
673+ (int ) $ occurrence[ 0 ]
674+ )-> setTime ( $ occurrence [ 1 ], $ occurrence [ 2 ], $ occurrence [ 3 ]) ;
638675
639676 return ;
640677 } else {
@@ -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,23 @@ 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+
939+ return $ a [3 ] - $ b [3 ];
940+ };
941+ usort ($ result , $ sortLex );
889942
890943 // The last thing that needs checking is the BYSETPOS. If it's set, it
891944 // means only certain items in the set survive the filter.
@@ -903,11 +956,40 @@ protected function getMonthlyOccurrences()
903956 }
904957 }
905958
906- sort ( $ filteredResult , SORT_NUMERIC );
959+ usort ( $ result , $ sortLex );
907960
908961 return $ filteredResult ;
909962 }
910963
964+ /**
965+ * Expends daily occurrences to an array of days that an event occurs on.
966+ *
967+ * @param array $result an array of integers with the day of month (1-31);
968+ *
969+ * @return array an array of arrays with the day of the month, hours, minute and seconds of the occurence
970+ */
971+ protected function addDailyOccurences (array $ result )
972+ {
973+ $ output = [];
974+ $ hour = (int ) $ this ->currentDate ->format ('G ' );
975+ $ minute = (int ) $ this ->currentDate ->format ('i ' );
976+ $ second = (int ) $ this ->currentDate ->format ('s ' );
977+ foreach ($ result as $ day ) {
978+ $ seconds = $ this ->bySecond ? $ this ->bySecond : [$ second ];
979+ $ minutes = $ this ->byMinute ? $ this ->byMinute : [$ minute ];
980+ $ hours = $ this ->byHour ? $ this ->byHour : [$ hour ];
981+ foreach ($ hours as $ h ) {
982+ foreach ($ minutes as $ m ) {
983+ foreach ($ seconds as $ s ) {
984+ $ output [] = [(int ) $ day , (int ) $ h , (int ) $ m , (int ) $ s ];
985+ }
986+ }
987+ }
988+ }
989+
990+ return $ output ;
991+ }
992+
911993 /**
912994 * Simple mapping from iCalendar day names to day numbers.
913995 *
0 commit comments