Skip to content
Merged
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
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.
55 changes: 54 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,59 @@ echo $date->diffForHumans();
echo $date->diffForHumans($other); // 1 hour ago;
```

## Working with Intervals

> [!TIP] Changed in 4.x
> `diff()` and `fromNow()` previously returned a native `DateInterval`. See the
> [4.x Migration Guide](/4-x-migration-guide).

`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