Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
1 change: 1 addition & 0 deletions docs/.vitepress/toc_en.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"collapsed": false,
"items": [
{ "text": "Introduction", "link": "/index" },
{ "text": "4.x Migration Guide", "link": "/4-x-migration-guide" },
{ "text": "3.x Migration Guide", "link": "/3-x-migration-guide" },
{ "text": "API", "link": "https://api.cakephp.org/chronos" }
]
Expand Down
28 changes: 28 additions & 0 deletions docs/en/4-x-migration-guide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# 4.x Migration Guide

Chronos 4.x contains breaking changes that could impact your application. This
guide provides an overview of the breaking changes made in 4.x

## diff() and fromNow() return ChronosInterval

`Chronos::diff()`, `ChronosDate::diff()` and `Chronos::fromNow()` now return a
`ChronosInterval` instead of a `DateInterval`. `Chronos::fromNow()` previously
returned `DateInterval|false`; it now always returns a `ChronosInterval`.

`ChronosInterval` decorates the native `DateInterval` and exposes the same
properties (`y`, `m`, `d`, `h`, `i`, `s`, `f`, `invert`, `days`), so most code
keeps working unchanged. However, it does **not** extend `DateInterval`: code
that type-hints `DateInterval` or relies on `instanceof DateInterval` against
the result must call `->toNative()` to get the underlying `DateInterval`:

```php
// Before (3.x)
$interval = $first->diff($second); // DateInterval

// After (4.x), when a native DateInterval is required
$interval = $first->diff($second)->toNative(); // DateInterval
```

In return, the result gains ISO 8601 duration formatting and a number of
convenience methods. See [Working with Intervals](/index#working-with-intervals)
for the full overview.
51 changes: 50 additions & 1 deletion docs/en/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ $time->isWithinNext('3 hours');
In addition to comparing datetimes, calculating differences or deltas between
two values is a common task:
```php
// Get a DateInterval representing the difference
// Get a ChronosInterval representing the difference
$first->diff($second);

// Get difference as a count of specific units.
Expand All @@ -230,6 +230,55 @@ echo $date->diffForHumans();
echo $date->diffForHumans($other); // 1 hour ago;
```

## Working with Intervals

`Chronos::diff()`, `ChronosDate::diff()` and `Chronos::fromNow()` return a
`ChronosInterval`. It decorates the native `DateInterval`, so all the usual
properties (`y`, `m`, `d`, `h`, `i`, `s`, `f`, `invert`, `days`) keep working
while adding convenience methods on top:
```php
$interval = $first->diff($second);

// ISO 8601 duration string. __toString() returns the same value.
echo $interval->toIso8601String(); // P1Y2M3D
echo $interval; // P1Y2M3D

// Totals. totalDays() is exact when the interval comes from diff();
// totalSeconds() approximates using 30-day months and 365-day years.
$interval->totalDays();
$interval->totalSeconds();

// State checks.
$interval->isZero();
$interval->isNegative();

// A strtotime()-compatible relative string.
echo $interval->toDateString(); // 1 year 2 months 3 days

// Component-wise arithmetic (no overflow normalization).
$interval->add($other);
$interval->sub($other);
```
You can also build intervals directly:
```php
use Cake\Chronos\ChronosInterval;

ChronosInterval::create('P1Y2M3D');
ChronosInterval::createFromValues(years: 1, months: 2, days: 3);
ChronosInterval::createFromDateString('1 year 2 days');
ChronosInterval::instance($dateInterval);
```
When an API requires a native `DateInterval`, call `toNative()`:
```php
$native = $first->diff($second)->toNative();
```

> [!NOTE]
> `ChronosInterval` is a decorator and does **not** extend `DateInterval`, so
> code that type-hints `DateInterval` or relies on `instanceof DateInterval`
> against the result of `diff()`/`fromNow()` must call `->toNative()` to get
> the wrapped `DateInterval` back.

## Formatting Strings

Chronos provides a number of methods for displaying our outputting datetime
Expand Down
3 changes: 3 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,6 @@ parameters:
message: "#with generic class DatePeriod but does not specify its types: TDate, TEnd, TRecurrences$#"
count: 1
path: src/ChronosDatePeriod.php
-
identifier: method.childReturnType
path: src/Chronos.php
12 changes: 7 additions & 5 deletions src/Chronos.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use DateTimeInterface;
use DateTimeZone;
use InvalidArgumentException;
use ReturnTypeWillChange;
use RuntimeException;
use Stringable;

Expand Down Expand Up @@ -1011,11 +1012,12 @@ public function modify(string $modifier): static
*
* @param \DateTimeInterface $target Target instance
* @param bool $absolute Whether the interval is forced to be positive
* @return \DateInterval
* @return \Cake\Chronos\ChronosInterval
*/
public function diff(DateTimeInterface $target, bool $absolute = false): DateInterval
#[ReturnTypeWillChange]
public function diff(DateTimeInterface $target, bool $absolute = false): ChronosInterval
{
return parent::diff($target, $absolute);
return new ChronosInterval(parent::diff($target, $absolute));
}

/**
Expand Down Expand Up @@ -2778,9 +2780,9 @@ public function secondsUntilEndOfDay(): int
* Convenience method for getting the remaining time from a given time.
*
* @param \DateTimeInterface $other The date to get the remaining time from.
* @return \DateInterval|bool The DateInterval object representing the difference between the two dates or FALSE on failure.
* @return \Cake\Chronos\ChronosInterval The ChronosInterval object representing the difference between the two dates.
*/
public static function fromNow(DateTimeInterface $other): DateInterval|bool
public static function fromNow(DateTimeInterface $other): ChronosInterval
{
$timeNow = new static();

Expand Down
6 changes: 3 additions & 3 deletions src/ChronosDate.php
Original file line number Diff line number Diff line change
Expand Up @@ -407,11 +407,11 @@ public function setISODate(int $year, int $week, int $dayOfWeek = 1): static
*
* @param \Cake\Chronos\ChronosDate $target Target instance
* @param bool $absolute Whether the interval is forced to be positive
* @return \DateInterval
* @return \Cake\Chronos\ChronosInterval
*/
public function diff(ChronosDate $target, bool $absolute = false): DateInterval
public function diff(ChronosDate $target, bool $absolute = false): ChronosInterval
{
return $this->native->diff($target->native, $absolute);
return new ChronosInterval($this->native->diff($target->native, $absolute));
}

/**
Expand Down
34 changes: 25 additions & 9 deletions tests/TestCase/ChronosIntervalTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,32 @@
namespace Cake\Chronos\Test\TestCase;

use Cake\Chronos\Chronos;
use Cake\Chronos\ChronosDate;
use Cake\Chronos\ChronosInterval;
use DateInterval;

class ChronosIntervalTest extends TestCase
{
public function testChronosDiffReturnsChronosInterval(): void
{
$start = new Chronos('2020-01-01');
$end = new Chronos('2020-01-11');
$diff = $start->diff($end);

$this->assertInstanceOf(ChronosInterval::class, $diff);
$this->assertSame(10, $diff->d);
}

public function testChronosDateDiffReturnsChronosInterval(): void
{
$start = ChronosDate::create(2020, 1, 1);
$end = ChronosDate::create(2020, 1, 11);
$diff = $start->diff($end);

$this->assertInstanceOf(ChronosInterval::class, $diff);
$this->assertSame(10, $diff->d);
}

public function testCreateFromSpec(): void
{
$interval = ChronosInterval::create('P1Y2M3D');
Expand Down Expand Up @@ -88,10 +109,8 @@ public function testToIso8601StringNegative(): void
{
$past = new Chronos('2020-01-01');
$future = new Chronos('2021-02-02');
$diff = $past->diff($future);
$diff->invert = 1;
$interval = $future->diff($past);

$interval = ChronosInterval::instance($diff);
$this->assertStringStartsWith('-P', $interval->toIso8601String());
}

Expand Down Expand Up @@ -123,9 +142,8 @@ public function testTotalDaysFromDiff(): void
{
$start = new Chronos('2020-01-01');
$end = new Chronos('2020-01-11');
$diff = $start->diff($end);
$interval = $start->diff($end);

$interval = ChronosInterval::instance($diff);
$this->assertSame(10, $interval->totalDays());
}

Expand All @@ -136,9 +154,8 @@ public function testIsNegative(): void

$past = new Chronos('2020-01-01');
$future = new Chronos('2020-01-02');
$diff = $future->diff($past);
$interval = $future->diff($past);

$interval = ChronosInterval::instance($diff);
$this->assertTrue($interval->isNegative());
}

Expand Down Expand Up @@ -296,9 +313,8 @@ public function testToDateStringNegative(): void
{
$past = new Chronos('2020-01-01');
$future = new Chronos('2020-01-02');
$diff = $future->diff($past);
$interval = $future->diff($past);

$interval = ChronosInterval::instance($diff);
$this->assertStringStartsWith('-', $interval->toDateString());
}

Expand Down
Loading