diff --git a/src/Chronos.php b/src/Chronos.php index a74bfb6..85f230f 100644 --- a/src/Chronos.php +++ b/src/Chronos.php @@ -1618,6 +1618,89 @@ public function previous(?int $dayOfWeek = null): static return $this->modify("last $day, midnight"); } + /** + * Get the next occurrence of a given day of the week at a specific time. + * + * Unlike `next()`, this method considers both the day AND the time. If + * today is the target day and the specified time hasn't passed yet, + * it returns today at that time. Otherwise, it returns next week. + * + * This is useful when you need a relative date that always points to + * the next future occurrence of a specific day and time. + * + * ### Example + * + * ``` + * // If it's Tuesday 9am, get "Tuesday 12pm" (today) + * // If it's Tuesday 4pm, get "Tuesday 12pm" (next week) + * $date = Chronos::now()->nextOccurrenceOf(Chronos::TUESDAY, 12, 0); + * ``` + * + * @param int $dayOfWeek The day of the week (use Chronos::MONDAY, etc.) + * @param int $hour The hour (0-23) + * @param int $minute The minute (0-59) + * @param int $second The second (0-59) + * @return static + */ + public function nextOccurrenceOf( + int $dayOfWeek, + int $hour, + int $minute = 0, + int $second = 0, + ): static { + // If today is the target day + if ($this->dayOfWeek === $dayOfWeek) { + $todayAtTime = $this->setTime($hour, $minute, $second); + // If the time hasn't passed yet, return today + if ($todayAtTime->greaterThan($this)) { + return $todayAtTime; + } + } + + // Otherwise, get next week's occurrence + return $this->next($dayOfWeek)->setTime($hour, $minute, $second); + } + + /** + * Get the previous occurrence of a given day of the week at a specific time. + * + * Unlike `previous()`, this method considers both the day AND the time. + * If today is the target day and the specified time has already passed, + * it returns today at that time. Otherwise, it returns last week. + * + * ### Example + * + * ``` + * // If it's Tuesday 4pm, get "Tuesday 12pm" (today, already passed) + * // If it's Tuesday 9am, get "Tuesday 12pm" (last week) + * $date = Chronos::now()->previousOccurrenceOf(Chronos::TUESDAY, 12, 0); + * ``` + * + * @param int $dayOfWeek The day of the week (use Chronos::MONDAY, etc.) + * @param int $hour The hour (0-23) + * @param int $minute The minute (0-59) + * @param int $second The second (0-59) + * @return static + */ + public function previousOccurrenceOf( + int $dayOfWeek, + int $hour, + int $minute = 0, + int $second = 0, + ): static { + // If today is the target day + if ($this->dayOfWeek === $dayOfWeek) { + $todayAtTime = $this->setTime($hour, $minute, $second); + // If the time has already passed, return today + if ($todayAtTime->lessThan($this)) { + return $todayAtTime; + } + } + + // Otherwise, get last week's occurrence + return $this->previous($dayOfWeek)->setTime($hour, $minute, $second); + } + /** * Modify to the first occurrence of a given day of the week * in the current month. If no dayOfWeek is provided, modify to the diff --git a/tests/TestCase/DateTime/DayOfWeekModifiersTest.php b/tests/TestCase/DateTime/DayOfWeekModifiersTest.php index aae7957..dfb1a5d 100644 --- a/tests/TestCase/DateTime/DayOfWeekModifiersTest.php +++ b/tests/TestCase/DateTime/DayOfWeekModifiersTest.php @@ -306,4 +306,102 @@ public function test3rdWednesdayOfYear() $d = Chronos::createFromDate(1975, 8, 5)->nthOfYear(3, 3); $this->assertDateTime($d, 1975, 1, 15, 0, 0, 0); } + + /** + * Test nextOccurrenceOf when today is the target day and time hasn't passed. + */ + public function testNextOccurrenceOfSameDayBeforeTime() + { + // It's Tuesday 9am, looking for Tuesday 12pm -> should be today + $d = Chronos::create(2024, 1, 9, 9, 0, 0); // Tuesday + $result = $d->nextOccurrenceOf(Chronos::TUESDAY, 12, 0); + $this->assertDateTime($result, 2024, 1, 9, 12, 0, 0); + } + + /** + * Test nextOccurrenceOf when today is the target day but time has passed. + */ + public function testNextOccurrenceOfSameDayAfterTime() + { + // It's Tuesday 4pm, looking for Tuesday 12pm -> should be next Tuesday + $d = Chronos::create(2024, 1, 9, 16, 0, 0); // Tuesday + $result = $d->nextOccurrenceOf(Chronos::TUESDAY, 12, 0); + $this->assertDateTime($result, 2024, 1, 16, 12, 0, 0); + } + + /** + * Test nextOccurrenceOf when today is not the target day. + */ + public function testNextOccurrenceOfDifferentDay() + { + // It's Monday, looking for Tuesday 12pm -> should be tomorrow + $d = Chronos::create(2024, 1, 8, 9, 0, 0); // Monday + $result = $d->nextOccurrenceOf(Chronos::TUESDAY, 12, 0); + $this->assertDateTime($result, 2024, 1, 9, 12, 0, 0); + } + + /** + * Test nextOccurrenceOf with seconds. + */ + public function testNextOccurrenceOfWithSeconds() + { + $d = Chronos::create(2024, 1, 8, 9, 0, 0); // Monday + $result = $d->nextOccurrenceOf(Chronos::WEDNESDAY, 14, 30, 45); + $this->assertDateTime($result, 2024, 1, 10, 14, 30, 45); + } + + /** + * Test nextOccurrenceOf at exact same time returns next week. + */ + public function testNextOccurrenceOfAtExactTime() + { + // It's Tuesday 12pm exactly, looking for Tuesday 12pm -> should be next week + $d = Chronos::create(2024, 1, 9, 12, 0, 0); // Tuesday 12pm + $result = $d->nextOccurrenceOf(Chronos::TUESDAY, 12, 0); + $this->assertDateTime($result, 2024, 1, 16, 12, 0, 0); + } + + /** + * Test previousOccurrenceOf when today is the target day and time has passed. + */ + public function testPreviousOccurrenceOfSameDayAfterTime() + { + // It's Tuesday 4pm, looking for previous Tuesday 12pm -> should be today + $d = Chronos::create(2024, 1, 9, 16, 0, 0); // Tuesday + $result = $d->previousOccurrenceOf(Chronos::TUESDAY, 12, 0); + $this->assertDateTime($result, 2024, 1, 9, 12, 0, 0); + } + + /** + * Test previousOccurrenceOf when today is the target day but time hasn't passed. + */ + public function testPreviousOccurrenceOfSameDayBeforeTime() + { + // It's Tuesday 9am, looking for previous Tuesday 12pm -> should be last Tuesday + $d = Chronos::create(2024, 1, 9, 9, 0, 0); // Tuesday + $result = $d->previousOccurrenceOf(Chronos::TUESDAY, 12, 0); + $this->assertDateTime($result, 2024, 1, 2, 12, 0, 0); + } + + /** + * Test previousOccurrenceOf when today is not the target day. + */ + public function testPreviousOccurrenceOfDifferentDay() + { + // It's Wednesday, looking for previous Tuesday 12pm -> should be yesterday + $d = Chronos::create(2024, 1, 10, 9, 0, 0); // Wednesday + $result = $d->previousOccurrenceOf(Chronos::TUESDAY, 12, 0); + $this->assertDateTime($result, 2024, 1, 9, 12, 0, 0); + } + + /** + * Test previousOccurrenceOf at exact same time returns last week. + */ + public function testPreviousOccurrenceOfAtExactTime() + { + // It's Tuesday 12pm exactly, looking for previous Tuesday 12pm -> should be last week + $d = Chronos::create(2024, 1, 9, 12, 0, 0); // Tuesday 12pm + $result = $d->previousOccurrenceOf(Chronos::TUESDAY, 12, 0); + $this->assertDateTime($result, 2024, 1, 2, 12, 0, 0); + } }