@@ -327,6 +327,38 @@ public static function hasTestNow(): bool
327327 return static ::$ testNow !== null ;
328328 }
329329
330+ /**
331+ * Temporarily sets "now" to the given value and executes the callback.
332+ *
333+ * After the callback is executed, the previous value of "now" is restored.
334+ * This is useful for testing time-sensitive code without affecting other tests.
335+ *
336+ * ### Example:
337+ *
338+ * ```
339+ * $result = Chronos::withTestNow('2023-06-15 12:00:00', function () {
340+ * return Chronos::now()->format('Y-m-d');
341+ * });
342+ * // $result === '2023-06-15'
343+ * ```
344+ *
345+ * @template T
346+ * @param \Cake\Chronos\Chronos|string|null $testNow The instance to use as "now".
347+ * @param callable(): T $callback The callback to execute.
348+ * @return T The return value of the callback.
349+ */
350+ public static function withTestNow (Chronos |string |null $ testNow , callable $ callback ): mixed
351+ {
352+ $ previous = static ::getTestNow ();
353+ static ::setTestNow ($ testNow );
354+
355+ try {
356+ return $ callback ();
357+ } finally {
358+ static ::setTestNow ($ previous );
359+ }
360+ }
361+
330362 /**
331363 * Determine if there is just a time in the time string
332364 *
@@ -1024,6 +1056,25 @@ public function setTimezone(DateTimeZone|string $value): static
10241056 return parent ::setTimezone (static ::safeCreateDateTimeZone ($ value ));
10251057 }
10261058
1059+ /**
1060+ * Change the timezone while keeping the local time.
1061+ *
1062+ * Unlike `setTimezone()` which converts the time to the new timezone,
1063+ * this method keeps the same wall clock time but changes the timezone.
1064+ *
1065+ * For example, if you have 10:00 AM in New York and shift to Chicago,
1066+ * you'll get 10:00 AM in Chicago (not 9:00 AM as setTimezone would give).
1067+ *
1068+ * @param \DateTimeZone|string $timezone The new timezone
1069+ * @return static
1070+ */
1071+ public function shiftTimezone (DateTimeZone |string $ timezone ): static
1072+ {
1073+ $ timezone = static ::safeCreateDateTimeZone ($ timezone );
1074+
1075+ return new static ($ this ->format ('Y-m-d H:i:s.u ' ), $ timezone );
1076+ }
1077+
10271078 /**
10281079 * Return time zone set for this instance.
10291080 *
@@ -1636,6 +1687,89 @@ public function previous(?int $dayOfWeek = null): static
16361687 return $ this ->modify ("last $ day, midnight " );
16371688 }
16381689
1690+ /**
1691+ * Get the next occurrence of a given day of the week at a specific time.
1692+ *
1693+ * Unlike `next()`, this method considers both the day AND the time. If
1694+ * today is the target day and the specified time hasn't passed yet,
1695+ * it returns today at that time. Otherwise, it returns next week.
1696+ *
1697+ * This is useful when you need a relative date that always points to
1698+ * the next future occurrence of a specific day and time.
1699+ *
1700+ * ### Example
1701+ *
1702+ * ```
1703+ * // If it's Tuesday 9am, get "Tuesday 12pm" (today)
1704+ * // If it's Tuesday 4pm, get "Tuesday 12pm" (next week)
1705+ * $date = Chronos::now()->nextOccurrenceOf(Chronos::TUESDAY, 12, 0);
1706+ * ```
1707+ *
1708+ * @param int $dayOfWeek The day of the week (use Chronos::MONDAY, etc.)
1709+ * @param int $hour The hour (0-23)
1710+ * @param int $minute The minute (0-59)
1711+ * @param int $second The second (0-59)
1712+ * @return static
1713+ */
1714+ public function nextOccurrenceOf (
1715+ int $ dayOfWeek ,
1716+ int $ hour ,
1717+ int $ minute = 0 ,
1718+ int $ second = 0 ,
1719+ ): static {
1720+ // If today is the target day
1721+ if ($ this ->dayOfWeek === $ dayOfWeek ) {
1722+ $ todayAtTime = $ this ->setTime ($ hour , $ minute , $ second );
1723+ // If the time hasn't passed yet, return today
1724+ if ($ todayAtTime ->greaterThan ($ this )) {
1725+ return $ todayAtTime ;
1726+ }
1727+ }
1728+
1729+ // Otherwise, get next week's occurrence
1730+ return $ this ->next ($ dayOfWeek )->setTime ($ hour , $ minute , $ second );
1731+ }
1732+
1733+ /**
1734+ * Get the previous occurrence of a given day of the week at a specific time.
1735+ *
1736+ * Unlike `previous()`, this method considers both the day AND the time.
1737+ * If today is the target day and the specified time has already passed,
1738+ * it returns today at that time. Otherwise, it returns last week.
1739+ *
1740+ * ### Example
1741+ *
1742+ * ```
1743+ * // If it's Tuesday 4pm, get "Tuesday 12pm" (today, already passed)
1744+ * // If it's Tuesday 9am, get "Tuesday 12pm" (last week)
1745+ * $date = Chronos::now()->previousOccurrenceOf(Chronos::TUESDAY, 12, 0);
1746+ * ```
1747+ *
1748+ * @param int $dayOfWeek The day of the week (use Chronos::MONDAY, etc.)
1749+ * @param int $hour The hour (0-23)
1750+ * @param int $minute The minute (0-59)
1751+ * @param int $second The second (0-59)
1752+ * @return static
1753+ */
1754+ public function previousOccurrenceOf (
1755+ int $ dayOfWeek ,
1756+ int $ hour ,
1757+ int $ minute = 0 ,
1758+ int $ second = 0 ,
1759+ ): static {
1760+ // If today is the target day
1761+ if ($ this ->dayOfWeek === $ dayOfWeek ) {
1762+ $ todayAtTime = $ this ->setTime ($ hour , $ minute , $ second );
1763+ // If the time has already passed, return today
1764+ if ($ todayAtTime ->lessThan ($ this )) {
1765+ return $ todayAtTime ;
1766+ }
1767+ }
1768+
1769+ // Otherwise, get last week's occurrence
1770+ return $ this ->previous ($ dayOfWeek )->setTime ($ hour , $ minute , $ second );
1771+ }
1772+
16391773 /**
16401774 * Modify to the first occurrence of a given day of the week
16411775 * in the current month. If no dayOfWeek is provided, modify to the
@@ -2689,6 +2823,25 @@ public function toNative(): DateTimeImmutable
26892823 return new DateTimeImmutable ($ this ->format ('Y-m-d H:i:s.u ' ), $ this ->getTimezone ());
26902824 }
26912825
2826+ /**
2827+ * Returns the date and time as an associative array.
2828+ *
2829+ * @return array{year: int, month: int, day: int, hour: int, minute: int, second: int, microsecond: int, timezone: string}
2830+ */
2831+ public function toArray (): array
2832+ {
2833+ return [
2834+ 'year ' => $ this ->year ,
2835+ 'month ' => $ this ->month ,
2836+ 'day ' => $ this ->day ,
2837+ 'hour ' => $ this ->hour ,
2838+ 'minute ' => $ this ->minute ,
2839+ 'second ' => $ this ->second ,
2840+ 'microsecond ' => $ this ->microsecond ,
2841+ 'timezone ' => $ this ->timezone ->getName (),
2842+ ];
2843+ }
2844+
26922845 /**
26932846 * Get a part of the object
26942847 *
0 commit comments