Skip to content

Commit

Permalink
Merge pull request #62 from ingenerator/feat-2.x-clock-relative
Browse files Browse the repository at this point in the history
feature: Add `ago()` and `future()` helpers to RealtimeClock
  • Loading branch information
acoulton authored Nov 14, 2024
2 parents 9f63045 + 4bb9436 commit 5d21d8a
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 5 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
### Unreleased

### v2.2.0 (2024-11-14)

* Add `->ago()` and `->future()` helper methods to RealtimeClock

### v2.1.1 (2024-09-13)

* Support specifying unescaped-slashes in `JSON::encode()`
Expand Down
35 changes: 32 additions & 3 deletions src/DateTime/Clock/RealtimeClock.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

namespace Ingenerator\PHPUtils\DateTime\Clock;

use DateInterval;
use DateTimeImmutable;
use Ingenerator\PHPUtils\DateTime\DateTimeImmutableFactory;

/**
* Simple wrapper around current date/time methods to allow easy injection of fake time in
* dependent classes
Expand All @@ -14,13 +18,12 @@
*/
class RealtimeClock
{

/**
* @return \DateTimeImmutable
* @return DateTimeImmutable
*/
public function getDateTime()
{
return new \DateTimeImmutable;
return new DateTimeImmutable;
}

/**
Expand All @@ -38,4 +41,30 @@ public function usleep($microseconds)
{
\usleep($microseconds);
}

/**
* Calculate a relative date in the past, optionally truncating time to 0 - sugar for getDateTime()->sub()
*/
public function ago(DateInterval $interval, bool $date_only = FALSE): DateTimeImmutable
{
$result = $this->getDateTime()->sub($interval);

return match ($date_only) {
FALSE => $result,
TRUE => DateTimeImmutableFactory::zeroTime($result)
};
}

/**
* Calculate a relative date in the future, optionally truncating time to 0 - sugar for getDateTime()->add()
*/
public function future(DateInterval $interval, bool $date_only = FALSE): DateTimeImmutable
{
$result = $this->getDateTime()->add($interval);

return match ($date_only) {
FALSE => $result,
TRUE => DateTimeImmutableFactory::zeroTime($result)
};
}
}
8 changes: 8 additions & 0 deletions src/DateTime/DateTimeImmutableFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -199,4 +199,12 @@ public static function zeroMicros(DateTimeImmutable $time = new DateTimeImmutabl
);
}

/**
* Remove the entire time component from a DateTimeImmutable (e.g. reset it to midnight)
*/
public static function zeroTime(DateTimeImmutable $date_time = new DateTimeImmutable()): DateTimeImmutable
{
return $date_time->setTime(0, 0);
}

}
59 changes: 57 additions & 2 deletions test/unit/DateTime/Clock/RealtimeClockTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@
namespace test\unit\Ingenerator\PHPUtils\DateTime\Clock;


use Closure;
use DateTimeImmutable;
use Ingenerator\PHPUtils\DateTime\Clock\RealtimeClock;
use Ingenerator\PHPUtils\DateTime\DateIntervalFactory;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

class RealtimeClockTest extends TestCase
Expand All @@ -21,10 +25,10 @@ public function test_it_is_initialisable()
public function test_it_returns_current_time()
{
$time = $this->newSubject()->getDateTime();
$this->assertInstanceOf(\DateTimeImmutable::class, $time);
$this->assertInstanceOf(DateTimeImmutable::class, $time);
// Allow for time changing during the test
$this->assertEqualsWithDelta(
new \DateTimeImmutable,
new DateTimeImmutable,
$time,
1,
'Should be roughly the real time'
Expand Down Expand Up @@ -82,10 +86,61 @@ public function test_time_continues_during_the_life_of_an_instance()
);
}

public static function provider_relative_times():array
{
return [
'in the past, with time component' => [
fn(RealtimeClock $clock) => $clock->ago(DateIntervalFactory::years(6)),
'-6 year',
false,
],
'in the past, with date only' => [
fn(RealtimeClock $clock) => $clock->ago(DateIntervalFactory::months(6), date_only: true),
'-6 months 00:00:00.000000',
true,
],
'in the future, with time component' => [
fn(RealtimeClock $clock) => $clock->future(DateIntervalFactory::years(2)),
'+2 year',
false,
],
'in the future, with date only' => [
fn(RealtimeClock $clock) => $clock->future(DateIntervalFactory::months(1), date_only: true),
'+1 months 00:00:00.000000',
true,
],
];

}

#[DataProvider('provider_relative_times')]
public function test_it_returns_relative_times(Closure $test_method, string $expect_result, bool $expect_zero_time)
{
$subject = $this->newSubject();
// Capture the time before and after we do the calculation - time will pass during the test so we need to just
// know that it's between the offset we would expect immediately before, and the offset we'd expect immediately
// after.
$expect_before= new DateTimeImmutable($expect_result);
$result= $test_method($subject);
$expect_after= new DateTimeImmutable($expect_result);

$this->assertGreaterThanOrEqual($expect_before, $result);
$this->assertLessThanOrEqual($expect_after, $result);

if ($expect_zero_time) {
$this->assertSame('00:00:00.000000', $result->format('H:i:s.u'));
}
}

protected function newSubject()
{
return new RealtimeClock();
}

protected function assertBetween(mixed $expect_min, mixed $expect_max, mixed $actual, string $msg)
{
$this->assertGreaterThanOrEqual($expect_min, $actual, $msg);
$this->assertLessThanOrEqual($expect_max, $actual, $msg);
}

}
24 changes: 24 additions & 0 deletions test/unit/DateTime/Clock/StoppedMockClockTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
use DateInterval;
use DateTimeImmutable;
use Ingenerator\PHPUtils\DateTime\Clock\StoppedMockClock;
use Ingenerator\PHPUtils\DateTime\DateIntervalFactory;
use Ingenerator\PHPUtils\DateTime\DateString;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\ExpectationFailedException;
use PHPUnit\Framework\TestCase;
Expand Down Expand Up @@ -183,4 +185,26 @@ public function test_assert_never_slept_if_ever_slept()
$this->expectException(ExpectationFailedException::class);
$clock->assertNeverSlept();
}

public function test_ago_and_future_work_as_expected()
{
$clock = StoppedMockClock::at('2024-11-14 13:56:20.203123');
$this->assertSame(
[
'ago_with_time' => '2023-11-14T13:56:20.203123+00:00',
'ago_date_only' => '2023-11-14T00:00:00.000000+00:00',
'future_with_time' => '2025-01-14T13:56:20.203123+00:00',
'future_date_only' => '2025-01-14T00:00:00.000000+00:00',
],
array_map(
DateString::isoMS(...),
[
'ago_with_time' => $clock->ago(DateIntervalFactory::years(1)),
'ago_date_only' => $clock->ago(DateIntervalFactory::years(1), date_only: TRUE),
'future_with_time' => $clock->future(DateIntervalFactory::months(2)),
'future_date_only' => $clock->future(DateIntervalFactory::months(2), date_only: TRUE),
]
)
);
}
}
19 changes: 19 additions & 0 deletions test/unit/DateTime/DateTimeImmutableFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -251,4 +251,23 @@ public function test_zero_micros_uses_current_time_by_default()
$this->assertGreaterThanOrEqual($before, $result, 'Should be after start of test (ignoring micros)');
}

public function test_it_can_factory_with_zero_time()
{
$result = DateTimeImmutableFactory::zeroTime(
DateTimeImmutableFactory::fromIso('2023-01-03T10:02:03.123456+01:00')
);
$this->assertSame('2023-01-03T00:00:00.000000+01:00', DateString::isoMS($result));
}

public function test_zero_time_uses_current_time_by_default()
{
$before = new DateTimeImmutable('00:00:00.000000');
$result = DateTimeImmutableFactory::zeroTime();
$after = new DateTimeImmutable('00:00:00.000000');

$this->assertSame('00:00:00.000000', $result->format('H:i:s.u'), 'Time is midnight');
$this->assertLessThanOrEqual($after, $result, 'Should be after start of test');
$this->assertGreaterThanOrEqual($before, $result, 'Should be before end of test (ignoring time)');
}

}

0 comments on commit 5d21d8a

Please sign in to comment.