diff --git a/src/ChronosTime.php b/src/ChronosTime.php new file mode 100644 index 00000000..67135ccb --- /dev/null +++ b/src/ChronosTime.php @@ -0,0 +1,275 @@ +ticks = 0; + } elseif ($time instanceof ChronosTime) { + $this->ticks = $time->ticks; + } else { + $this->ticks = static::parseTime(is_string($time) ? $time : $time->format('H:i:s.u')); + } + } + + /** + * @param string $time Time string in the format HH[:.]mm or HH[:.]mm[:.]ss.u + * @return int + */ + protected static function parseTime(string $time): int + { + if (!preg_match('/^\s*(\d{1,2})[:.](\d{1,2})(?|[:.](\d{1,2})[.](\d+)|[:.](\d{1,2}))?\s*$/', $time, $matches)) { + throw new InvalidArgumentException( + 'Time string is not in expected format: "HH[:.]mm" or "HH[:.]mm[:.]ss.u".' + ); + } + + $hours = (int)$matches[1]; + $minutes = (int)$matches[2]; + $seconds = (int)($matches[3] ?? 0); + $microseconds = (int)substr($matches[4] ?? '', 0, 6); + + if ($hours > 23 || $minutes > 59 || $seconds > 59 || $microseconds > 999_999) { + throw new InvalidArgumentException('Time string contains invalid values.'); + } + + $ticks = $hours * self::TICKS_PER_HOUR; + $ticks += $minutes * self::TICKS_PER_MINUTE; + $ticks += $seconds * self::TICKS_PER_SECOND; + $ticks += $microseconds * self::TICKS_PER_MICROSECOND; + + return $ticks; + } + + /** + * Returns clock microseconds. + * + * @return int + */ + public function getMicroseconds(): int + { + return intdiv($this->ticks % self::TICKS_PER_SECOND, self::TICKS_PER_MICROSECOND); + } + + /** + * Sets clock microseconds. + * + * @param int $microseconds Clock microseconds + * @return static + */ + public function setMicroseconds(int $microseconds): static + { + $clone = clone $this; + + $prevTicks = $this->ticks % self::TICKS_PER_SECOND; + $newTicks = static::mod($microseconds, 1_000_000) * self::TICKS_PER_MICROSECOND; + + $clone->ticks = $this->ticks - $prevTicks + $newTicks; + + return $clone; + } + + /** + * Return clock seconds. + * + * @return int + */ + public function getSeconds(): int + { + $secondsTicks = $this->ticks % self::TICKS_PER_MINUTE - $this->ticks % self::TICKS_PER_SECOND; + + return intdiv($secondsTicks, self::TICKS_PER_SECOND); + } + + /** + * Set clock seconds. + * + * @param int $seconds Clock seconds + * @return static + */ + public function setSeconds(int $seconds): static + { + $prevSecondsTicks = $this->ticks % self::TICKS_PER_MINUTE - $this->ticks % self::TICKS_PER_SECOND; + $newSecondsTicks = static::mod($seconds, 60) * self::TICKS_PER_SECOND; + + $clone = clone $this; + $clone->ticks = $this->ticks - $prevSecondsTicks + $newSecondsTicks; + + return $clone; + } + + /** + * Returns clock minutes. + * + * @return int + */ + public function getMinutes(): int + { + $minutesTicks = $this->ticks % self::TICKS_PER_HOUR - $this->ticks % self::TICKS_PER_MINUTE; + + return intdiv($minutesTicks, self::TICKS_PER_MINUTE); + } + + /** + * Set clock minutes. + * + * @param int $minutes Clock minutes + * @return static + */ + public function setMinutes(int $minutes): static + { + $prevMinutesTicks = $this->ticks % self::TICKS_PER_HOUR - $this->ticks % self::TICKS_PER_MINUTE; + $newMinutesTicks = static::mod($minutes, 60) * self::TICKS_PER_MINUTE; + + $clone = clone $this; + $clone->ticks = $this->ticks - $prevMinutesTicks + $newMinutesTicks; + + return $clone; + } + + /** + * Returns clock hours. + * + * @return int + */ + public function getHours(): int + { + $hoursInTicks = $this->ticks - $this->ticks % self::TICKS_PER_HOUR; + + return intdiv($hoursInTicks, self::TICKS_PER_HOUR); + } + + /** + * Set clock hours. + * + * @param int $hours Clock hours + * @return static + */ + public function setHours(int $hours): static + { + $prevHoursTicks = $this->ticks - $this->ticks % self::TICKS_PER_HOUR; + $newHoursTicks = static::mod($hours, 24) * self::TICKS_PER_HOUR; + + $clone = clone $this; + $clone->ticks = $this->ticks - $prevHoursTicks + $newHoursTicks; + + return $clone; + } + + /** + * Sets clock time. + * + * @param int $hours Clock hours + * @param int $minutes Clock minutes + * @param int $seconds Clock seconds + * @param int $microseconds Clock microseconds + * @return static + */ + public function setTime(int $hours = 0, int $minutes = 0, int $seconds = 0, int $microseconds = 0): static + { + $clone = clone $this; + $clone->ticks = static::mod($hours, 24) * self::TICKS_PER_HOUR + + static::mod($minutes, 60) * self::TICKS_PER_MINUTE + + static::mod($seconds, 60) * self::TICKS_PER_SECOND + + static::mod($microseconds, 1_000_000) * self::TICKS_PER_MICROSECOND; + + return $clone; + } + + /** + * @param int $a Left side + * @param int $a Right side + * @return int + */ + protected static function mod(int $a, int $b): int + { + if ($a < 0) { + return $a % $b + $b; + } + + return $a % $b; + } + + /** + * Formats string using the same syntax as `DateTimeImmutable::format()`. + * + * As this uses DateTimeImmutable::format() to format the string, non-time formatters + * will still be interpreted. Be sure to escape those characters first. + * + * @param string $format Format string + * @return string + */ + public function format(string $format): string + { + return $this->toNative()->format($format); + } + + /** + * Returns an `DateTimeImmutable` instance set to this clock time. + * + * @return \DateTimeImmutable + */ + public function toNative(): DateTimeImmutable + { + return (new DateTimeImmutable())->setTime( + $this->getHours(), + $this->getMinutes(), + $this->getSeconds(), + $this->getMicroseconds() + ); + } +} diff --git a/tests/TestCase/ChronosTimeTest.php b/tests/TestCase/ChronosTimeTest.php new file mode 100644 index 00000000..ba6edfdc --- /dev/null +++ b/tests/TestCase/ChronosTimeTest.php @@ -0,0 +1,177 @@ +assertSame(0, $t->getMicroseconds()); + $this->assertSame(0, $t->getSeconds()); + $this->assertSame(0, $t->getMinutes()); + $this->assertSame(0, $t->getHours()); + + $t = new ChronosTime(null); + $this->assertSame(0, $t->getMicroseconds()); + $this->assertSame(0, $t->getSeconds()); + $this->assertSame(0, $t->getMinutes()); + $this->assertSame(0, $t->getHours()); + } + + public function testConstructFromString(): void + { + $t = new ChronosTime('0.0.0.0'); + $this->assertSame(0, $t->getMicroseconds()); + $this->assertSame(0, $t->getSeconds()); + $this->assertSame(0, $t->getMinutes()); + $this->assertSame(0, $t->getHours()); + + $t = new ChronosTime('1:01:1.000001'); + $this->assertSame(1, $t->getMicroseconds()); + $this->assertSame(1, $t->getSeconds()); + $this->assertSame(1, $t->getMinutes()); + $this->assertSame(1, $t->getHours()); + + $t = new ChronosTime('23:59.59.999999'); + $this->assertSame(999999, $t->getMicroseconds()); + $this->assertSame(59, $t->getSeconds()); + $this->assertSame(59, $t->getMinutes()); + $this->assertSame(23, $t->getHours()); + + $t = new ChronosTime('23:59.59.9999991'); + $this->assertSame(999999, $t->getMicroseconds()); + $this->assertSame(59, $t->getSeconds()); + $this->assertSame(59, $t->getMinutes()); + $this->assertSame(23, $t->getHours()); + + $t = new ChronosTime('12:13'); + $this->assertSame(0, $t->getMicroseconds()); + $this->assertSame(0, $t->getSeconds()); + $this->assertSame(13, $t->getMinutes()); + $this->assertSame(12, $t->getHours()); + } + + public function testConstructFromInstance(): void + { + $t = new ChronosTime(new DateTimeImmutable('23:59:59.999999')); + $this->assertSame(999999, $t->getMicroseconds()); + $this->assertSame(59, $t->getSeconds()); + $this->assertSame(59, $t->getMinutes()); + $this->assertSame(23, $t->getHours()); + + $t = new ChronosTime(new Chronos('23:59:59.999999')); + $this->assertSame(999999, $t->getMicroseconds()); + $this->assertSame(59, $t->getSeconds()); + $this->assertSame(59, $t->getMinutes()); + $this->assertSame(23, $t->getHours()); + + $t = new ChronosTime(new ChronosTime(new Chronos('23:59:59.999999'))); + $this->assertSame(999999, $t->getMicroseconds()); + $this->assertSame(59, $t->getSeconds()); + $this->assertSame(59, $t->getMinutes()); + $this->assertSame(23, $t->getHours()); + } + + public function testConstructInvalid(): void + { + $this->expectException(InvalidArgumentException::class); + new ChronosTime('now'); + } + + public function testConstructIncomplete(): void + { + $this->expectException(InvalidArgumentException::class); + new ChronosTime('23'); + } + + public function testConstructInvalidHours(): void + { + $this->expectException(InvalidArgumentException::class); + new ChronosTime('24:00:00'); + } + + public function testConstructInvalidMinutes(): void + { + $this->expectException(InvalidArgumentException::class); + new ChronosTime('23:60:00'); + } + + public function testConstructInvalidSeconds(): void + { + $this->expectException(InvalidArgumentException::class); + new ChronosTime('23:59:60'); + } + + public function testSetTime(): void + { + $t = new ChronosTime(); + $new = $t->setTime(); + $this->assertNotSame($new, $t); + $this->assertSame(0, $new->getHours()); + $this->assertSame(0, $new->getMinutes()); + $this->assertSame(0, $new->getSeconds()); + $this->assertSame(0, $new->getMicroseconds()); + + $t = new ChronosTime('23:59.59.9999991'); + + $new = $t->setHours(24); + $this->assertSame(0, $new->getHours()); + + $new = $t->setHours(-1); + $this->assertSame(23, $new->getHours()); + + $new = $t->setMinutes(60); + $this->assertSame(0, $new->getMinutes()); + + $new = $t->setMinutes(-1); + $this->assertSame(59, $new->getMinutes()); + + $new = $t->setSeconds(60); + $this->assertSame(0, $new->getSeconds()); + + $new = $t->setSeconds(-1); + $this->assertSame(59, $new->getSeconds()); + + $new = $t->setMicroseconds(1_000_000); + $this->assertSame(0, $new->getMicroseconds()); + + $new = $t->setMicroseconds(-1); + $this->assertSame(999_999, $new->getMicroseconds()); + + $new = $t->setTime(24, 60, 60, 1_000_000); + $this->assertSame(0, $new->getHours()); + $this->assertSame(0, $new->getMinutes()); + $this->assertSame(0, $new->getSeconds()); + $this->assertSame(0, $new->getMicroseconds()); + + $new = $t->setTime(-1, -1, -1, -1); + $this->assertSame(23, $new->getHours()); + $this->assertSame(59, $new->getMinutes()); + $this->assertSame(59, $new->getSeconds()); + $this->assertSame(999_999, $new->getMicroseconds()); + } + + public function testFormat(): void + { + $t = new ChronosTime('23:59:59.999999'); + $this->assertSame('23:59:59.999999', $t->format('H:i:s.u')); + } +} diff --git a/tests/TestCase/DateTime/GettersTest.php b/tests/TestCase/DateTime/GettersTest.php index 7d18ce91..419bcd0c 100644 --- a/tests/TestCase/DateTime/GettersTest.php +++ b/tests/TestCase/DateTime/GettersTest.php @@ -312,6 +312,6 @@ public function testInvalidGetter() $this->expectException(InvalidArgumentException::class); $d = Chronos::now(); - $bb = $d->doesNotExit; + $d->doesNotExit; } }