Skip to content

Commit 7daf7f7

Browse files
author
CipherdevNL
committed
fix: skipped run after daylight saving turnover point
1 parent f37e405 commit 7daf7f7

File tree

2 files changed

+119
-14
lines changed

2 files changed

+119
-14
lines changed

src/Cron/HoursField.php

+6
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ public function increment(DateTimeInterface &$date, $invert = false, $parts = nu
160160

161161
$target = (int) $hours[$position];
162162
$originalHour = (int)$date->format('H');
163+
$originalDst = (int)$date->format('I');
163164

164165
$originalDay = (int)$date->format('d');
165166
$previousOffset = $date->getOffset();
@@ -200,6 +201,11 @@ public function increment(DateTimeInterface &$date, $invert = false, $parts = nu
200201
$date = $this->timezoneSafeModify($date, "-{$distance} hours");
201202
}
202203

204+
$actualDst = (int)$date->format('I');
205+
if ($originalDst < $actualDst) {
206+
$date = $this->timezoneSafeModify($date, "-1 hours");
207+
}
208+
203209
$date = $this->setTimeHour($date, $invert, $originalTimestamp);
204210

205211
$actualHour = (int)$date->format('H');

tests/Cron/DaylightSavingsTest.php

+113-14
Original file line numberDiff line numberDiff line change
@@ -209,28 +209,127 @@ public function testOffsetDecrementsPreviousRunDate(): void
209209
$this->assertEquals($dtExpected, $cron->getPreviousRunDate($dtCurrent, 0, false, $tz->getName()));
210210
}
211211

212-
public function testOffsetIncrementsMultipleRunDates(): void
212+
public static function dayLightSavingExamples(): \Generator
213213
{
214-
$expression = "0 1 * * 0";
215-
$cron = new CronExpression($expression);
216-
$tz = new \DateTimeZone("Europe/London");
214+
yield 'fixed hour before turnover point' => [
215+
"0 1 * * 0",
216+
[
217+
"2021-03-14 01:00+00:00",
218+
"2021-03-21 01:00+00:00",
219+
"2021-03-28 02:00+01:00",
220+
"2021-04-04 01:00+01:00",
221+
"2021-04-11 01:00+01:00",
222+
],
223+
new \DateTimeZone("Europe/London"),
224+
"2021-03-13 00:00+00:00",
225+
"2021-04-12 00:00+01:00",
226+
];
217227

218-
$expected = [
219-
$this->createDateTimeExactly("2021-03-14 01:00+00:00", $tz),
220-
$this->createDateTimeExactly("2021-03-21 01:00+00:00", $tz),
221-
$this->createDateTimeExactly("2021-03-28 02:00+01:00", $tz),
222-
$this->createDateTimeExactly("2021-04-04 01:00+01:00", $tz),
223-
$this->createDateTimeExactly("2021-04-11 01:00+01:00", $tz),
228+
yield 'fixed hour expression while daylight saving is starting (#154)' => [
229+
"30 07 * * *",
230+
[
231+
"2023-03-11 07:30-05:00",
232+
"2023-03-12 07:30-04:00",
233+
"2023-03-13 07:30-04:00",
234+
],
235+
new \DateTimeZone("America/New_York"),
236+
"2023-03-10 08:00-05:00",
237+
"2023-03-13 08:00-04:00",
224238
];
225239

226-
$dtCurrent = $this->createDateTimeExactly("2021-03-13 00:00+00:00", $tz);
227-
$actual = $cron->getMultipleRunDates(5, $dtCurrent, false, true, $tz->getName());
240+
yield 'fixed hour expression while daylight saving is starting (#202)' => [
241+
"0 10 * * *",
242+
[
243+
"2025-03-08 10:00-06:00",
244+
"2025-03-09 10:00-05:00",
245+
"2025-03-10 10:00-05:00",
246+
],
247+
new \DateTimeZone("America/Chicago"),
248+
"2025-03-08 09:00-06:00",
249+
"2025-03-10 11:00-05:00",
250+
];
251+
252+
yield 'fixed hour expression while daylight saving is ending' => [
253+
"0 10 * * *",
254+
[
255+
"2025-11-01 10:00-05:00",
256+
"2025-11-02 10:00-06:00",
257+
"2025-11-03 10:00-06:00",
258+
],
259+
new \DateTimeZone("America/Chicago"),
260+
"2025-11-01 09:00-05:00",
261+
"2025-11-03 11:00-06:00",
262+
];
263+
264+
yield 'every hour expression while daylight saving is starting' => [
265+
"30 */1 * * *",
266+
[
267+
"2025-03-09 00:30-06:00",
268+
"2025-03-09 01:30-06:00",
269+
"2025-03-09 03:30-05:00",
270+
"2025-03-09 04:30-05:00",
271+
],
272+
new \DateTimeZone("America/Chicago"),
273+
"2025-03-09 00:00-06:00",
274+
"2025-03-09 05:00-05:00",
275+
];
276+
277+
yield 'every hour expression while daylight saving is ending' => [
278+
"30 */1 * * *",
279+
[
280+
"2025-11-02 00:30-05:00",
281+
"2025-11-02 01:30-05:00",
282+
"2025-11-02 01:30-06:00",
283+
"2025-11-02 02:30-06:00",
284+
],
285+
new \DateTimeZone("America/Chicago"),
286+
"2025-11-02 00:00-05:00",
287+
"2025-11-02 03:00-06:00",
288+
];
289+
290+
yield 'fixed time expression inside the daylight transition hour' => [
291+
"30 2 * * *",
292+
[
293+
"2025-03-08 02:30-06:00",
294+
"2025-03-09 03:30-05:00",
295+
"2025-03-10 02:30-05:00",
296+
],
297+
new \DateTimeZone("America/Chicago"),
298+
"2025-03-08 01:30-06:00",
299+
"2025-03-10 11:00-05:00",
300+
];
301+
}
302+
303+
/**
304+
* @param string[] $expected
305+
*
306+
* @dataProvider dayLightSavingExamples
307+
*/
308+
public function testOffsetIncrementsMultipleRunDates(
309+
string $expression,
310+
array $expected,
311+
\DateTimeZone $tz,
312+
string $currentDate,
313+
string $currentInvertedDate
314+
): void {
315+
$cron = new CronExpression($expression);
316+
317+
$expected = array_map(
318+
function (string $dtString) use ($tz) {
319+
return $this->createDateTimeExactly($dtString, $tz);
320+
},
321+
$expected
322+
);
323+
$total = count($expected);
324+
325+
$dtCurrent = $this->createDateTimeExactly($currentDate, $tz);
326+
$actual = $cron->getMultipleRunDates($total, $dtCurrent, false, true, $tz->getName());
228327
foreach ($expected as $dtExpected) {
229328
$this->assertContainsEquals($dtExpected, $actual);
230329
}
231330

232-
$dtCurrent = $this->createDateTimeExactly("2021-04-12 00:00+01:00", $tz);
233-
$actual = $cron->getMultipleRunDates(5, $dtCurrent, true, true, $tz->getName());
331+
$dtCurrent = $this->createDateTimeExactly($currentInvertedDate, $tz);
332+
$actual = $cron->getMultipleRunDates($total, $dtCurrent, true, true, $tz->getName());
234333
foreach ($expected as $dtExpected) {
235334
$this->assertContainsEquals($dtExpected, $actual);
236335
}

0 commit comments

Comments
 (0)