Skip to content

Commit d9b1416

Browse files
committed
Add clock component
1 parent d1547a2 commit d9b1416

9 files changed

Lines changed: 426 additions & 0 deletions

File tree

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
name: Close Pull Request
2+
3+
on:
4+
pull_request_target:
5+
types: [opened]
6+
7+
jobs:
8+
run:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- uses: superbrothers/close-pull-request@v3
12+
with:
13+
comment: |
14+
Thanks for your Pull Request! We love contributions.
15+
16+
However, you should instead open your PR on the main repository:
17+
https://github.com/sxbrsky/aether
18+
19+
This repository is what we call a "subtree split": a read-only subset of that main repository.

packages/Clock/LICENSE

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Copyright (c) 2024 Dominik Szamburski
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy
4+
of this software and associated documentation files (the "Software"), to deal
5+
in the Software without restriction, including without limitation the rights
6+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
copies of the Software, and to permit persons to whom the Software is
8+
furnished to do so, subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in all
11+
copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19+
SOFTWARE.

packages/Clock/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Aether Clock Component

packages/Clock/composer.json

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"name": "aether/clock",
3+
"description": "The Aether clock package.",
4+
"license": "MIT",
5+
"type": "library",
6+
"support": {
7+
"issues": "https://github.com/sxbrsky/aether/issues",
8+
"source": "https://github.com/sxbrsky/aether"
9+
},
10+
"require": {
11+
"php": "^8.2",
12+
"psr/clock": "^1.0"
13+
},
14+
"provide": {
15+
"psr/clock-implementation": "*"
16+
},
17+
"minimum-stability": "dev",
18+
"autoload": {
19+
"psr-4": {
20+
"Aether\\Clock\\": "src/"
21+
}
22+
},
23+
"autoload-dev": {
24+
"psr-4": {
25+
"Aether\\Tests\\Clock\\": "tests/"
26+
}
27+
},
28+
"config": {
29+
"sort-packages": true
30+
}
31+
}

packages/Clock/src/Clock.php

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the aether/aether.
5+
*
6+
* Copyright (C) 2024 Dominik Szamburski
7+
*
8+
* This software may be modified and distributed under the terms
9+
* of the MIT license. See the LICENSE file for details.
10+
*/
11+
12+
namespace Aether\Clock;
13+
14+
use Psr\Clock\ClockInterface;
15+
16+
interface Clock extends ClockInterface, \Stringable
17+
{
18+
/**
19+
* Default timezone.
20+
*/
21+
public const DEFAULT_TIMEZONE = 'UTC';
22+
23+
/**
24+
* Returns the current time as a DateTimeImmutable Object
25+
*
26+
* @throws \DateMalformedStringException When an invalid datetime string is detected.
27+
*/
28+
public function now(): \DateTimeImmutable;
29+
30+
/**
31+
* Return an instance with the specified timezone.
32+
*
33+
* @param \DateTimeZone|non-empty-string $timezone A timezone.
34+
* @return static
35+
* @throws \DateInvalidTimeZoneException When $timezone is invalid.
36+
*/
37+
public function withTimezone(\DateTimeZone|string $timezone): static;
38+
39+
/**
40+
* Delays the program execution for the given number of seconds.
41+
*
42+
* @param int|float $seconds Halt time in seconds or microseconds.
43+
* @return void
44+
*/
45+
public function sleep(int|float $seconds): void;
46+
}

packages/Clock/src/FrozenClock.php

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the aether/aether.
5+
*
6+
* Copyright (C) 2024 Dominik Szamburski
7+
*
8+
* This software may be modified and distributed under the terms
9+
* of the MIT license. See the LICENSE file for details.
10+
*/
11+
12+
namespace Aether\Clock;
13+
14+
class FrozenClock implements Clock
15+
{
16+
private \DateTimeImmutable $datetime;
17+
18+
/**
19+
* @param \DateTimeImmutable|non-empty-string $datetime
20+
* @param \DateTimeZone|string $timezone
21+
*
22+
* @throws \DateInvalidTimeZoneException When $timezone is invalid.
23+
* @throws \DateMalformedStringException When $datetime is invalid.
24+
*/
25+
public function __construct(
26+
\DateTimeImmutable|string $datetime = 'now',
27+
\DateTimeZone|string $timezone = Clock::DEFAULT_TIMEZONE
28+
) {
29+
if (\is_string($timezone)) {
30+
$timezone = new \DateTimeZone($timezone);
31+
}
32+
33+
if (\is_string($datetime)) {
34+
$datetime = new \DateTimeImmutable($datetime);
35+
}
36+
37+
$this->datetime = $datetime->setTimezone($timezone);
38+
}
39+
40+
/**
41+
* {@inheritDoc}
42+
*/
43+
public function __toString(): string
44+
{
45+
return \sprintf(
46+
'%s (%s)',
47+
$this->now()->format(\DateTimeInterface::ISO8601_EXPANDED),
48+
$this->now()->getTimezone()->getName()
49+
);
50+
}
51+
52+
/**
53+
* {@inheritDoc}
54+
*/
55+
#[\Override]
56+
public function now(): \DateTimeImmutable
57+
{
58+
return $this->datetime;
59+
}
60+
61+
/**
62+
* {@inheritDoc}
63+
*/
64+
public function withTimezone(\DateTimeZone|string $timezone): static
65+
{
66+
if (\is_string($timezone)) {
67+
$timezone = new \DateTimeZone($timezone);
68+
}
69+
70+
$self = clone $this;
71+
$self->datetime = $self->datetime->setTimezone($timezone);
72+
73+
return $self;
74+
}
75+
76+
/**
77+
* {@inheritDoc}
78+
*/
79+
public function sleep(float|int $seconds): void
80+
{
81+
$wholeSeconds = \floor($seconds);
82+
$microSeconds = \round(($seconds - $wholeSeconds) * 1E6);
83+
84+
if ($seconds > 0) {
85+
if (($dt = $this->datetime->modify("$wholeSeconds second")) !== false) {
86+
$this->datetime = $dt;
87+
}
88+
}
89+
90+
if ($microSeconds > 0) {
91+
if (($dt = $this->datetime->modify("$microSeconds microsecond")) !== false) {
92+
$this->datetime = $dt;
93+
}
94+
}
95+
}
96+
}

packages/Clock/src/SystemClock.php

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the aether/aether.
5+
*
6+
* Copyright (C) 2024 Dominik Szamburski
7+
*
8+
* This software may be modified and distributed under the terms
9+
* of the MIT license. See the LICENSE file for details.
10+
*/
11+
12+
namespace Aether\Clock;
13+
14+
use DateTimeImmutable;
15+
16+
class SystemClock implements Clock
17+
{
18+
private \DateTimeZone $timezone;
19+
20+
/**
21+
* @param \DateTimeZone|non-empty-string $timezone
22+
* @throws \DateInvalidTimeZoneException
23+
*/
24+
public function __construct(\DateTimeZone|string $timezone = Clock::DEFAULT_TIMEZONE)
25+
{
26+
$this->timezone = \is_string($timezone) ? $this->withTimezone($timezone)->timezone : $timezone;
27+
}
28+
29+
/**
30+
* {@inheritDoc}
31+
*/
32+
public function __toString(): string
33+
{
34+
return \sprintf(
35+
'%s (%s)',
36+
$this->now()->format(\DateTimeInterface::ISO8601_EXPANDED),
37+
$this->now()->getTimezone()->getName()
38+
);
39+
}
40+
41+
/**
42+
* {@inheritDoc}
43+
*/
44+
public function now(): DateTimeImmutable
45+
{
46+
return new \DateTimeImmutable('now', $this->timezone);
47+
}
48+
49+
/**
50+
* {@inheritDoc}
51+
*/
52+
public function withTimezone(\DateTimeZone|string $timezone): static
53+
{
54+
if (\is_string($timezone)) {
55+
try {
56+
$timezone = new \DateTimeZone($timezone);
57+
} catch (\Exception $e) {
58+
throw new \DateInvalidTimeZoneException($e->getMessage(), (int) $e->getCode(), $e);
59+
}
60+
}
61+
62+
$clone = clone $this;
63+
$clone->timezone = $timezone;
64+
65+
return $clone;
66+
}
67+
68+
/**
69+
* {@inheritDoc}
70+
*/
71+
public function sleep(float|int $seconds): void
72+
{
73+
if (0 < $s = (int) $seconds) {
74+
\sleep($s);
75+
}
76+
77+
if (0 < $us = $seconds - $s) {
78+
\usleep((int) ($us * 1E6));
79+
}
80+
}
81+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the sxbrsky/clock.
5+
*
6+
* Copyright (C) 2024 Dominik Szamburski
7+
*
8+
* This software may be modified and distributed under the terms
9+
* of the MIT license. See the LICENSE file for details.
10+
*/
11+
12+
namespace Aether\Tests\Clock;
13+
14+
use Aether\Clock\Clock;
15+
use Aether\Clock\FrozenClock;
16+
use PHPUnit\Framework\Attributes\CoversClass;
17+
use PHPUnit\Framework\TestCase;
18+
use Psr\Clock\ClockInterface;
19+
20+
#[CoversClass(FrozenClock::class)]
21+
22+
class FrozenClockTest extends TestCase
23+
{
24+
public function testInstanceOfClockInterface(): void
25+
{
26+
$clock = new FrozenClock();
27+
28+
self::assertInstanceOf(Clock::class, $clock);
29+
self::assertInstanceOf(ClockInterface::class, $clock);
30+
}
31+
32+
public function testWithTimezone(): void
33+
{
34+
$clock = new FrozenClock();
35+
$newClock = $clock->withTimezone(new \DateTimeZone('Europe/Warsaw'));
36+
37+
self::assertNotSame($newClock, $clock);
38+
self::assertSame('Europe/Warsaw', $newClock->now()->getTimezone()->getName());
39+
}
40+
41+
public function testTimeDoesNotChange(): void
42+
{
43+
$clock = new FrozenClock();
44+
45+
$first = $clock->now()->format('U.u');
46+
\usleep(10);
47+
$second = $clock->now()->format('U.u');
48+
49+
self::assertSame($first, $second);
50+
}
51+
52+
public function testSleep(): void
53+
{
54+
$clock = new FrozenClock('2024-01-27 23:53:00.999Z');
55+
$clock->sleep(2.002001);
56+
57+
self::assertSame(
58+
'2024-01-27 23:53:03.001001',
59+
$clock->now()->format('Y-m-d H:i:s.u')
60+
);
61+
}
62+
}

0 commit comments

Comments
 (0)