diff --git a/CHANGELOG.md b/CHANGELOG.md index 2505e97..49496af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ - `Innmind\TimeContinuum\Clock::ofFormat()` - `Innmind\TimeContinuum\Offset::plus()` - `Innmind\TimeContinuum\Offset::minus()` +- `Innmind\TimeContinuum\ElapsedPeriod::asPeriod()` +- `Innmind\TimeContinuum\Period\Value::seconds()` ### Changed @@ -72,6 +74,9 @@ - `Innmind\TimeContinuum\ElapsedPeriod::ofPeriod()` - `Innmind\TimeContinuum\PointInTime::milliseconds()` - `Innmind\TimeContinuum\Offset::of()` +- `Innmind\TimeContinuum\ElapsedPeriod::milliseconds()` +- `Innmind\TimeContinuum\Period\Value::second` +- `Innmind\TimeContinuum\Period\Value::milliseconds()` ## 3.4.1 - 2023-09-17 diff --git a/blackbox.php b/blackbox.php index 07580d3..86b7986 100644 --- a/blackbox.php +++ b/blackbox.php @@ -23,6 +23,7 @@ ->dumpTo('coverage.clover') ->enableWhen(true), ) + ->scenariiPerProof(1_000), ) ->tryToProve(static function() { yield from Load::everythingIn(__DIR__.'/proofs/')(); diff --git a/proofs/elapsedPeriod.php b/proofs/elapsedPeriod.php new file mode 100644 index 0000000..8801273 --- /dev/null +++ b/proofs/elapsedPeriod.php @@ -0,0 +1,149 @@ +filter(static fn($start, $end) => $end > $start), + static function( + $assert, + $startSeconds, + $endSeconds, + $startNanoseconds, + $endNanoseconds, + ) { + $start = HighResolution::of($startSeconds, $startNanoseconds); + $end = HighResolution::of($endSeconds, $endNanoseconds); + + $assert->true( + $start + ->elapsedSince($start) + ->equals( + Period::microsecond(0)->asElapsedPeriod(), + ), + ); + $assert->true( + $end + ->elapsedSince($start) + ->longerThan( + Period::microsecond(1)->asElapsedPeriod(), + ), + ); + }, + ); + + yield proof( + 'High resolution elapsed period within same second', + given( + Set\Integers::between(0, 999_999_999), + Set\Integers::between(0, 999_999_999), + )->filter(static fn($start, $end) => $end > $start), + static function( + $assert, + $startNanoseconds, + $endNanoseconds, + ) { + $start = HighResolution::of(0, $startNanoseconds); + $end = HighResolution::of(0, $endNanoseconds); + + $assert->true( + $start + ->elapsedSince($start) + ->equals( + Period::microsecond(0)->asElapsedPeriod(), + ), + ); + $assert->true( + $end + ->elapsedSince($start) + ->longerThan( + Period::microsecond(1)->asElapsedPeriod(), + ), + ); + }, + ); + + yield proof( + 'Elapsed period', + given( + Fixtures\PointInTime::any(), + Set\Integers::above(1), + ), + static function($assert, $start, $microsecond) { + $assert->true( + $start + ->elapsedSince($start) + ->equals( + Period::microsecond(0)->asElapsedPeriod(), + ), + ); + $assert->true( + $start + ->goForward(Period::microsecond($microsecond)) + ->elapsedSince($start) + ->equals( + Period::microsecond($microsecond)->asElapsedPeriod(), + ), + ); + $assert->true( + $start + ->goForward(Period::microsecond($microsecond)) + ->elapsedSince($start) + ->longerThan( + Period::microsecond(0)->asElapsedPeriod(), + ), + ); + }, + ); + + yield proof( + 'Negative elapsed periods throws', + given( + Fixtures\PointInTime::any(), + Set\Integers::above(1), + ), + static function($assert, $start, $microsecond) { + $assert->throws( + static fn() => $start + ->goBack(Period::microsecond($microsecond)) + ->elapsedSince($start), + ); + }, + ); + + yield proof( + 'Negative high resolution elapsed periods throws', + given( + Set\Integers::above(0), + Set\Integers::above(0), + Set\Integers::between(0, 999_999_999), + Set\Integers::between(0, 999_999_999), + )->filter(static fn($start, $end) => $end > $start), + static function( + $assert, + $startSeconds, + $endSeconds, + $startNanoseconds, + $endNanoseconds, + ) { + $start = HighResolution::of($startSeconds, $startNanoseconds); + $end = HighResolution::of($endSeconds, $endNanoseconds); + + $assert->throws( + static fn() => $start->elapsedSince($end), + ); + }, + ); +}; diff --git a/src/ElapsedPeriod.php b/src/ElapsedPeriod.php index cacb0af..204408f 100644 --- a/src/ElapsedPeriod.php +++ b/src/ElapsedPeriod.php @@ -9,14 +9,24 @@ final class ElapsedPeriod { /** @var int<0, max> */ + private int $seconds; + /** @var int<0, 999> */ + private int $milliseconds; + /** @var int<0, 999> */ private int $microseconds; - private function __construct(int $microseconds) - { - if ($microseconds < 0) { - throw new \RuntimeException((string) $microseconds); - } - + /** + * @param int<0, max> $seconds + * @param int<0, 999> $milliseconds + * @param int<0, 999> $microseconds + */ + private function __construct( + int $seconds, + int $milliseconds, + int $microseconds, + ) { + $this->seconds = $seconds; + $this->milliseconds = $milliseconds; $this->microseconds = $microseconds; } @@ -24,37 +34,42 @@ private function __construct(int $microseconds) * @psalm-pure * @internal * - * @throws \RuntimeException + * @param int<0, max> $seconds + * @param int<0, 999> $milliseconds + * @param int<0, 999> $microseconds */ - public static function of(int $microseconds): self - { - return new self($microseconds); + public static function of( + int $seconds, + int $milliseconds, + int $microseconds, + ): self { + return new self($seconds, $milliseconds, $microseconds); } - /** - * @return int<0, max> - */ - public function milliseconds(): int + public function longerThan(self $period): bool { - /** @var int<0, max> */ - return (int) ($this->microseconds / 1_000); - } + if ($this->seconds > $period->seconds) { + return true; + } - /** - * @return int<0, max> - */ - public function microseconds(): int - { - return $this->microseconds; - } + if ($this->milliseconds > $period->milliseconds) { + return true; + } - public function longerThan(self $period): bool - { return $this->microseconds > $period->microseconds; } public function equals(self $period): bool { - return $this->microseconds === $period->microseconds; + return $this->seconds === $period->seconds && + $this->milliseconds === $period->milliseconds && + $this->microseconds === $period->microseconds; + } + + public function asPeriod(): Period + { + return Period::second($this->seconds) + ->add(Period::millisecond($this->milliseconds)) + ->add(Period::microsecond($this->microseconds)); } } diff --git a/src/Period.php b/src/Period.php index d757f1d..bdf9091 100644 --- a/src/Period.php +++ b/src/Period.php @@ -458,13 +458,15 @@ public function asElapsedPeriod(): ElapsedPeriod throw new \LogicException('Months and years can not be converted to microseconds'); } - $milliseconds = Period\Value::day->milliseconds($this->days()) + - Period\Value::hour->milliseconds($this->hours()) + - Period\Value::minute->milliseconds($this->minutes()) + - Period\Value::second->milliseconds($this->seconds()) + - $this->milliseconds(); - $milliseconds *= 1_000; - - return ElapsedPeriod::of($milliseconds + $this->microseconds()); + $seconds = Period\Value::day->seconds($this->days()) + + Period\Value::hour->seconds($this->hours()) + + Period\Value::minute->seconds($this->minutes()) + + $this->second; + + return ElapsedPeriod::of( + $seconds, + $this->millisecond, + $this->microsecond, + ); } } diff --git a/src/Period/Value.php b/src/Period/Value.php index 123c504..00b9530 100644 --- a/src/Period/Value.php +++ b/src/Period/Value.php @@ -8,22 +8,24 @@ */ enum Value { - case second; case minute; case hour; case day; /** - * Returns the number of milliseconds contained in the number of seconds, - * minutes, hours and days + * Returns the number of seconds contained in the number of minutes, hours + * and days + * + * @param int<0, max> $number + * + * @return int<0, max> */ - public function milliseconds(int $number): int + public function seconds(int $number): int { return match ($this) { - self::second => $number * 1000, - self::minute => $number * self::second->milliseconds(60), - self::hour => $number * self::minute->milliseconds(60), - self::day => $number * self::hour->milliseconds(24), + self::minute => $number * 60, + self::hour => $number * self::minute->seconds(60), + self::day => $number * self::hour->seconds(24), }; } } diff --git a/src/PointInTime.php b/src/PointInTime.php index 64dccfc..acfecf2 100644 --- a/src/PointInTime.php +++ b/src/PointInTime.php @@ -143,14 +143,44 @@ public function elapsedSince(self $point): ElapsedPeriod } $seconds = ((int) $this->date->format('U')) - ((int) $point->date->format('U')); - $milliseconds = $seconds * 1_000; - $milliseconds += $this->millisecond()->toInt(); - $milliseconds -= $point->millisecond()->toInt(); - $microseconds = $milliseconds * 1_000; - $microseconds += $this->microsecond()->toInt(); - $microseconds -= $point->microsecond()->toInt(); - - return ElapsedPeriod::of($microseconds); + $milliseconds = $this->millisecond()->toInt() - $point->millisecond()->toInt(); + $microseconds = $this->microsecond()->toInt() - $point->microsecond()->toInt(); + + if ($milliseconds < 0) { + $seconds -= 1; + $milliseconds += 1_000; + } + + if ($microseconds < 0) { + $milliseconds -= 1; + $microseconds += 1_000; + } + + if ($milliseconds < 0) { + // This handles the case where any second diff is positive, but zero + // milliseconds and any microsecond diff. + // Duplication could be avoided by switching the 2 previous if but + // it would require to compute the number of seconds to subtract. + // The duplication seems more obvious to understand (at least for + // now). + $seconds -= 1; + $milliseconds += 1_000; + } + + if ($seconds < 0) { + throw new \RuntimeException(\sprintf( + 'Negative period : %ss, %smillis, %smicros', + $seconds, + $milliseconds, + $microseconds, + )); + } + + return ElapsedPeriod::of( + $seconds, + $milliseconds, + $microseconds, + ); } public function goBack(Period $period): self diff --git a/src/PointInTime/HighResolution.php b/src/PointInTime/HighResolution.php index 7b20455..6868c25 100644 --- a/src/PointInTime/HighResolution.php +++ b/src/PointInTime/HighResolution.php @@ -13,7 +13,7 @@ final class HighResolution { /** * @param int<0, max> $seconds - * @param int<0, max> $nanoseconds + * @param int<0, 999_999_999> $nanoseconds */ private function __construct( private int $seconds, @@ -28,7 +28,7 @@ public static function now(): self { /** * @var int<0, max> $seconds - * @var int<0, max> $nanoseconds + * @var int<0, 999_999_999> $nanoseconds */ [$seconds, $nanoseconds] = \hrtime(); @@ -39,20 +39,41 @@ public static function now(): self * @internal * * @param int<0, max> $seconds - * @param int<0, max> $nanoseconds + * @param int<0, 999_999_999> $nanoseconds */ public static function of(int $seconds, int $nanoseconds): self { return new self($seconds, $nanoseconds); } - public function elapsedSince(self $time): ElapsedPeriod + public function elapsedSince(self $other): ElapsedPeriod { - $seconds = $this->seconds - $time->seconds; - $nanoseconds = $this->nanoseconds - $time->nanoseconds; + $seconds = $this->seconds - $other->seconds; + $nanoseconds = $this->nanoseconds - $other->nanoseconds; - $microseconds = ($seconds * 1_000_000) + (int) ($nanoseconds / 1_000); + if ($nanoseconds < 0) { + $seconds -= 1; + $nanoseconds += 1_000_000_000; + } - return ElapsedPeriod::of($microseconds); + /** @var int<0, 999> */ + $microseconds = ((int) ($nanoseconds / 1_000)) % 1_000; + /** @var int<0, 999> */ + $milliseconds = ((int) ($nanoseconds / 1_000_000)) % 1_000; + + if ($seconds < 0) { + throw new \RuntimeException(\sprintf( + 'Negative period : %ss, %smillis, %smicros', + $seconds, + $milliseconds, + $microseconds, + )); + } + + return ElapsedPeriod::of( + $seconds, + $milliseconds, + $microseconds, + ); } } diff --git a/tests/ElapsedPeriodTest.php b/tests/ElapsedPeriodTest.php index cef8933..8506c45 100644 --- a/tests/ElapsedPeriodTest.php +++ b/tests/ElapsedPeriodTest.php @@ -13,35 +13,41 @@ class ElapsedPeriodTest extends TestCase { public function testInterface() { - $period = ElapsedPeriod::of(42); + $period = ElapsedPeriod::of(0, 0, 42); - $this->assertSame(0, $period->milliseconds()); - $this->assertSame(42, $period->microseconds()); + $this->assertTrue( + $period->equals( + Period::microsecond(42)->asElapsedPeriod(), + ), + ); - $period = ElapsedPeriod::of(42_000); + $period = ElapsedPeriod::of(0, 42, 0); - $this->assertSame(42, $period->milliseconds()); - $this->assertSame(42_000, $period->microseconds()); - } + $this->assertTrue( + $period->equals( + Period::millisecond(42)->asElapsedPeriod(), + ), + ); - public function testThrowWhenTryingToBuildANegativePeriod() - { - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage('-42'); + $period = ElapsedPeriod::of(42, 0, 0); - ElapsedPeriod::of(-42); + $this->assertTrue( + $period->equals( + Period::second(42)->asElapsedPeriod(), + ), + ); } public function testLongerThan() { $this->assertTrue( - ElapsedPeriod::of(42)->longerThan( - ElapsedPeriod::of(0), + ElapsedPeriod::of(0, 0, 42)->longerThan( + ElapsedPeriod::of(0, 0, 0), ), ); $this->assertFalse( - ElapsedPeriod::of(42)->longerThan( - ElapsedPeriod::of(66), + ElapsedPeriod::of(0, 0, 42)->longerThan( + ElapsedPeriod::of(0, 0, 66), ), ); } @@ -49,13 +55,13 @@ public function testLongerThan() public function testEquals() { $this->assertTrue( - ElapsedPeriod::of(42)->equals( - ElapsedPeriod::of(42), + ElapsedPeriod::of(0, 0, 42)->equals( + ElapsedPeriod::of(0, 0, 42), ), ); $this->assertFalse( - ElapsedPeriod::of(42)->equals( - ElapsedPeriod::of(66), + ElapsedPeriod::of(0, 0, 42)->equals( + ElapsedPeriod::of(0, 0, 66), ), ); } diff --git a/tests/NowTest.php b/tests/NowTest.php index 7cc7d22..cde7529 100644 --- a/tests/NowTest.php +++ b/tests/NowTest.php @@ -95,7 +95,11 @@ public function testElapsedSince() $this->assertInstanceOf(ElapsedPeriod::class, $elapsed); //make sure there's at least 1 second elapsed due to the sleep() - $this->assertTrue(1000 <= $elapsed->milliseconds()); + $this->assertTrue( + $elapsed->longerThan( + Period::second(1)->asElapsedPeriod(), + ), + ); } public function testAheadOf() diff --git a/tests/Period/DayTest.php b/tests/Period/DayTest.php index 38552b2..c862ae4 100644 --- a/tests/Period/DayTest.php +++ b/tests/Period/DayTest.php @@ -54,16 +54,16 @@ public function testAdd() public function testAsElapsedPeriod() { $this->assertSame( - 86_400_000, - Period::day(1)->asElapsedPeriod()->milliseconds(), + 1, + Period::day(1)->asElapsedPeriod()->asPeriod()->days(), ); $this->assertSame( - 172_800_000, - Period::day(2)->asElapsedPeriod()->milliseconds(), + 2, + Period::day(2)->asElapsedPeriod()->asPeriod()->days(), ); $this->assertSame( - 259_200_000, - Period::day(3)->asElapsedPeriod()->milliseconds(), + 3, + Period::day(3)->asElapsedPeriod()->asPeriod()->days(), ); } } diff --git a/tests/Period/HourTest.php b/tests/Period/HourTest.php index d0dd38e..a0b6efa 100644 --- a/tests/Period/HourTest.php +++ b/tests/Period/HourTest.php @@ -68,16 +68,16 @@ public function testAdd() public function testAsElapsedPeriod() { $this->assertSame( - 3_600_000, - Period::hour(1)->asElapsedPeriod()->milliseconds(), + 1, + Period::hour(1)->asElapsedPeriod()->asPeriod()->hours(), ); $this->assertSame( - 7_200_000, - Period::hour(2)->asElapsedPeriod()->milliseconds(), + 2, + Period::hour(2)->asElapsedPeriod()->asPeriod()->hours(), ); $this->assertSame( - 10_800_000, - Period::hour(3)->asElapsedPeriod()->milliseconds(), + 3, + Period::hour(3)->asElapsedPeriod()->asPeriod()->hours(), ); } diff --git a/tests/Period/MillisecondTest.php b/tests/Period/MillisecondTest.php index a9e9363..2875fad 100644 --- a/tests/Period/MillisecondTest.php +++ b/tests/Period/MillisecondTest.php @@ -75,15 +75,15 @@ public function testAsElapsedPeriod() { $this->assertSame( 1, - Period::millisecond(1)->asElapsedPeriod()->milliseconds(), + Period::millisecond(1)->asElapsedPeriod()->asPeriod()->milliseconds(), ); $this->assertSame( 2, - Period::millisecond(2)->asElapsedPeriod()->milliseconds(), + Period::millisecond(2)->asElapsedPeriod()->asPeriod()->milliseconds(), ); $this->assertSame( 3, - Period::millisecond(3)->asElapsedPeriod()->milliseconds(), + Period::millisecond(3)->asElapsedPeriod()->asPeriod()->milliseconds(), ); } diff --git a/tests/Period/MinuteTest.php b/tests/Period/MinuteTest.php index c6baa6c..debdcbd 100644 --- a/tests/Period/MinuteTest.php +++ b/tests/Period/MinuteTest.php @@ -72,16 +72,16 @@ public function testAdd() public function testAsElapsedPeriod() { $this->assertSame( - 60_000, - Period::minute(1)->asElapsedPeriod()->milliseconds(), + 1, + Period::minute(1)->asElapsedPeriod()->asPeriod()->minutes(), ); $this->assertSame( - 120_000, - Period::minute(2)->asElapsedPeriod()->milliseconds(), + 2, + Period::minute(2)->asElapsedPeriod()->asPeriod()->minutes(), ); $this->assertSame( - 180_000, - Period::minute(3)->asElapsedPeriod()->milliseconds(), + 3, + Period::minute(3)->asElapsedPeriod()->asPeriod()->minutes(), ); } diff --git a/tests/Period/SecondTest.php b/tests/Period/SecondTest.php index ddcda7c..510610e 100644 --- a/tests/Period/SecondTest.php +++ b/tests/Period/SecondTest.php @@ -73,16 +73,16 @@ public function testAdd() public function testAsElapsedPeriod() { $this->assertSame( - 1000, - Period::second(1)->asElapsedPeriod()->milliseconds(), + 1, + Period::second(1)->asElapsedPeriod()->asPeriod()->seconds(), ); $this->assertSame( - 2000, - Period::second(2)->asElapsedPeriod()->milliseconds(), + 2, + Period::second(2)->asElapsedPeriod()->asPeriod()->seconds(), ); $this->assertSame( - 3000, - Period::second(3)->asElapsedPeriod()->milliseconds(), + 3, + Period::second(3)->asElapsedPeriod()->asPeriod()->seconds(), ); } diff --git a/tests/PointInTime/HighResolutionTest.php b/tests/PointInTime/HighResolutionTest.php index 3a29d0c..23cc0cc 100644 --- a/tests/PointInTime/HighResolutionTest.php +++ b/tests/PointInTime/HighResolutionTest.php @@ -13,7 +13,10 @@ public function testDiffWithinSameSecond() $started = HighResolution::of(1, 1_000_000); $end = HighResolution::of(1, 2_000_000); - $this->assertSame(1, $end->elapsedSince($started)->milliseconds()); + $period = $end->elapsedSince($started)->asPeriod(); + $this->assertSame(0, $period->seconds()); + $this->assertSame(1, $period->milliseconds()); + $this->assertSame(0, $period->microseconds()); } public function testDiffLessThanOneSecondButNotInSameSecond() @@ -21,7 +24,10 @@ public function testDiffLessThanOneSecondButNotInSameSecond() $started = HighResolution::of(1, 2_000_000); $end = HighResolution::of(2, 1_000_000); - $this->assertSame(999, $end->elapsedSince($started)->milliseconds()); + $period = $end->elapsedSince($started)->asPeriod(); + $this->assertSame(0, $period->seconds()); + $this->assertSame(999, $period->milliseconds()); + $this->assertSame(0, $period->microseconds()); } public function testDiffMoreThanOneSecond() @@ -29,7 +35,10 @@ public function testDiffMoreThanOneSecond() $started = HighResolution::of(1, 2_000_000); $end = HighResolution::of(2, 3_000_000); - $this->assertSame(1001, $end->elapsedSince($started)->milliseconds()); + $period = $end->elapsedSince($started)->asPeriod(); + $this->assertSame(1, $period->seconds()); + $this->assertSame(1, $period->milliseconds()); + $this->assertSame(0, $period->microseconds()); } public function testDiffMoreThanMultipleSeconds() @@ -37,7 +46,10 @@ public function testDiffMoreThanMultipleSeconds() $started = HighResolution::of(1, 2_000_000); $end = HighResolution::of(3, 3_000_000); - $this->assertSame(2001, $end->elapsedSince($started)->milliseconds()); + $period = $end->elapsedSince($started)->asPeriod(); + $this->assertSame(2, $period->seconds()); + $this->assertSame(1, $period->milliseconds()); + $this->assertSame(0, $period->microseconds()); } public function testDiffMoreThanMultipleSeconds2() @@ -45,7 +57,10 @@ public function testDiffMoreThanMultipleSeconds2() $started = HighResolution::of(1, 2_000_000); $end = HighResolution::of(3, 1_000_000); - $this->assertSame(1999, $end->elapsedSince($started)->milliseconds()); + $period = $end->elapsedSince($started)->asPeriod(); + $this->assertSame(1, $period->seconds()); + $this->assertSame(999, $period->milliseconds()); + $this->assertSame(0, $period->microseconds()); } public function test1() @@ -53,7 +68,8 @@ public function test1() $started = HighResolution::of(684461, 547614375); $end = HighResolution::of(684462, 602541); - $this->assertSame(452, $end->elapsedSince($started)->milliseconds()); - $this->assertSame(452989, $end->elapsedSince($started)->microseconds()); + $period = $end->elapsedSince($started)->asPeriod(); + $this->assertSame(452, $period->milliseconds()); + $this->assertSame(988, $period->microseconds()); } } diff --git a/tests/PointInTimeTest.php b/tests/PointInTimeTest.php index de06caa..ad53c6a 100644 --- a/tests/PointInTimeTest.php +++ b/tests/PointInTimeTest.php @@ -7,7 +7,6 @@ PointInTime, Offset, Format, - ElapsedPeriod, PointInTime\Year, PointInTime\Month, PointInTime\Day, @@ -89,10 +88,12 @@ public function testElapsedSince() { $point = PointInTime::at(new \DateTimeImmutable('2016-10-05T08:01:30.123+02:00')); $point2 = PointInTime::at(new \DateTimeImmutable('2016-10-05T08:03:30.234+02:00')); - $elapsed = $point2->elapsedSince($point); + $elapsed = $point2->elapsedSince($point)->asPeriod(); - $this->assertInstanceOf(ElapsedPeriod::class, $elapsed); - $this->assertSame(120111, $elapsed->milliseconds()); + $this->assertSame(2, $elapsed->minutes()); + $this->assertSame(0, $elapsed->seconds()); + $this->assertSame(111, $elapsed->milliseconds()); + $this->assertSame(0, $elapsed->microseconds()); } public function testAheadOf()