From a8d52100decb96e7cfb5372e5d2fbf97994b0ce6 Mon Sep 17 00:00:00 2001 From: Ben Ramsey Date: Sun, 19 Jan 2020 22:06:42 -0600 Subject: [PATCH] Support microtime in returned DateTimeInterface instances Fixes #90 and supersedes #93. --- CHANGELOG.md | 8 +- src/Converter/Number/BigNumberConverter.php | 2 +- src/Converter/Time/BigNumberTimeConverter.php | 8 +- src/Converter/Time/GenericTimeConverter.php | 21 +++-- src/Converter/Time/PhpTimeConverter.php | 84 ++++++++++++++++--- src/Converter/TimeConverterInterface.php | 12 +-- src/DeprecatedUuidMethodsTrait.php | 15 ++-- src/FeatureSet.php | 2 +- src/Math/BrickMathCalculator.php | 28 +++++-- src/Math/CalculatorInterface.php | 39 +++++---- src/Rfc4122/UuidV1.php | 9 +- src/Type/Decimal.php | 64 ++++++++++++++ src/Type/IntegerValue.php | 10 ++- src/Type/NumberInterface.php | 25 ++++++ .../Time/BigNumberTimeConverterTest.php | 18 +--- .../Time/GenericTimeConverterTest.php | 24 ++++-- tests/Converter/Time/PhpTimeConverterTest.php | 42 ++++------ tests/Math/BrickMathCalculatorTest.php | 2 +- tests/UuidTest.php | 21 +++-- 19 files changed, 308 insertions(+), 126 deletions(-) create mode 100644 src/Type/Decimal.php create mode 100644 src/Type/NumberInterface.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 336fbc3..4c62a65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,6 +62,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. * Add `Converter\Number\GenericNumberConverter` and `Converter\Time\GenericTimeConverter` which will use the calculator provided to convert numbers and time to values for UUIDs. +* The `\DateTimeInterface` instance returned by `UuidInterface::getDateTime()` + (and now `Rfc4122\UuidV1::getDateTime()`) now includes microseconds, as + specified by the version 1 UUID. ### Changed @@ -91,9 +94,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. * `NumberConverterInterface::fromHex(string $hex): string` * `NumberConverterInterface::toHex(string $number): string` * `TimeConverterInterface::calculateTime(string $seconds, string $microSeconds): array` - * `TimeConverterInterface::convertTime(string $timestamp): string` -* `UnsatisfiedDependencyException` and `UnsupportedOperationException` are now - descended from `\LogicException`. Previously, they descended from `\RuntimeException`. +* `UnsupportedOperationException` is now descended from `\LogicException`. + Previously, it descended from `\RuntimeException`. * When encoding to bytes or decoding from bytes, `OrderedTimeCodec` now checks whether the UUID is an RFC 4122 variant, version 1 UUID. If not, it will throw an exception—`InvalidArgumentException` when using diff --git a/src/Converter/Number/BigNumberConverter.php b/src/Converter/Number/BigNumberConverter.php index 446672f..7a86108 100644 --- a/src/Converter/Number/BigNumberConverter.php +++ b/src/Converter/Number/BigNumberConverter.php @@ -19,7 +19,7 @@ use Ramsey\Uuid\Math\BrickMathCalculator; /** * Previously used to integrate moontoast/math as a bignum arithmetic library, - * BigNumberConverter is deprecated in favor of ArbitraryPrecisionNumberConverter + * BigNumberConverter is deprecated in favor of GenericNumberConverter * * @deprecated Transition to {@see GenericNumberConverter}. */ diff --git a/src/Converter/Time/BigNumberTimeConverter.php b/src/Converter/Time/BigNumberTimeConverter.php index 6d04a5d..eb28650 100644 --- a/src/Converter/Time/BigNumberTimeConverter.php +++ b/src/Converter/Time/BigNumberTimeConverter.php @@ -16,10 +16,12 @@ namespace Ramsey\Uuid\Converter\Time; use Ramsey\Uuid\Converter\TimeConverterInterface; use Ramsey\Uuid\Math\BrickMathCalculator; +use Ramsey\Uuid\Type\Hexadecimal; +use Ramsey\Uuid\Type\Time; /** * Previously used to integrate moontoast/math as a bignum arithmetic library, - * BigNumberTimeConverter is deprecated in favor of ArbitraryPrecisionTimeConverter + * BigNumberTimeConverter is deprecated in favor of GenericTimeConverter * * @deprecated Transition to {@see GenericTimeConverter}. */ @@ -48,8 +50,8 @@ class BigNumberTimeConverter implements TimeConverterInterface * @inheritDoc * @psalm-pure */ - public function convertTime(string $timestamp): string + public function convertTime(Hexadecimal $uuidTimestamp): Time { - return $this->converter->convertTime($timestamp); + return $this->converter->convertTime($uuidTimestamp); } } diff --git a/src/Converter/Time/GenericTimeConverter.php b/src/Converter/Time/GenericTimeConverter.php index b053df2..fbe2846 100644 --- a/src/Converter/Time/GenericTimeConverter.php +++ b/src/Converter/Time/GenericTimeConverter.php @@ -17,6 +17,7 @@ namespace Ramsey\Uuid\Converter\Time; use Ramsey\Uuid\Converter\TimeConverterInterface; use Ramsey\Uuid\Math\CalculatorInterface; use Ramsey\Uuid\Math\RoundingMode; +use Ramsey\Uuid\Type\Hexadecimal; use Ramsey\Uuid\Type\IntegerValue; use Ramsey\Uuid\Type\Time; @@ -54,6 +55,7 @@ class GenericTimeConverter implements TimeConverterInterface new IntegerValue('10') ); + /** @var IntegerValue $uuidTime */ $uuidTime = $this->calculator->add( $sec, $usec, @@ -78,12 +80,14 @@ class GenericTimeConverter implements TimeConverterInterface * @inheritDoc * @psalm-pure */ - public function convertTime(string $timestamp): string + public function convertTime(Hexadecimal $uuidTimestamp): Time { - $timestamp = new IntegerValue($timestamp); - - $unixTimestamp = $this->calculator->subtract( - $timestamp, + // From the total, subtract the number of 100-nanosecond intervals from + // the UUID epoch (Gregorian calendar date) to the Unix epoch. This + // gives us the number of 100-nanosecond intervals from the Unix epoch, + // which also includes the microtime. + $epochNanoseconds = $this->calculator->subtract( + $this->calculator->toIntegerValue($uuidTimestamp), new IntegerValue('122192928000000000') ); @@ -91,10 +95,13 @@ class GenericTimeConverter implements TimeConverterInterface // into the next second, giving us the wrong Unix timestamp. $unixTimestamp = $this->calculator->divide( RoundingMode::DOWN, - $unixTimestamp, + 6, + $epochNanoseconds, new IntegerValue('10000000') ); - return $unixTimestamp->toString(); + $split = explode('.', (string) $unixTimestamp, 2); + + return new Time($split[0], $split[1] ?? 0); } } diff --git a/src/Converter/Time/PhpTimeConverter.php b/src/Converter/Time/PhpTimeConverter.php index 22c43b5..f054c1b 100644 --- a/src/Converter/Time/PhpTimeConverter.php +++ b/src/Converter/Time/PhpTimeConverter.php @@ -16,7 +16,10 @@ namespace Ramsey\Uuid\Converter\Time; use Ramsey\Uuid\Converter\TimeConverterInterface; use Ramsey\Uuid\Math\BrickMathCalculator; +use Ramsey\Uuid\Math\CalculatorInterface; +use Ramsey\Uuid\Type\Hexadecimal; use Ramsey\Uuid\Type\IntegerValue; +use Ramsey\Uuid\Type\Time; /** * PhpTimeConverter uses built-in PHP functions and standard math operations @@ -25,18 +28,36 @@ use Ramsey\Uuid\Type\IntegerValue; */ class PhpTimeConverter implements TimeConverterInterface { + /** + * @var CalculatorInterface + */ + private $calculator; + /** * @var TimeConverterInterface */ private $fallbackConverter; - public function __construct(?TimeConverterInterface $fallbackConverter = null) - { - if ($fallbackConverter === null) { - $fallbackConverter = new GenericTimeConverter(new BrickMathCalculator()); + /** + * @var int + */ + private $phpPrecision; + + public function __construct( + ?CalculatorInterface $calculator = null, + ?TimeConverterInterface $fallbackConverter = null + ) { + if ($calculator === null) { + $calculator = new BrickMathCalculator(); } + if ($fallbackConverter === null) { + $fallbackConverter = new GenericTimeConverter($calculator); + } + + $this->calculator = $calculator; $this->fallbackConverter = $fallbackConverter; + $this->phpPrecision = (int) ini_get('precision'); } /** @@ -79,18 +100,57 @@ class PhpTimeConverter implements TimeConverterInterface * @inheritDoc * @psalm-pure */ - public function convertTime(string $timestamp): string + public function convertTime(Hexadecimal $uuidTimestamp): Time { - $timestamp = new IntegerValue($timestamp); + $timestamp = $this->calculator->toIntegerValue($uuidTimestamp); - $unixTime = ((int) $timestamp->toString() - 0x01b21dd213814000) / 10000000; + $splitTime = $this->splitTime( + ((int) $timestamp->toString() - 0x01b21dd213814000) / 10000000 + ); - if (!is_int($unixTime)) { - return $this->fallbackConverter->convertTime( - $timestamp->toString() - ); + if (count($splitTime) === 0) { + return $this->fallbackConverter->convertTime($uuidTimestamp); } - return (string) $unixTime; + return new Time($splitTime['sec'], $splitTime['usec']); + } + + /** + * @param int|float $time The time to split into seconds and microseconds + * + * @return string[] + * + * @psalm-pure + */ + private function splitTime($time): array + { + $split = explode('.', (string) $time, 2); + + // If the $time value is a float but $split only has 1 element, then the + // float math was rounded up to the next second, so we want to return + // an empty array to allow use of the fallback converter. + if (is_float($time) && count($split) === 1) { + return []; + } + + if (count($split) === 1) { + return [ + 'sec' => $split[0], + 'usec' => '0', + ]; + } + + // If the microseconds are less than six characters AND the length of + // the number is greater than or equal the PHP precision , then it's + // possible that we lost some precision for the microseconds. Return an + // empty array, so that we can choose to use the fallback converter. + if (strlen($split[1]) < 6 && strlen((string) $time) >= $this->phpPrecision) { + return []; + } + + return [ + 'sec' => $split[0], + 'usec' => str_pad($split[1], 6, '0', STR_PAD_RIGHT), + ]; } } diff --git a/src/Converter/TimeConverterInterface.php b/src/Converter/TimeConverterInterface.php index c428e39..de4785a 100644 --- a/src/Converter/TimeConverterInterface.php +++ b/src/Converter/TimeConverterInterface.php @@ -14,6 +14,9 @@ declare(strict_types=1); namespace Ramsey\Uuid\Converter; +use Ramsey\Uuid\Type\Hexadecimal; +use Ramsey\Uuid\Type\Time; + /** * A time converter converts timestamps into representations that may be used * in UUIDs @@ -40,14 +43,13 @@ interface TimeConverterInterface /** * Converts a timestamp extracted from a UUID to a Unix timestamp * - * @param string $timestamp A string integer representation of a UUID + * @param Hexadecimal $uuidTimestamp A hexadecimal representation of a UUID * timestamp; a UUID timestamp is a count of 100-nanosecond intervals - * since UTC 00:00:00.00, 15 October 1582; this must be a numeric string - * to accommodate unsigned integers greater than PHP_INT_MAX. + * since UTC 00:00:00.00, 15 October 1582. * - * @return string String representation of an integer + * @return Time An instance of {@see Time} * * @psalm-pure */ - public function convertTime(string $timestamp): string; + public function convertTime(Hexadecimal $uuidTimestamp): Time; } diff --git a/src/DeprecatedUuidMethodsTrait.php b/src/DeprecatedUuidMethodsTrait.php index f15821a..b8585dc 100644 --- a/src/DeprecatedUuidMethodsTrait.php +++ b/src/DeprecatedUuidMethodsTrait.php @@ -136,18 +136,15 @@ trait DeprecatedUuidMethodsTrait throw new UnsupportedOperationException('Not a time-based UUID'); } - $unixTime = $this->timeConverter->convertTime( - $this->numberConverter->fromHex($this->fields->getTimestamp()->toString()) - ); + $time = $this->timeConverter->convertTime($this->fields->getTimestamp()); try { - return new DateTimeImmutable("@{$unixTime}"); - } catch (Throwable $exception) { - throw new DateTimeException( - $exception->getMessage(), - (int) $exception->getCode(), - $exception + return new DateTimeImmutable( + date('Y-m-d H:i:s', (int) $time->getSeconds()->toString()) . '.' + . str_pad($time->getMicroSeconds()->toString(), 6, '0', STR_PAD_LEFT) ); + } catch (Throwable $e) { + throw new DateTimeException($e->getMessage(), (int) $e->getCode(), $e); } } diff --git a/src/FeatureSet.php b/src/FeatureSet.php index 921d07a..1e822b2 100644 --- a/src/FeatureSet.php +++ b/src/FeatureSet.php @@ -349,7 +349,7 @@ class FeatureSet $genericConverter = new GenericTimeConverter($calculator); if ($this->is64BitSystem()) { - return new PhpTimeConverter($genericConverter); + return new PhpTimeConverter($calculator, $genericConverter); } return $genericConverter; diff --git a/src/Math/BrickMathCalculator.php b/src/Math/BrickMathCalculator.php index 52839b5..f5798fd 100644 --- a/src/Math/BrickMathCalculator.php +++ b/src/Math/BrickMathCalculator.php @@ -14,12 +14,15 @@ declare(strict_types=1); namespace Ramsey\Uuid\Math; +use Brick\Math\BigDecimal; use Brick\Math\BigInteger; use Brick\Math\Exception\MathException; use Brick\Math\RoundingMode as BrickMathRounding; use Ramsey\Uuid\Exception\InvalidArgumentException; +use Ramsey\Uuid\Type\Decimal; use Ramsey\Uuid\Type\Hexadecimal; use Ramsey\Uuid\Type\IntegerValue; +use Ramsey\Uuid\Type\NumberInterface; /** * A calculator using the brick/math library for arbitrary-precision arithmetic @@ -41,7 +44,7 @@ final class BrickMathCalculator implements CalculatorInterface RoundingMode::HALF_EVEN => BrickMathRounding::HALF_EVEN, ]; - public function add(IntegerValue $augend, IntegerValue ...$addends): IntegerValue + public function add(NumberInterface $augend, NumberInterface ...$addends): NumberInterface { /** @psalm-suppress ImpureMethodCall */ $sum = BigInteger::of($augend->toString()); @@ -54,7 +57,7 @@ final class BrickMathCalculator implements CalculatorInterface return new IntegerValue((string) $sum); } - public function subtract(IntegerValue $minuend, IntegerValue ...$subtrahends): IntegerValue + public function subtract(NumberInterface $minuend, NumberInterface ...$subtrahends): NumberInterface { /** @psalm-suppress ImpureMethodCall */ $difference = BigInteger::of($minuend->toString()); @@ -67,7 +70,7 @@ final class BrickMathCalculator implements CalculatorInterface return new IntegerValue((string) $difference); } - public function multiply(IntegerValue $multiplicand, IntegerValue ...$multipliers): IntegerValue + public function multiply(NumberInterface $multiplicand, NumberInterface ...$multipliers): NumberInterface { /** @psalm-suppress ImpureMethodCall */ $product = BigInteger::of($multiplicand->toString()); @@ -80,19 +83,28 @@ final class BrickMathCalculator implements CalculatorInterface return new IntegerValue((string) $product); } - public function divide(int $roundingMode, IntegerValue $dividend, IntegerValue ...$divisors): IntegerValue - { + public function divide( + int $roundingMode, + int $scale, + NumberInterface $dividend, + NumberInterface ...$divisors + ): NumberInterface { $brickRounding = $this->getBrickRoundingMode($roundingMode); /** @psalm-suppress ImpureMethodCall */ - $quotient = BigInteger::of($dividend->toString()); + $quotient = BigDecimal::of($dividend->toString()); foreach ($divisors as $divisor) { /** @psalm-suppress ImpureMethodCall */ - $quotient = $quotient->dividedBy($divisor->toString(), $brickRounding); + $quotient = $quotient->dividedBy($divisor->toString(), $scale, $brickRounding); } - return new IntegerValue((string) $quotient); + if ($scale === 0) { + /** @psalm-suppress ImpureMethodCall */ + return new IntegerValue((string) $quotient->toBigInteger()); + } + + return new Decimal((string) $quotient); } public function fromBase(string $value, int $base): IntegerValue diff --git a/src/Math/CalculatorInterface.php b/src/Math/CalculatorInterface.php index 93a9657..2f4bdf0 100644 --- a/src/Math/CalculatorInterface.php +++ b/src/Math/CalculatorInterface.php @@ -16,6 +16,7 @@ namespace Ramsey\Uuid\Math; use Ramsey\Uuid\Type\Hexadecimal; use Ramsey\Uuid\Type\IntegerValue; +use Ramsey\Uuid\Type\NumberInterface; /** * A calculator performs arithmetic operations on numbers @@ -27,45 +28,51 @@ interface CalculatorInterface /** * Returns the sum of all the provided parameters * - * @param IntegerValue $augend The first addend (the integer being added to) - * @param IntegerValue ...$addends The additional integers to a add to the augend + * @param NumberInterface $augend The first addend (the integer being added to) + * @param NumberInterface ...$addends The additional integers to a add to the augend * - * @return IntegerValue The sum of all the parameters + * @return NumberInterface The sum of all the parameters */ - public function add(IntegerValue $augend, IntegerValue ...$addends): IntegerValue; + public function add(NumberInterface $augend, NumberInterface ...$addends): NumberInterface; /** * Returns the difference of all the provided parameters * - * @param IntegerValue $minuend The integer being subtracted from - * @param IntegerValue ...$subtrahends The integers to subtract from the minuend + * @param NumberInterface $minuend The integer being subtracted from + * @param NumberInterface ...$subtrahends The integers to subtract from the minuend * - * @return IntegerValue The difference after subtracting all parameters + * @return NumberInterface The difference after subtracting all parameters */ - public function subtract(IntegerValue $minuend, IntegerValue ...$subtrahends): IntegerValue; + public function subtract(NumberInterface $minuend, NumberInterface ...$subtrahends): NumberInterface; /** * Returns the product of all the provided parameters * - * @param IntegerValue $multiplicand The integer to be multiplied - * @param IntegerValue ...$multipliers The factors by which to multiply the multiplicand + * @param NumberInterface $multiplicand The integer to be multiplied + * @param NumberInterface ...$multipliers The factors by which to multiply the multiplicand * - * @return IntegerValue The product of multiplying all the provided parameters + * @return NumberInterface The product of multiplying all the provided parameters */ - public function multiply(IntegerValue $multiplicand, IntegerValue ...$multipliers): IntegerValue; + public function multiply(NumberInterface $multiplicand, NumberInterface ...$multipliers): NumberInterface; /** * Returns the quotient of the provided parameters divided left-to-right * * @param int $roundingMode The RoundingMode constant to use for this operation - * @param IntegerValue $dividend The integer to be divided - * @param IntegerValue ...$divisors The integers to divide the dividend, in + * @param int $scale The scale to use for this operation + * @param NumberInterface $dividend The integer to be divided + * @param NumberInterface ...$divisors * the order in which the division operations should take place * (left-to-right) * - * @return IntegerValue The quotient of dividing the provided parameters left-to-right + * @return NumberInterface The quotient of dividing the provided parameters left-to-right */ - public function divide(int $roundingMode, IntegerValue $dividend, IntegerValue ...$divisors): IntegerValue; + public function divide( + int $roundingMode, + int $scale, + NumberInterface $dividend, + NumberInterface ...$divisors + ): NumberInterface; /** * Converts a value from an arbitrary base to a base-10 integer value diff --git a/src/Rfc4122/UuidV1.php b/src/Rfc4122/UuidV1.php index 97b8a12..d53bac9 100644 --- a/src/Rfc4122/UuidV1.php +++ b/src/Rfc4122/UuidV1.php @@ -40,12 +40,13 @@ final class UuidV1 extends Uuid implements UuidInterface */ public function getDateTime(): DateTimeInterface { - $unixTime = $this->timeConverter->convertTime( - $this->numberConverter->fromHex($this->fields->getTimestamp()->toString()) - ); + $time = $this->timeConverter->convertTime($this->fields->getTimestamp()); try { - return new DateTimeImmutable("@{$unixTime}"); + return new DateTimeImmutable( + date('Y-m-d H:i:s', (int) $time->getSeconds()->toString()) . '.' + . str_pad($time->getMicroSeconds()->toString(), 6, '0', STR_PAD_LEFT) + ); } catch (Throwable $e) { throw new DateTimeException($e->getMessage(), (int) $e->getCode(), $e); } diff --git a/src/Type/Decimal.php b/src/Type/Decimal.php new file mode 100644 index 0000000..c072c06 --- /dev/null +++ b/src/Type/Decimal.php @@ -0,0 +1,64 @@ + + * @license http://opensource.org/licenses/MIT MIT + */ + +declare(strict_types=1); + +namespace Ramsey\Uuid\Type; + +use Ramsey\Uuid\Exception\InvalidArgumentException; + +/** + * A value object representing a decimal + * + * This class exists for type-safety purposes, to ensure that decimals + * returned from ramsey/uuid methods as strings are truly decimals and not some + * other kind of string. + * + * To support values as true decimals and not as floats or doubles, we store the + * decimals as strings. + * + * @psalm-immutable + */ +final class Decimal implements NumberInterface +{ + /** + * @var string + */ + private $value; + + /** + * @param mixed $value The decimal value to store + */ + public function __construct($value) + { + $value = (string) $value; + + if (!is_numeric($value)) { + throw new InvalidArgumentException( + 'Value must be a signed decimal or a string containing only ' + . 'digits 0-9 and, optionally, a decimal point or sign (+ or -)' + ); + } + + $this->value = $value; + } + + public function toString(): string + { + return $this->value; + } + + public function __toString(): string + { + return $this->toString(); + } +} diff --git a/src/Type/IntegerValue.php b/src/Type/IntegerValue.php index 2d6fb75..2c47453 100644 --- a/src/Type/IntegerValue.php +++ b/src/Type/IntegerValue.php @@ -30,7 +30,7 @@ use function ctype_digit; * * @psalm-immutable */ -final class IntegerValue +final class IntegerValue implements NumberInterface { /** * @var string @@ -58,6 +58,14 @@ final class IntegerValue ); } + // Trim any leading zeros. + $value = ltrim($value, '0'); + + // Set to zero if the string is empty after trimming zeros. + if ($value === '') { + $value = '0'; + } + // Add the negative sign back to the value. if ($sign === '-' && $value !== '0') { $value = $sign . $value; diff --git a/src/Type/NumberInterface.php b/src/Type/NumberInterface.php new file mode 100644 index 0000000..4cbc9ad --- /dev/null +++ b/src/Type/NumberInterface.php @@ -0,0 +1,25 @@ + + * @license http://opensource.org/licenses/MIT MIT + */ + +declare(strict_types=1); + +namespace Ramsey\Uuid\Type; + +/** + * NumberInterface ensures consistency in numeric values returned by ramsey/uuid + */ +interface NumberInterface +{ + public function toString(): string; + + public function __toString(): string; +} diff --git a/tests/Converter/Time/BigNumberTimeConverterTest.php b/tests/Converter/Time/BigNumberTimeConverterTest.php index fb1d437..963f6e7 100644 --- a/tests/Converter/Time/BigNumberTimeConverterTest.php +++ b/tests/Converter/Time/BigNumberTimeConverterTest.php @@ -8,6 +8,7 @@ use Brick\Math\BigInteger; use Ramsey\Uuid\Converter\Time\BigNumberTimeConverter; use Ramsey\Uuid\Exception\InvalidArgumentException; use Ramsey\Uuid\Test\TestCase; +use Ramsey\Uuid\Type\Hexadecimal; class BigNumberTimeConverterTest extends TestCase { @@ -40,9 +41,9 @@ class BigNumberTimeConverterTest extends TestCase public function testConvertTime(): void { $converter = new BigNumberTimeConverter(); - $returned = $converter->convertTime('135606608744910000'); + $returned = $converter->convertTime(new Hexadecimal('1e1c57dff6f8cb0')); - $this->assertSame('1341368074', $returned); + $this->assertSame('1341368074', $returned->getSeconds()->toString()); } public function testCalculateTimeThrowsExceptionWhenSecondsIsNotOnlyDigits(): void @@ -70,17 +71,4 @@ class BigNumberTimeConverterTest extends TestCase $converter->calculateTime('1234', '56.78'); } - - public function testConvertTimeThrowsExceptionWhenTimestampIsNotOnlyDigits(): void - { - $converter = new BigNumberTimeConverter(); - - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage( - 'Value must be a signed integer or a string containing only digits ' - . '0-9 and, optionally, a sign (+ or -)' - ); - - $converter->convertTime('1234.56'); - } } diff --git a/tests/Converter/Time/GenericTimeConverterTest.php b/tests/Converter/Time/GenericTimeConverterTest.php index ceebed1..ccb94e3 100644 --- a/tests/Converter/Time/GenericTimeConverterTest.php +++ b/tests/Converter/Time/GenericTimeConverterTest.php @@ -7,6 +7,7 @@ namespace Ramsey\Uuid\Test\Converter\Time; use Ramsey\Uuid\Converter\Time\GenericTimeConverter; use Ramsey\Uuid\Math\BrickMathCalculator; use Ramsey\Uuid\Test\TestCase; +use Ramsey\Uuid\Type\Hexadecimal; class GenericTimeConverterTest extends TestCase { @@ -88,14 +89,15 @@ class GenericTimeConverterTest extends TestCase /** * @dataProvider provideConvertTime */ - public function testConvertTime(string $uuidTimestamp, string $unixTimestamp): void + public function testConvertTime(Hexadecimal $uuidTimestamp, string $unixTimestamp, string $microSeconds): void { $calculator = new BrickMathCalculator(); $converter = new GenericTimeConverter($calculator); $result = $converter->convertTime($uuidTimestamp); - $this->assertSame($unixTimestamp, $result); + $this->assertSame($unixTimestamp, $result->getSeconds()->toString()); + $this->assertSame($microSeconds, $result->getMicroSeconds()->toString()); } /** @@ -105,38 +107,44 @@ class GenericTimeConverterTest extends TestCase { return [ [ - 'uuidTimestamp' => '135606608744910000', + 'uuidTimestamp' => new Hexadecimal('1e1c57dff6f8cb0'), 'unixTimestamp' => '1341368074', + 'microSeconds' => '491000', ], [ - 'uuidTimestamp' => '137979051595210230', + 'uuidTimestamp' => new Hexadecimal('1ea333764c71df6'), 'unixTimestamp' => '1578612359', + 'microSeconds' => '521023', ], [ - 'uuidTimestamp' => '1152921504599999990', + 'uuidTimestamp' => new Hexadecimal('fffffffff9785f6'), 'unixTimestamp' => '103072857659', + 'microSeconds' => '999999', ], // This is the last possible time supported by v1 UUIDs. When // converted to a Unix timestamp, the microseconds are lost. // 60038-03-11 05:36:10.955161 [ - 'uuidTimestamp' => '18446744073709551610', + 'uuidTimestamp' => new Hexadecimal('fffffffffffffffa'), 'unixTimestamp' => '1832455114570', + 'microSeconds' => '955161', ], // This is the earliest possible date supported by v1 UUIDs: // 1582-10-15 00:00:00.000000 [ - 'uuidTimestamp' => '0', + 'uuidTimestamp' => new Hexadecimal('000000000000'), 'unixTimestamp' => '-12219292800', + 'microSeconds' => '0', ], // This is the Unix epoch: // 1970-01-01 00:00:00.000000 [ - 'uuidTimestamp' => '122192928000000000', + 'uuidTimestamp' => new Hexadecimal('1b21dd213814000'), 'unixTimestamp' => '0', + 'microSeconds' => '0', ], ]; } diff --git a/tests/Converter/Time/PhpTimeConverterTest.php b/tests/Converter/Time/PhpTimeConverterTest.php index d36a6e3..638636e 100644 --- a/tests/Converter/Time/PhpTimeConverterTest.php +++ b/tests/Converter/Time/PhpTimeConverterTest.php @@ -11,6 +11,7 @@ use Ramsey\Uuid\Converter\Time\PhpTimeConverter; use Ramsey\Uuid\Exception\InvalidArgumentException; use Ramsey\Uuid\Math\BrickMathCalculator; use Ramsey\Uuid\Test\TestCase; +use Ramsey\Uuid\Type\Hexadecimal; class PhpTimeConverterTest extends TestCase { @@ -68,32 +69,19 @@ class PhpTimeConverterTest extends TestCase $converter->calculateTime('1234', '56.78'); } - public function testConvertTimeThrowsExceptionWhenTimestampIsNotOnlyDigits(): void - { - /** @var Mockery\MockInterface & PhpTimeConverter $converter */ - $converter = Mockery::mock(PhpTimeConverter::class)->makePartial(); - - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage( - 'Value must be a signed integer or a string containing only digits ' - . '0-9 and, optionally, a sign (+ or -)' - ); - - $converter->convertTime('1234.56'); - } - /** * @dataProvider provideConvertTime */ - public function testConvertTime(string $uuidTimestamp, string $unixTimestamp): void + public function testConvertTime(Hexadecimal $uuidTimestamp, string $unixTimestamp, string $microSeconds): void { $calculator = new BrickMathCalculator(); $fallbackConverter = new GenericTimeConverter($calculator); - $converter = new PhpTimeConverter($fallbackConverter); + $converter = new PhpTimeConverter($calculator, $fallbackConverter); $result = $converter->convertTime($uuidTimestamp); - $this->assertSame($unixTimestamp, $result); + $this->assertSame($unixTimestamp, $result->getSeconds()->toString()); + $this->assertSame($microSeconds, $result->getMicroSeconds()->toString()); } /** @@ -103,38 +91,44 @@ class PhpTimeConverterTest extends TestCase { return [ [ - 'uuidTimestamp' => '135606608744910000', + 'uuidTimestamp' => new Hexadecimal('1e1c57dff6f8cb0'), 'unixTimestamp' => '1341368074', + 'microSeconds' => '491000', ], [ - 'uuidTimestamp' => '137979051595210230', + 'uuidTimestamp' => new Hexadecimal('1ea333764c71df6'), 'unixTimestamp' => '1578612359', + 'microSeconds' => '521023', ], [ - 'uuidTimestamp' => '1152921504599999990', + 'uuidTimestamp' => new Hexadecimal('fffffffff9785f6'), 'unixTimestamp' => '103072857659', + 'microSeconds' => '999999', ], // This is the last possible time supported by v1 UUIDs. When // converted to a Unix timestamp, the microseconds are lost. // 60038-03-11 05:36:10.955161 [ - 'uuidTimestamp' => '18446744073709551610', + 'uuidTimestamp' => new Hexadecimal('fffffffffffffffa'), 'unixTimestamp' => '1832455114570', + 'microSeconds' => '955161', ], // This is the earliest possible date supported by v1 UUIDs: // 1582-10-15 00:00:00.000000 [ - 'uuidTimestamp' => '0', + 'uuidTimestamp' => new Hexadecimal('000000000000'), 'unixTimestamp' => '-12219292800', + 'microSeconds' => '0', ], // This is the Unix epoch: // 1970-01-01 00:00:00.000000 [ - 'uuidTimestamp' => '122192928000000000', + 'uuidTimestamp' => new Hexadecimal('1b21dd213814000'), 'unixTimestamp' => '0', + 'microSeconds' => '0', ], ]; } @@ -148,7 +142,7 @@ class PhpTimeConverterTest extends TestCase { $calculator = new BrickMathCalculator(); $fallbackConverter = new GenericTimeConverter($calculator); - $converter = new PhpTimeConverter($fallbackConverter); + $converter = new PhpTimeConverter($calculator, $fallbackConverter); $result = $converter->calculateTime($seconds, $microSeconds); diff --git a/tests/Math/BrickMathCalculatorTest.php b/tests/Math/BrickMathCalculatorTest.php index 77ae7de..1ebda63 100644 --- a/tests/Math/BrickMathCalculatorTest.php +++ b/tests/Math/BrickMathCalculatorTest.php @@ -59,7 +59,7 @@ class BrickMathCalculatorTest extends TestCase $calculator = new BrickMathCalculator(); - $result = $calculator->divide(RoundingMode::HALF_UP, $int1, $int2, $int3); + $result = $calculator->divide(RoundingMode::HALF_UP, 0, $int1, $int2, $int3); $this->assertSame('24', $result->toString()); } diff --git a/tests/UuidTest.php b/tests/UuidTest.php index c0278bd..694d751 100644 --- a/tests/UuidTest.php +++ b/tests/UuidTest.php @@ -162,22 +162,26 @@ class UuidTest extends TestCase // Check a recent date $uuid = Uuid::fromString('ff6f8cb0-c57d-11e1-9b21-0800200c9a66'); $this->assertInstanceOf(DateTimeInterface::class, $uuid->getDateTime()); - $this->assertEquals('2012-07-04T02:14:34+00:00', $uuid->getDateTime()->format('c')); + $this->assertSame('2012-07-04T02:14:34+00:00', $uuid->getDateTime()->format('c')); + $this->assertSame('1341368074.491000', $uuid->getDateTime()->format('U.u')); // Check an old date $uuid = Uuid::fromString('0901e600-0154-1000-9b21-0800200c9a66'); $this->assertInstanceOf(DateTimeInterface::class, $uuid->getDateTime()); - $this->assertEquals('1582-10-16T16:34:04+00:00', $uuid->getDateTime()->format('c')); + $this->assertSame('1582-10-16T16:34:04+00:00', $uuid->getDateTime()->format('c')); + $this->assertSame('-12219146756.000000', $uuid->getDateTime()->format('U.u')); // Check a future date $uuid = Uuid::fromString('ff9785f6-ffff-1fff-9669-00007ffffffe'); $this->assertInstanceOf(DateTimeInterface::class, $uuid->getDateTime()); - $this->assertEquals('5236-03-31T21:20:59+00:00', $uuid->getDateTime()->format('c')); + $this->assertSame('5236-03-31T21:20:59+00:00', $uuid->getDateTime()->format('c')); + $this->assertSame('103072857659.999999', $uuid->getDateTime()->format('U.u')); // Check the oldest date $uuid = Uuid::fromString('00000000-0000-1000-9669-00007ffffffe'); $this->assertInstanceOf(DateTimeInterface::class, $uuid->getDateTime()); - $this->assertEquals('1582-10-15T00:00:00+00:00', $uuid->getDateTime()->format('c')); + $this->assertSame('1582-10-15T00:00:00+00:00', $uuid->getDateTime()->format('c')); + $this->assertSame('-12219292800.000000', $uuid->getDateTime()->format('U.u')); } public function testGetDateTimeFromNonVersion1Uuid(): void @@ -188,7 +192,7 @@ class UuidTest extends TestCase $this->expectException(UnsupportedOperationException::class); $this->expectExceptionMessage('Not a time-based UUID'); - $date = $uuid->getDateTime(); + $uuid->getDateTime(); } public function testGetFields(): void @@ -1427,7 +1431,7 @@ class UuidTest extends TestCase $this->assertEquals($uuid->getVersion(), Uuid::UUID_TYPE_HASH_SHA1); } - public function testGetDateTimeThrowsExceptionWhenDateTimeCannotParseString(): void + public function testGetDateTimeThrowsExceptionWhenDateTimeCannotParseDate(): void { $numberConverter = new BigNumberConverter(); $timeConverter = Mockery::mock(TimeConverterInterface::class); @@ -1435,7 +1439,7 @@ class UuidTest extends TestCase $timeConverter ->shouldReceive('convertTime') ->once() - ->andReturn('foobar'); + ->andReturn(new Time(1579476464, '1234567890')); $builder = new DefaultUuidBuilder($numberConverter, $timeConverter); $codec = new StringCodec($builder); @@ -1448,7 +1452,8 @@ class UuidTest extends TestCase $this->expectException(DateTimeException::class); $this->expectExceptionMessage( 'DateTimeImmutable::__construct(): Failed to parse time string ' - . '(@foobar) at position 0 (@): Unexpected character' + . '(2020-01-19 23:27:44.1234567890) at position 11 (2): ' + . 'The timezone could not be found in the database' ); $uuid->getDateTime();