Skip to content

Commit db88a8b

Browse files
committed
Add workaround for Carbon 3 in occursAt
This should fix #164 until a proper fix in Carbon itself
1 parent ece03d9 commit db88a8b

File tree

4 files changed

+51
-8
lines changed

4 files changed

+51
-8
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
- Drop support for PHP < 7.3 [#119](https://github.com/rlanvin/php-rrule/issues/119)
88
- Add support for PHP 8.4
99

10+
### Fixed
11+
12+
- Added a workaround for a Carbon 3 bug that makes occursAt fail in some cases [#164](https://github.com/rlanvin/php-rrule/issues/164)
13+
1014
## [2.6.0] - 2025-04-25
1115

1216
### Added

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
},
2424
"require-dev": {
2525
"phpunit/phpunit": "^9.0",
26-
"phpmd/phpmd" : "@stable"
26+
"phpmd/phpmd" : "@stable",
27+
"nesbot/carbon": "*"
2728
}
2829
}

src/RRule.php

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,30 @@ function is_leap_year($year)
7171
return true;
7272
}
7373

74+
/**
75+
* Carbon 3 specific workaround - shouldn't be necessary but here we are.
76+
* Original bug: https://github.com/briannesbitt/Carbon/issues/3018
77+
* Workaround could be removed if the bug is fixed
78+
*
79+
* @see https://github.com/rlanvin/php-rrule/issues/164
80+
* @return int
81+
*/
82+
function date_interval_days(\DateInterval $interval): int
83+
{
84+
if ($interval->days !== false) {
85+
return $interval->days;
86+
}
87+
88+
// if days is false, we might be dealing with Carbon.
89+
// we try to get it from format() instead, and cast it to int
90+
$days = $interval->format('%a');
91+
if ($days !== '(unknown)' && is_numeric($days)) {
92+
return (int) $days;
93+
}
94+
95+
throw new \RuntimeException("Unable to get days from DateInterval. This shouldn't happen. If you are using a custom date library, trying passing a normal \DateTime.");
96+
}
97+
7498
/**
7599
* Implementation of RRULE as defined by RFC 5545 (iCalendar).
76100
* Heavily based on python-dateutil/rrule
@@ -847,37 +871,37 @@ public function occursAt($date)
847871
// count nb of days and divide by 7 to get number of weeks
848872
// we add some days to align dtstart with wkst
849873
$diff = $date->diff($this->dtstart);
850-
$diff = (int) (($diff->days + pymod($this->dtstart->format('N') - $this->wkst,7)) / 7);
874+
$diff = (int) ((date_interval_days($diff) + pymod($this->dtstart->format('N') - $this->wkst,7)) / 7);
851875
if ($diff % $this->interval !== 0) {
852876
return false;
853877
}
854878
break;
855879
case self::DAILY:
856880
// count nb of days
857881
$diff = $date->diff($this->dtstart);
858-
if ($diff->days % $this->interval !== 0) {
882+
if (date_interval_days($diff) % $this->interval !== 0) {
859883
return false;
860884
}
861885
break;
862886
// XXX: I'm not sure the 3 formulas below take the DST into account...
863887
case self::HOURLY:
864888
$diff = $date->diff($this->dtstart);
865-
$diff = $diff->h + $diff->days * 24;
889+
$diff = $diff->h + date_interval_days($diff) * 24;
866890
if ($diff % $this->interval !== 0) {
867891
return false;
868892
}
869893
break;
870894
case self::MINUTELY:
871895
$diff = $date->diff($this->dtstart);
872-
$diff = $diff->i + $diff->h * 60 + $diff->days * 1440;
896+
$diff = $diff->i + $diff->h * 60 + date_interval_days($diff) * 1440;
873897
if ($diff % $this->interval !== 0) {
874898
return false;
875899
}
876900
break;
877901
case self::SECONDLY:
878902
$diff = $date->diff($this->dtstart);
879903
// XXX does not account for leap second (should it?)
880-
$diff = $diff->s + $diff->i * 60 + $diff->h * 3600 + $diff->days * 86400;
904+
$diff = $diff->s + $diff->i * 60 + $diff->h * 3600 + date_interval_days($diff) * 86400;
881905
if ($diff % $this->interval !== 0) {
882906
return false;
883907
}

tests/RRuleTest.php

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1786,6 +1786,11 @@ public function notOccurrences()
17861786
array('freq' => 'secondly', 'dtstart' => '1999-09-02 09:00:00', 'INTERVAL' => 5),
17871787
array('1999-09-02 09:00:01')
17881788
),
1789+
// https://github.com/rlanvin/php-rrule/issues/164
1790+
'issue164' => [
1791+
"DTSTART:20250505T000000Z\nRRULE:FREQ=WEEKLY;UNTIL=20250520T000000Z;INTERVAL=2;BYDAY=MO",
1792+
['2025-05-12']
1793+
]
17891794
);
17901795
}
17911796

@@ -1800,6 +1805,17 @@ public function testNotOccurrences($rule, $not_occurrences)
18001805
}
18011806
}
18021807

1808+
/**
1809+
* @dataProvider notOccurrences
1810+
*/
1811+
public function testNotOccurrencesWithCarbon($rule, $not_occurrences)
1812+
{
1813+
$rule = new RRule($rule);
1814+
foreach ($not_occurrences as $date) {
1815+
$this->assertFalse($rule->occursAt(new \Carbon\Carbon($date)), "Rule must not match $date");
1816+
}
1817+
}
1818+
18031819
public function rulesBeyondMaxCycles()
18041820
{
18051821
return [
@@ -2217,8 +2233,6 @@ public function testRfcStringParser($str, $occurrences)
22172233
}
22182234
}
22192235

2220-
2221-
22222236
public function testRfcStringParserWithDtStart()
22232237
{
22242238
$rrule = new RRule('RRULE:FREQ=YEARLY');

0 commit comments

Comments
 (0)