Skip to content

Commit 5d21d8a

Browse files
authored
Merge pull request #62 from ingenerator/feat-2.x-clock-relative
feature: Add `ago()` and `future()` helpers to RealtimeClock
2 parents 9f63045 + 4bb9436 commit 5d21d8a

File tree

6 files changed

+144
-5
lines changed

6 files changed

+144
-5
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
### Unreleased
22

3+
### v2.2.0 (2024-11-14)
4+
5+
* Add `->ago()` and `->future()` helper methods to RealtimeClock
6+
37
### v2.1.1 (2024-09-13)
48

59
* Support specifying unescaped-slashes in `JSON::encode()`

src/DateTime/Clock/RealtimeClock.php

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66

77
namespace Ingenerator\PHPUtils\DateTime\Clock;
88

9+
use DateInterval;
10+
use DateTimeImmutable;
11+
use Ingenerator\PHPUtils\DateTime\DateTimeImmutableFactory;
12+
913
/**
1014
* Simple wrapper around current date/time methods to allow easy injection of fake time in
1115
* dependent classes
@@ -14,13 +18,12 @@
1418
*/
1519
class RealtimeClock
1620
{
17-
1821
/**
19-
* @return \DateTimeImmutable
22+
* @return DateTimeImmutable
2023
*/
2124
public function getDateTime()
2225
{
23-
return new \DateTimeImmutable;
26+
return new DateTimeImmutable;
2427
}
2528

2629
/**
@@ -38,4 +41,30 @@ public function usleep($microseconds)
3841
{
3942
\usleep($microseconds);
4043
}
44+
45+
/**
46+
* Calculate a relative date in the past, optionally truncating time to 0 - sugar for getDateTime()->sub()
47+
*/
48+
public function ago(DateInterval $interval, bool $date_only = FALSE): DateTimeImmutable
49+
{
50+
$result = $this->getDateTime()->sub($interval);
51+
52+
return match ($date_only) {
53+
FALSE => $result,
54+
TRUE => DateTimeImmutableFactory::zeroTime($result)
55+
};
56+
}
57+
58+
/**
59+
* Calculate a relative date in the future, optionally truncating time to 0 - sugar for getDateTime()->add()
60+
*/
61+
public function future(DateInterval $interval, bool $date_only = FALSE): DateTimeImmutable
62+
{
63+
$result = $this->getDateTime()->add($interval);
64+
65+
return match ($date_only) {
66+
FALSE => $result,
67+
TRUE => DateTimeImmutableFactory::zeroTime($result)
68+
};
69+
}
4170
}

src/DateTime/DateTimeImmutableFactory.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,4 +199,12 @@ public static function zeroMicros(DateTimeImmutable $time = new DateTimeImmutabl
199199
);
200200
}
201201

202+
/**
203+
* Remove the entire time component from a DateTimeImmutable (e.g. reset it to midnight)
204+
*/
205+
public static function zeroTime(DateTimeImmutable $date_time = new DateTimeImmutable()): DateTimeImmutable
206+
{
207+
return $date_time->setTime(0, 0);
208+
}
209+
202210
}

test/unit/DateTime/Clock/RealtimeClockTest.php

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@
77
namespace test\unit\Ingenerator\PHPUtils\DateTime\Clock;
88

99

10+
use Closure;
11+
use DateTimeImmutable;
1012
use Ingenerator\PHPUtils\DateTime\Clock\RealtimeClock;
13+
use Ingenerator\PHPUtils\DateTime\DateIntervalFactory;
14+
use PHPUnit\Framework\Attributes\DataProvider;
1115
use PHPUnit\Framework\TestCase;
1216

1317
class RealtimeClockTest extends TestCase
@@ -21,10 +25,10 @@ public function test_it_is_initialisable()
2125
public function test_it_returns_current_time()
2226
{
2327
$time = $this->newSubject()->getDateTime();
24-
$this->assertInstanceOf(\DateTimeImmutable::class, $time);
28+
$this->assertInstanceOf(DateTimeImmutable::class, $time);
2529
// Allow for time changing during the test
2630
$this->assertEqualsWithDelta(
27-
new \DateTimeImmutable,
31+
new DateTimeImmutable,
2832
$time,
2933
1,
3034
'Should be roughly the real time'
@@ -82,10 +86,61 @@ public function test_time_continues_during_the_life_of_an_instance()
8286
);
8387
}
8488

89+
public static function provider_relative_times():array
90+
{
91+
return [
92+
'in the past, with time component' => [
93+
fn(RealtimeClock $clock) => $clock->ago(DateIntervalFactory::years(6)),
94+
'-6 year',
95+
false,
96+
],
97+
'in the past, with date only' => [
98+
fn(RealtimeClock $clock) => $clock->ago(DateIntervalFactory::months(6), date_only: true),
99+
'-6 months 00:00:00.000000',
100+
true,
101+
],
102+
'in the future, with time component' => [
103+
fn(RealtimeClock $clock) => $clock->future(DateIntervalFactory::years(2)),
104+
'+2 year',
105+
false,
106+
],
107+
'in the future, with date only' => [
108+
fn(RealtimeClock $clock) => $clock->future(DateIntervalFactory::months(1), date_only: true),
109+
'+1 months 00:00:00.000000',
110+
true,
111+
],
112+
];
113+
114+
}
115+
116+
#[DataProvider('provider_relative_times')]
117+
public function test_it_returns_relative_times(Closure $test_method, string $expect_result, bool $expect_zero_time)
118+
{
119+
$subject = $this->newSubject();
120+
// Capture the time before and after we do the calculation - time will pass during the test so we need to just
121+
// know that it's between the offset we would expect immediately before, and the offset we'd expect immediately
122+
// after.
123+
$expect_before= new DateTimeImmutable($expect_result);
124+
$result= $test_method($subject);
125+
$expect_after= new DateTimeImmutable($expect_result);
126+
127+
$this->assertGreaterThanOrEqual($expect_before, $result);
128+
$this->assertLessThanOrEqual($expect_after, $result);
129+
130+
if ($expect_zero_time) {
131+
$this->assertSame('00:00:00.000000', $result->format('H:i:s.u'));
132+
}
133+
}
85134

86135
protected function newSubject()
87136
{
88137
return new RealtimeClock();
89138
}
90139

140+
protected function assertBetween(mixed $expect_min, mixed $expect_max, mixed $actual, string $msg)
141+
{
142+
$this->assertGreaterThanOrEqual($expect_min, $actual, $msg);
143+
$this->assertLessThanOrEqual($expect_max, $actual, $msg);
144+
}
145+
91146
}

test/unit/DateTime/Clock/StoppedMockClockTest.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
use DateInterval;
77
use DateTimeImmutable;
88
use Ingenerator\PHPUtils\DateTime\Clock\StoppedMockClock;
9+
use Ingenerator\PHPUtils\DateTime\DateIntervalFactory;
10+
use Ingenerator\PHPUtils\DateTime\DateString;
911
use PHPUnit\Framework\Attributes\DataProvider;
1012
use PHPUnit\Framework\ExpectationFailedException;
1113
use PHPUnit\Framework\TestCase;
@@ -183,4 +185,26 @@ public function test_assert_never_slept_if_ever_slept()
183185
$this->expectException(ExpectationFailedException::class);
184186
$clock->assertNeverSlept();
185187
}
188+
189+
public function test_ago_and_future_work_as_expected()
190+
{
191+
$clock = StoppedMockClock::at('2024-11-14 13:56:20.203123');
192+
$this->assertSame(
193+
[
194+
'ago_with_time' => '2023-11-14T13:56:20.203123+00:00',
195+
'ago_date_only' => '2023-11-14T00:00:00.000000+00:00',
196+
'future_with_time' => '2025-01-14T13:56:20.203123+00:00',
197+
'future_date_only' => '2025-01-14T00:00:00.000000+00:00',
198+
],
199+
array_map(
200+
DateString::isoMS(...),
201+
[
202+
'ago_with_time' => $clock->ago(DateIntervalFactory::years(1)),
203+
'ago_date_only' => $clock->ago(DateIntervalFactory::years(1), date_only: TRUE),
204+
'future_with_time' => $clock->future(DateIntervalFactory::months(2)),
205+
'future_date_only' => $clock->future(DateIntervalFactory::months(2), date_only: TRUE),
206+
]
207+
)
208+
);
209+
}
186210
}

test/unit/DateTime/DateTimeImmutableFactoryTest.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,4 +251,23 @@ public function test_zero_micros_uses_current_time_by_default()
251251
$this->assertGreaterThanOrEqual($before, $result, 'Should be after start of test (ignoring micros)');
252252
}
253253

254+
public function test_it_can_factory_with_zero_time()
255+
{
256+
$result = DateTimeImmutableFactory::zeroTime(
257+
DateTimeImmutableFactory::fromIso('2023-01-03T10:02:03.123456+01:00')
258+
);
259+
$this->assertSame('2023-01-03T00:00:00.000000+01:00', DateString::isoMS($result));
260+
}
261+
262+
public function test_zero_time_uses_current_time_by_default()
263+
{
264+
$before = new DateTimeImmutable('00:00:00.000000');
265+
$result = DateTimeImmutableFactory::zeroTime();
266+
$after = new DateTimeImmutable('00:00:00.000000');
267+
268+
$this->assertSame('00:00:00.000000', $result->format('H:i:s.u'), 'Time is midnight');
269+
$this->assertLessThanOrEqual($after, $result, 'Should be after start of test');
270+
$this->assertGreaterThanOrEqual($before, $result, 'Should be before end of test (ignoring time)');
271+
}
272+
254273
}

0 commit comments

Comments
 (0)