Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,64 @@ echo $today->modify('+3 hours');

Like instances of `Chronos`, `ChronosDate` objects are also *immutable*.

# Time-only Values

When you need to work with just times (without dates), use `ChronosTime`:

```php
use Cake\Chronos\ChronosTime;

$time = new ChronosTime('14:30:00');
echo $time->format('g:i A'); // 2:30 PM

// Create from components
$time = ChronosTime::create(14, 30, 0);

// Arithmetic
$later = $time->addHours(2)->addMinutes(15);
```

`ChronosTime` is useful for recurring schedules, business hours, or any scenario
where the date is irrelevant.

# Testing with Chronos

Chronos provides `setTestNow()` to freeze time during testing:

```php
use Cake\Chronos\Chronos;

// Freeze time for predictable tests
Chronos::setTestNow('2024-01-15 10:00:00');

$now = Chronos::now(); // Always 2024-01-15 10:00:00

// Reset to real time
Chronos::setTestNow(null);
```

# PSR-20 Clock Interface

For dependency injection, use `ClockFactory` which implements PSR-20:

```php
use Cake\Chronos\ClockFactory;

$clock = new ClockFactory('UTC');
$now = $clock->now(); // Returns Chronos instance

// In your service
class OrderService
{
public function __construct(private ClockInterface $clock) {}

public function createOrder(): Order
{
return new Order(createdAt: $this->clock->now());
}
}
```

# Documentation

A more descriptive documentation can be found at [book.cakephp.org/chronos/3/](https://book.cakephp.org/chronos/3/).
Expand Down
165 changes: 122 additions & 43 deletions src/Chronos.php
Original file line number Diff line number Diff line change
Expand Up @@ -769,6 +769,22 @@ protected static function safeCreateDateTimeZone(DateTimeZone|string|null $objec
/**
* Create a new DateInterval instance from specified values.
*
* Values that exceed their natural limits will automatically roll over
* to the next higher unit. For example, 90 minutes becomes 1 hour and
* 30 minutes, 25 hours becomes 1 day and 1 hour.
*
* Rollover cascades upward: microseconds -> seconds -> minutes -> hours -> days.
* Years, months, and weeks do not roll over.
*
* Example:
* ```
* // 90 seconds becomes 1 minute 30 seconds
* $interval = Chronos::createInterval(seconds: 90);
*
* // 25 hours becomes 1 day 1 hour
* $interval = Chronos::createInterval(hours: 25);
* ```
*
* @param int|null $years The year to use.
* @param int|null $months The month to use.
* @param int|null $weeks The week to use.
Expand Down Expand Up @@ -1584,7 +1600,8 @@ public function endOfWeek(): static
* of the current day of the week. Use the supplied consts
* to indicate the desired dayOfWeek, ex. Chronos::MONDAY.
*
* @param int|null $dayOfWeek The day of the week to move to.
* @param int|null $dayOfWeek The day of the week (use Chronos::MONDAY through
* Chronos::SUNDAY), or null for a sensible default.
* @return static
*/
public function next(?int $dayOfWeek = null): static
Expand All @@ -1604,7 +1621,8 @@ public function next(?int $dayOfWeek = null): static
* of the current day of the week. Use the supplied consts
* to indicate the desired dayOfWeek, ex. Chronos::MONDAY.
*
* @param int|null $dayOfWeek The day of the week to move to.
* @param int|null $dayOfWeek The day of the week (use Chronos::MONDAY through
* Chronos::SUNDAY), or null for a sensible default.
* @return static
*/
public function previous(?int $dayOfWeek = null): static
Expand All @@ -1624,7 +1642,8 @@ public function previous(?int $dayOfWeek = null): static
* first day of the current month. Use the supplied consts
* to indicate the desired dayOfWeek, ex. Chronos::MONDAY.
*
* @param int|null $dayOfWeek The day of the week to move to.
* @param int|null $dayOfWeek The day of the week (use Chronos::MONDAY through
* Chronos::SUNDAY), or null for a sensible default.
* @return static
*/
public function firstOfMonth(?int $dayOfWeek = null): static
Expand All @@ -1640,7 +1659,8 @@ public function firstOfMonth(?int $dayOfWeek = null): static
* last day of the current month. Use the supplied consts
* to indicate the desired dayOfWeek, ex. Chronos::MONDAY.
*
* @param int|null $dayOfWeek The day of the week to move to.
* @param int|null $dayOfWeek The day of the week (use Chronos::MONDAY through
* Chronos::SUNDAY), or null for a sensible default.
* @return static
*/
public function lastOfMonth(?int $dayOfWeek = null): static
Expand All @@ -1651,14 +1671,22 @@ public function lastOfMonth(?int $dayOfWeek = null): static
}

/**
* Modify to the given occurrence of a given day of the week
* in the current month. If the calculated occurrence is outside the scope
* of the current month, then return false and no modifications are made.
* Use the supplied consts to indicate the desired dayOfWeek, ex. Chronos::MONDAY.
* Get the nth occurrence of a day of the week in the current month.
*
* @param int $nth The offset to use.
* @param int $dayOfWeek The day of the week to move to.
* @return static|false
* Returns false if the requested occurrence doesn't exist in the month.
* For example, requesting the 5th Monday will return false for months
* that only have 4 Mondays.
*
* Example:
* ```
* $date = new Chronos('2024-01-15');
* $date->nthOfMonth(2, Chronos::TUESDAY); // 2nd Tuesday of January
* $date->nthOfMonth(5, Chronos::MONDAY); // false if no 5th Monday
* ```
*
* @param int $nth The occurrence number (1 = first, 2 = second, etc.).
* @param int $dayOfWeek The day of the week (use Chronos::MONDAY, etc.).
* @return static|false The date of the nth occurrence, or false if it doesn't exist.
*/
public function nthOfMonth(int $nth, int $dayOfWeek): static|false
{
Expand All @@ -1675,7 +1703,8 @@ public function nthOfMonth(int $nth, int $dayOfWeek): static|false
* first day of the current quarter. Use the supplied consts
* to indicate the desired dayOfWeek, ex. Chronos::MONDAY.
*
* @param int|null $dayOfWeek The day of the week to move to.
* @param int|null $dayOfWeek The day of the week (use Chronos::MONDAY through
* Chronos::SUNDAY), or null for a sensible default.
* @return static
*/
public function firstOfQuarter(?int $dayOfWeek = null): static
Expand All @@ -1692,7 +1721,8 @@ public function firstOfQuarter(?int $dayOfWeek = null): static
* last day of the current quarter. Use the supplied consts
* to indicate the desired dayOfWeek, ex. Chronos::MONDAY.
*
* @param int|null $dayOfWeek The day of the week to move to.
* @param int|null $dayOfWeek The day of the week (use Chronos::MONDAY through
* Chronos::SUNDAY), or null for a sensible default.
* @return static
*/
public function lastOfQuarter(?int $dayOfWeek = null): static
Expand All @@ -1704,14 +1734,20 @@ public function lastOfQuarter(?int $dayOfWeek = null): static
}

/**
* Modify to the given occurrence of a given day of the week
* in the current quarter. If the calculated occurrence is outside the scope
* of the current quarter, then return false and no modifications are made.
* Use the supplied consts to indicate the desired dayOfWeek, ex. Chronos::MONDAY.
* Get the nth occurrence of a day of the week in the current quarter.
*
* @param int $nth The offset to use.
* @param int $dayOfWeek The day of the week to move to.
* @return static|false
* Returns false if the requested occurrence doesn't exist in the quarter.
* Quarters are: Q1 (Jan-Mar), Q2 (Apr-Jun), Q3 (Jul-Sep), Q4 (Oct-Dec).
*
* Example:
* ```
* $date = new Chronos('2024-02-15'); // Q1
* $date->nthOfQuarter(5, Chronos::FRIDAY); // 5th Friday of Q1
* ```
*
* @param int $nth The occurrence number (1 = first, 2 = second, etc.).
* @param int $dayOfWeek The day of the week (use Chronos::MONDAY, etc.).
* @return static|false The date of the nth occurrence, or false if it doesn't exist.
*/
public function nthOfQuarter(int $nth, int $dayOfWeek): static|false
{
Expand All @@ -1729,7 +1765,8 @@ public function nthOfQuarter(int $nth, int $dayOfWeek): static|false
* first day of the current year. Use the supplied consts
* to indicate the desired dayOfWeek, ex. Chronos::MONDAY.
*
* @param int|null $dayOfWeek The day of the week to move to.
* @param int|null $dayOfWeek The day of the week (use Chronos::MONDAY through
* Chronos::SUNDAY), or null for a sensible default.
* @return static
*/
public function firstOfYear(?int $dayOfWeek = null): static
Expand All @@ -1745,7 +1782,8 @@ public function firstOfYear(?int $dayOfWeek = null): static
* last day of the current year. Use the supplied consts
* to indicate the desired dayOfWeek, ex. Chronos::MONDAY.
*
* @param int|null $dayOfWeek The day of the week to move to.
* @param int|null $dayOfWeek The day of the week (use Chronos::MONDAY through
* Chronos::SUNDAY), or null for a sensible default.
* @return static
*/
public function lastOfYear(?int $dayOfWeek = null): static
Expand All @@ -1756,13 +1794,20 @@ public function lastOfYear(?int $dayOfWeek = null): static
}

/**
* Modify to the given occurrence of a given day of the week
* in the current year. If the calculated occurrence is outside the scope
* of the current year, then return false and no modifications are made.
* Use the supplied consts to indicate the desired dayOfWeek, ex. Chronos::MONDAY.
* Get the nth occurrence of a day of the week in the current year.
*
* Returns false if the requested occurrence doesn't exist in the year
* (e.g., requesting the 53rd Monday in a year with only 52).
*
* @param int $nth The offset to use.
* @param int $dayOfWeek The day of the week to move to.
* Example:
* ```
* $date = new Chronos('2024-06-15');
* $date->nthOfYear(1, Chronos::MONDAY); // First Monday of the year
* $date->nthOfYear(52, Chronos::FRIDAY); // 52nd Friday of the year
* ```
*
* @param int $nth The occurrence number (1 = first, 2 = second, etc.).
* @param int $dayOfWeek The day of the week (use Chronos::MONDAY, etc.).
* @return static|false
*/
public function nthOfYear(int $nth, int $dayOfWeek): static|false
Expand Down Expand Up @@ -1914,10 +1959,13 @@ public function farthest(DateTimeInterface $first, DateTimeInterface $second, Da
}

/**
* Get the minimum instance between a given instance (default now) and the current instance.
* Get the earlier of this instance and another.
*
* @param \DateTimeInterface|null $other The instance to compare with.
* @return static
* Returns whichever datetime comes first chronologically.
* If no other instance is provided, compares against the current time.
*
* @param \DateTimeInterface|null $other The instance to compare with. Defaults to now.
* @return static The earlier of the two datetimes.
*/
public function min(?DateTimeInterface $other = null): static
{
Expand All @@ -1931,10 +1979,13 @@ public function min(?DateTimeInterface $other = null): static
}

/**
* Get the maximum instance between a given instance (default now) and the current instance.
* Get the later of this instance and another.
*
* @param \DateTimeInterface|null $other The instance to compare with.
* @return static
* Returns whichever datetime comes last chronologically.
* If no other instance is provided, compares against the current time.
*
* @param \DateTimeInterface|null $other The instance to compare with. Defaults to now.
* @return static The later of the two datetimes.
*/
public function max(?DateTimeInterface $other = null): static
{
Expand All @@ -1948,10 +1999,21 @@ public function max(?DateTimeInterface $other = null): static
}

/**
* Modify the current instance to the average of a given instance (default now) and the current instance.
* Get the midpoint between this instance and another.
*
* @param \DateTimeInterface|null $other The instance to compare with.
* @return static
* Calculates the datetime that is exactly halfway between the current
* instance and the given instance. If no other instance is provided,
* uses the current time (now).
*
* Example:
* ```
* $jan1 = new Chronos('2024-01-01 00:00:00');
* $jan3 = new Chronos('2024-01-03 00:00:00');
* $midpoint = $jan1->average($jan3); // 2024-01-02 00:00:00
* ```
*
* @param \DateTimeInterface|null $other The instance to find midpoint with. Defaults to now.
* @return static The datetime exactly between this instance and the other.
*/
public function average(?DateTimeInterface $other = null): static
{
Expand Down Expand Up @@ -2303,14 +2365,31 @@ public function isWithinNext(string|int $timeInterval): bool
}

/**
* Get the difference by the given interval using a filter callable
* Get the difference by the given interval using a filter callback.
*
* @param \DateInterval $interval An interval to traverse by
* @param callable $callback The callback to use for filtering.
* @param \DateTimeInterface|null $other The instance to difference from.
* @param bool $absolute Get the absolute of the difference
* Iterates through the date range at the given interval and counts
* how many times the callback returns true.
*
* Example:
* ```
* // Count weekdays between two dates
* $start = new Chronos('2024-01-01');
* $end = new Chronos('2024-01-31');
* $weekdays = $start->diffFiltered(
* new DateInterval('P1D'),
* fn($date) => !$date->isWeekend(),
* $end
* );
* ```
*
* @param \DateInterval $interval An interval to traverse by (e.g., P1D for daily).
* @param callable $callback Filter function that receives each date in the range.
* Should return true to count the date, false to skip it.
* Signature: `function(Chronos $date): bool`
* @param \DateTimeInterface|null $other The end date. Defaults to now.
* @param bool $absolute Get the absolute of the difference.
* @param int $options DatePeriod options, {@see https://www.php.net/manual/en/class.dateperiod.php}
* @return int
* @return int The count of intervals where the callback returned true.
*/
public function diffFiltered(
DateInterval $interval,
Expand Down
Loading
Loading