From aa1e488afaa3cc5f62108b6fce66ef0b74eab0d5 Mon Sep 17 00:00:00 2001 From: Ben Ramsey Date: Tue, 13 Sep 2022 22:13:33 -0500 Subject: [PATCH] feat: support version 7 (Unix Epoch time) UUIDs --- psalm-baseline.xml | 9 +- src/Converter/Time/UnixTimeConverter.php | 93 ++++++++ src/FeatureSet.php | 32 ++- src/Generator/UnixTimeGenerator.php | 59 ++++++ src/Rfc4122/Fields.php | 13 +- src/Rfc4122/UuidBuilder.php | 39 ++-- src/Rfc4122/UuidV7.php | 60 ++++++ src/Rfc4122/VersionTrait.php | 15 +- src/Uuid.php | 29 +++ src/UuidFactory.php | 28 ++- .../Converter/Time/UnixTimeConverterTest.php | 198 ++++++++++++++++++ tests/FeatureSetTest.php | 8 + tests/Generator/UnixTimeGeneratorTest.php | 39 ++++ tests/Guid/FieldsTest.php | 8 +- tests/Rfc4122/FieldsTest.php | 20 +- tests/Rfc4122/UuidBuilderTest.php | 7 + tests/Rfc4122/UuidV7Test.php | 132 ++++++++++++ tests/UuidFactoryTest.php | 2 + tests/UuidTest.php | 30 +++ tests/benchmark/UuidGenerationBench.php | 5 + 20 files changed, 784 insertions(+), 42 deletions(-) create mode 100644 src/Converter/Time/UnixTimeConverter.php create mode 100644 src/Generator/UnixTimeGenerator.php create mode 100644 src/Rfc4122/UuidV7.php create mode 100644 tests/Converter/Time/UnixTimeConverterTest.php create mode 100644 tests/Generator/UnixTimeGeneratorTest.php create mode 100644 tests/Rfc4122/UuidV7Test.php diff --git a/psalm-baseline.xml b/psalm-baseline.xml index cf70c9a..96f8617 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -95,13 +95,10 @@ $macs[] - - - Uuid::UUID_TYPE_PEABODY - - - + + $this + $this $this $this $this diff --git a/src/Converter/Time/UnixTimeConverter.php b/src/Converter/Time/UnixTimeConverter.php new file mode 100644 index 0000000..d94233f --- /dev/null +++ b/src/Converter/Time/UnixTimeConverter.php @@ -0,0 +1,93 @@ + + * @license http://opensource.org/licenses/MIT MIT + */ + +declare(strict_types=1); + +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\Integer as IntegerObject; +use Ramsey\Uuid\Type\Time; + +use function explode; +use function str_pad; + +use const STR_PAD_LEFT; + +/** + * UnixTimeConverter converts Unix Epoch timestamps to/from hexadecimal values + * consisting of milliseconds elapsed since the Unix Epoch + * + * @psalm-immutable + */ +class UnixTimeConverter implements TimeConverterInterface +{ + private const MILLISECONDS = 1000; + + private CalculatorInterface $calculator; + + public function __construct(CalculatorInterface $calculator) + { + $this->calculator = $calculator; + } + + public function calculateTime(string $seconds, string $microseconds): Hexadecimal + { + $timestamp = new Time($seconds, $microseconds); + + // Convert the seconds into milliseconds. + $sec = $this->calculator->multiply( + $timestamp->getSeconds(), + new IntegerObject(self::MILLISECONDS), + ); + + // Convert the microseconds into milliseconds; the scale is zero because + // we need to discard the fractional part. + $usec = $this->calculator->divide( + RoundingMode::DOWN, // Always round down to stay in the previous millisecond. + 0, + $timestamp->getMicroseconds(), + new IntegerObject(self::MILLISECONDS), + ); + + /** @var IntegerObject $unixTime */ + $unixTime = $this->calculator->add($sec, $usec); + + $unixTimeHex = str_pad( + $this->calculator->toHexadecimal($unixTime)->toString(), + 12, + '0', + STR_PAD_LEFT + ); + + return new Hexadecimal($unixTimeHex); + } + + public function convertTime(Hexadecimal $uuidTimestamp): Time + { + $milliseconds = $this->calculator->toInteger($uuidTimestamp); + + $unixTimestamp = $this->calculator->divide( + RoundingMode::HALF_UP, + 6, + $milliseconds, + new IntegerObject(self::MILLISECONDS) + ); + + $split = explode('.', (string) $unixTimestamp, 2); + + return new Time($split[0], $split[1] ?? '0'); + } +} diff --git a/src/FeatureSet.php b/src/FeatureSet.php index 819f99a..482a8de 100644 --- a/src/FeatureSet.php +++ b/src/FeatureSet.php @@ -23,6 +23,7 @@ use Ramsey\Uuid\Converter\Number\GenericNumberConverter; use Ramsey\Uuid\Converter\NumberConverterInterface; use Ramsey\Uuid\Converter\Time\GenericTimeConverter; use Ramsey\Uuid\Converter\Time\PhpTimeConverter; +use Ramsey\Uuid\Converter\Time\UnixTimeConverter; use Ramsey\Uuid\Converter\TimeConverterInterface; use Ramsey\Uuid\Generator\DceSecurityGenerator; use Ramsey\Uuid\Generator\DceSecurityGeneratorInterface; @@ -35,6 +36,7 @@ use Ramsey\Uuid\Generator\RandomGeneratorFactory; use Ramsey\Uuid\Generator\RandomGeneratorInterface; use Ramsey\Uuid\Generator\TimeGeneratorFactory; use Ramsey\Uuid\Generator\TimeGeneratorInterface; +use Ramsey\Uuid\Generator\UnixTimeGenerator; use Ramsey\Uuid\Guid\GuidBuilder; use Ramsey\Uuid\Math\BrickMathCalculator; use Ramsey\Uuid\Math\CalculatorInterface; @@ -141,6 +143,8 @@ class FeatureSet */ private $calculator; + private TimeGeneratorInterface $unixTimeGenerator; + /** * @param bool $useGuids True build UUIDs using the GuidStringCodec * @param bool $force32Bit True to force the use of 32-bit functionality @@ -164,15 +168,18 @@ class FeatureSet $this->ignoreSystemNode = $ignoreSystemNode; $this->enablePecl = $enablePecl; + $this->randomGenerator = $this->buildRandomGenerator(); $this->setCalculator(new BrickMathCalculator()); $this->builder = $this->buildUuidBuilder($useGuids); $this->codec = $this->buildCodec($useGuids); $this->nodeProvider = $this->buildNodeProvider(); $this->nameGenerator = $this->buildNameGenerator(); - $this->randomGenerator = $this->buildRandomGenerator(); $this->setTimeProvider(new SystemTimeProvider()); $this->setDceSecurityProvider(new SystemDceSecurityProvider()); $this->validator = new GenericValidator(); + + assert($this->timeProvider !== null); + $this->unixTimeGenerator = $this->buildUnixTimeGenerator($this->timeProvider); } /** @@ -255,6 +262,14 @@ class FeatureSet return $this->timeGenerator; } + /** + * Returns the Unix Epoch time generator configured for this environment + */ + public function getUnixTimeGenerator(): TimeGeneratorInterface + { + return $this->unixTimeGenerator; + } + /** * Returns the validator configured for this environment */ @@ -396,6 +411,21 @@ class FeatureSet ))->getGenerator(); } + /** + * Returns a Unix Epoch time generator configured for this environment + * + * @param TimeProviderInterface $timeProvider The time provider to use with + * the time generator + */ + private function buildUnixTimeGenerator(TimeProviderInterface $timeProvider): TimeGeneratorInterface + { + return new UnixTimeGenerator( + new UnixTimeConverter(new BrickMathCalculator()), + $timeProvider, + $this->randomGenerator, + ); + } + /** * Returns a name generator configured for this environment */ diff --git a/src/Generator/UnixTimeGenerator.php b/src/Generator/UnixTimeGenerator.php new file mode 100644 index 0000000..74914dd --- /dev/null +++ b/src/Generator/UnixTimeGenerator.php @@ -0,0 +1,59 @@ + + * @license http://opensource.org/licenses/MIT MIT + */ + +declare(strict_types=1); + +namespace Ramsey\Uuid\Generator; + +use Ramsey\Uuid\Converter\TimeConverterInterface; +use Ramsey\Uuid\Provider\TimeProviderInterface; + +use function hex2bin; + +/** + * UnixTimeGenerator generates bytes that combine a 48-bit timestamp in + * milliseconds since the Unix Epoch with 80 random bits + */ +class UnixTimeGenerator implements TimeGeneratorInterface +{ + private RandomGeneratorInterface $randomGenerator; + private TimeConverterInterface $timeConverter; + private TimeProviderInterface $timeProvider; + + public function __construct( + TimeConverterInterface $timeConverter, + TimeProviderInterface $timeProvider, + RandomGeneratorInterface $randomGenerator + ) { + $this->timeConverter = $timeConverter; + $this->timeProvider = $timeProvider; + $this->randomGenerator = $randomGenerator; + } + + /** + * @inheritDoc + */ + public function generate($node = null, ?int $clockSeq = null): string + { + // Generate 10 random bytes to append to the string returned, since our + // time bytes will consist of 6 bytes. + $random = $this->randomGenerator->generate(10); + + $time = $this->timeProvider->getTime(); + $unixTimeHex = $this->timeConverter->calculateTime( + $time->getSeconds()->toString(), + $time->getMicroseconds()->toString(), + ); + + return hex2bin($unixTimeHex->toString()) . $random; + } +} diff --git a/src/Rfc4122/Fields.php b/src/Rfc4122/Fields.php index 2ccc20b..6103cdc 100644 --- a/src/Rfc4122/Fields.php +++ b/src/Rfc4122/Fields.php @@ -150,7 +150,7 @@ final class Fields implements FieldsInterface ); break; - case Uuid::UUID_TYPE_PEABODY: + case Uuid::UUID_TYPE_REORDERED_TIME: $timestamp = sprintf( '%08s%04s%03x', $this->getTimeLow()->toString(), @@ -158,6 +158,17 @@ final class Fields implements FieldsInterface hexdec($this->getTimeHiAndVersion()->toString()) & 0x0fff ); + break; + case Uuid::UUID_TYPE_UNIX_TIME: + // The Unix timestamp in version 7 UUIDs is a 48-bit number, + // but for consistency, we will return a 60-bit number, padded + // to the left with zeros. + $timestamp = sprintf( + '%011s%04s', + $this->getTimeLow()->toString(), + $this->getTimeMid()->toString(), + ); + break; default: $timestamp = sprintf( diff --git a/src/Rfc4122/UuidBuilder.php b/src/Rfc4122/UuidBuilder.php index df7ab30..10eded9 100644 --- a/src/Rfc4122/UuidBuilder.php +++ b/src/Rfc4122/UuidBuilder.php @@ -17,10 +17,13 @@ namespace Ramsey\Uuid\Rfc4122; use Ramsey\Uuid\Builder\UuidBuilderInterface; use Ramsey\Uuid\Codec\CodecInterface; use Ramsey\Uuid\Converter\NumberConverterInterface; +use Ramsey\Uuid\Converter\Time\UnixTimeConverter; use Ramsey\Uuid\Converter\TimeConverterInterface; use Ramsey\Uuid\Exception\UnableToBuildUuidException; use Ramsey\Uuid\Exception\UnsupportedOperationException; +use Ramsey\Uuid\Math\BrickMathCalculator; use Ramsey\Uuid\Rfc4122\UuidInterface as Rfc4122UuidInterface; +use Ramsey\Uuid\Uuid; use Ramsey\Uuid\UuidInterface; use Throwable; @@ -31,15 +34,9 @@ use Throwable; */ class UuidBuilder implements UuidBuilderInterface { - /** - * @var NumberConverterInterface - */ - private $numberConverter; - - /** - * @var TimeConverterInterface - */ - private $timeConverter; + private NumberConverterInterface $numberConverter; + private TimeConverterInterface $timeConverter; + private TimeConverterInterface $unixTimeConverter; /** * Constructs the DefaultUuidBuilder @@ -47,14 +44,20 @@ class UuidBuilder implements UuidBuilderInterface * @param NumberConverterInterface $numberConverter The number converter to * use when constructing the Uuid * @param TimeConverterInterface $timeConverter The time converter to use - * for converting timestamps extracted from a UUID to Unix timestamps + * for converting Gregorian time extracted from version 1, 2, and 6 + * UUIDs to Unix timestamps + * @param TimeConverterInterface|null $unixTimeConverter The time converter + * to use for converter Unix Epoch time extracted from version 7 UUIDs + * to Unix timestamps */ public function __construct( NumberConverterInterface $numberConverter, - TimeConverterInterface $timeConverter + TimeConverterInterface $timeConverter, + ?TimeConverterInterface $unixTimeConverter = null ) { $this->numberConverter = $numberConverter; $this->timeConverter = $timeConverter; + $this->unixTimeConverter = $unixTimeConverter ?? new UnixTimeConverter(new BrickMathCalculator()); } /** @@ -77,18 +80,20 @@ class UuidBuilder implements UuidBuilderInterface } switch ($fields->getVersion()) { - case 1: + case Uuid::UUID_TYPE_TIME: return new UuidV1($fields, $this->numberConverter, $codec, $this->timeConverter); - case 2: + case Uuid::UUID_TYPE_DCE_SECURITY: return new UuidV2($fields, $this->numberConverter, $codec, $this->timeConverter); - case 3: + case Uuid::UUID_TYPE_HASH_MD5: return new UuidV3($fields, $this->numberConverter, $codec, $this->timeConverter); - case 4: + case Uuid::UUID_TYPE_RANDOM: return new UuidV4($fields, $this->numberConverter, $codec, $this->timeConverter); - case 5: + case Uuid::UUID_TYPE_HASH_SHA1: return new UuidV5($fields, $this->numberConverter, $codec, $this->timeConverter); - case 6: + case Uuid::UUID_TYPE_REORDERED_TIME: return new UuidV6($fields, $this->numberConverter, $codec, $this->timeConverter); + case Uuid::UUID_TYPE_UNIX_TIME: + return new UuidV7($fields, $this->numberConverter, $codec, $this->unixTimeConverter); } throw new UnsupportedOperationException( diff --git a/src/Rfc4122/UuidV7.php b/src/Rfc4122/UuidV7.php new file mode 100644 index 0000000..90c2471 --- /dev/null +++ b/src/Rfc4122/UuidV7.php @@ -0,0 +1,60 @@ + + * @license http://opensource.org/licenses/MIT MIT + */ + +declare(strict_types=1); + +namespace Ramsey\Uuid\Rfc4122; + +use Ramsey\Uuid\Codec\CodecInterface; +use Ramsey\Uuid\Converter\NumberConverterInterface; +use Ramsey\Uuid\Converter\TimeConverterInterface; +use Ramsey\Uuid\Exception\InvalidArgumentException; +use Ramsey\Uuid\Rfc4122\FieldsInterface as Rfc4122FieldsInterface; +use Ramsey\Uuid\Uuid; + +/** + * Gregorian time, or version 1, UUIDs include timestamp, clock sequence, and node + * values that are combined into a 128-bit unsigned integer + * + * @psalm-immutable + */ +final class UuidV7 extends Uuid implements UuidInterface +{ + use TimeTrait; + + /** + * Creates a version 7 (Unix Epoch time) UUID + * + * @param Rfc4122FieldsInterface $fields The fields from which to construct a UUID + * @param NumberConverterInterface $numberConverter The number converter to use + * for converting hex values to/from integers + * @param CodecInterface $codec The codec to use when encoding or decoding + * UUID strings + * @param TimeConverterInterface $timeConverter The time converter to use + * for converting timestamps extracted from a UUID to unix timestamps + */ + public function __construct( + Rfc4122FieldsInterface $fields, + NumberConverterInterface $numberConverter, + CodecInterface $codec, + TimeConverterInterface $timeConverter + ) { + if ($fields->getVersion() !== Uuid::UUID_TYPE_UNIX_TIME) { + throw new InvalidArgumentException( + 'Fields used to create a UuidV7 must represent a ' + . 'version 7 (Unix Epoch time) UUID' + ); + } + + parent::__construct($fields, $numberConverter, $codec, $timeConverter); + } +} diff --git a/src/Rfc4122/VersionTrait.php b/src/Rfc4122/VersionTrait.php index cee55fb..b65ca7b 100644 --- a/src/Rfc4122/VersionTrait.php +++ b/src/Rfc4122/VersionTrait.php @@ -14,6 +14,8 @@ declare(strict_types=1); namespace Ramsey\Uuid\Rfc4122; +use Ramsey\Uuid\Uuid; + /** * Provides common functionality for handling the version, as defined by RFC 4122 * @@ -43,12 +45,13 @@ trait VersionTrait } switch ($this->getVersion()) { - case 1: - case 2: - case 3: - case 4: - case 5: - case 6: + case Uuid::UUID_TYPE_TIME: + case Uuid::UUID_TYPE_DCE_SECURITY: + case Uuid::UUID_TYPE_HASH_MD5: + case Uuid::UUID_TYPE_RANDOM: + case Uuid::UUID_TYPE_HASH_SHA1: + case Uuid::UUID_TYPE_REORDERED_TIME: + case Uuid::UUID_TYPE_UNIX_TIME: return true; } diff --git a/src/Uuid.php b/src/Uuid.php index deaa58e..ee04da8 100644 --- a/src/Uuid.php +++ b/src/Uuid.php @@ -18,6 +18,7 @@ use DateTimeInterface; use Ramsey\Uuid\Codec\CodecInterface; use Ramsey\Uuid\Converter\NumberConverterInterface; use Ramsey\Uuid\Converter\TimeConverterInterface; +use Ramsey\Uuid\Exception\UnsupportedOperationException; use Ramsey\Uuid\Fields\FieldsInterface; use Ramsey\Uuid\Lazy\LazyUuidFromString; use Ramsey\Uuid\Rfc4122\FieldsInterface as Rfc4122FieldsInterface; @@ -27,6 +28,7 @@ use ValueError; use function assert; use function bin2hex; +use function method_exists; use function preg_match; use function sprintf; use function str_replace; @@ -167,6 +169,13 @@ class Uuid implements UuidInterface */ public const UUID_TYPE_REORDERED_TIME = 6; + /** + * Version 7 (Unix Epoch time) UUID + * + * @link https://datatracker.ietf.org/doc/html/draft-peabody-dispatch-new-uuid-format-04#section-5.2 UUID Version 7 + */ + public const UUID_TYPE_UNIX_TIME = 7; + /** * DCE Security principal domain * @@ -670,4 +679,24 @@ class Uuid implements UuidInterface ): UuidInterface { return self::getFactory()->uuid6($node, $clockSeq); } + + /** + * Returns a version 7 (Unix Epoch time) UUID + * + * @return UuidInterface A UuidInterface instance that represents a + * version 7 UUID + */ + public static function uuid7(): UuidInterface + { + $factory = self::getFactory(); + + if (method_exists($factory, 'uuid7')) { + /** @var UuidInterface */ + return $factory->uuid7(); + } + + throw new UnsupportedOperationException( + 'The provided factory does not support the uuid7() method', + ); + } } diff --git a/src/UuidFactory.php b/src/UuidFactory.php index 6f2cea0..de3707c 100644 --- a/src/UuidFactory.php +++ b/src/UuidFactory.php @@ -98,6 +98,8 @@ class UuidFactory implements UuidFactoryInterface /** @var bool whether the feature set was provided from outside, or we can operate under "default" assumptions */ private $isDefaultFeatureSet; + private TimeGeneratorInterface $unixTimeGenerator; + /** * @param FeatureSet $features A set of available features in the current environment */ @@ -117,6 +119,7 @@ class UuidFactory implements UuidFactoryInterface $this->timeGenerator = $features->getTimeGenerator(); $this->uuidBuilder = $features->getBuilder(); $this->validator = $features->getValidator(); + $this->unixTimeGenerator = $features->getUnixTimeGenerator(); } /** @@ -342,7 +345,7 @@ class UuidFactory implements UuidFactoryInterface $bytes = $timeGenerator->generate($nodeHex, $clockSeq); - return $this->uuidFromBytesAndVersion($bytes, 1); + return $this->uuidFromBytesAndVersion($bytes, Uuid::UUID_TYPE_TIME); } /** @@ -352,7 +355,7 @@ class UuidFactory implements UuidFactoryInterface { $bytes = $this->timeGenerator->generate($node, $clockSeq); - return $this->uuidFromBytesAndVersion($bytes, 1); + return $this->uuidFromBytesAndVersion($bytes, Uuid::UUID_TYPE_TIME); } public function uuid2( @@ -368,7 +371,7 @@ class UuidFactory implements UuidFactoryInterface $clockSeq ); - return $this->uuidFromBytesAndVersion($bytes, 2); + return $this->uuidFromBytesAndVersion($bytes, Uuid::UUID_TYPE_DCE_SECURITY); } /** @@ -377,14 +380,14 @@ class UuidFactory implements UuidFactoryInterface */ public function uuid3($ns, string $name): UuidInterface { - return $this->uuidFromNsAndName($ns, $name, 3, 'md5'); + return $this->uuidFromNsAndName($ns, $name, Uuid::UUID_TYPE_HASH_MD5, 'md5'); } public function uuid4(): UuidInterface { $bytes = $this->randomGenerator->generate(16); - return $this->uuidFromBytesAndVersion($bytes, 4); + return $this->uuidFromBytesAndVersion($bytes, Uuid::UUID_TYPE_RANDOM); } /** @@ -393,7 +396,7 @@ class UuidFactory implements UuidFactoryInterface */ public function uuid5($ns, string $name): UuidInterface { - return $this->uuidFromNsAndName($ns, $name, 5, 'sha1'); + return $this->uuidFromNsAndName($ns, $name, Uuid::UUID_TYPE_HASH_SHA1, 'sha1'); } public function uuid6(?Hexadecimal $node = null, ?int $clockSeq = null): UuidInterface @@ -412,7 +415,18 @@ class UuidFactory implements UuidFactoryInterface $v6Bytes = hex2bin(substr($v6, 1, 12) . '0' . substr($v6, -3)); $v6Bytes .= substr($bytes, 8); - return $this->uuidFromBytesAndVersion($v6Bytes, 6); + return $this->uuidFromBytesAndVersion($v6Bytes, Uuid::UUID_TYPE_REORDERED_TIME); + } + + /** + * Returns a version 7 (Unix Epoch time) UUID + * + * @return UuidInterface A UuidInterface instance that represents a + * version 7 UUID + */ + public function uuid7(): UuidInterface + { + return $this->uuidFromBytesAndVersion($this->unixTimeGenerator->generate(), Uuid::UUID_TYPE_UNIX_TIME); } /** diff --git a/tests/Converter/Time/UnixTimeConverterTest.php b/tests/Converter/Time/UnixTimeConverterTest.php new file mode 100644 index 0000000..3a22f8a --- /dev/null +++ b/tests/Converter/Time/UnixTimeConverterTest.php @@ -0,0 +1,198 @@ +convertTime($uuidTimestamp); + + $this->assertSame($unixTimestamp, $result->getSeconds()->toString()); + $this->assertSame($microseconds, $result->getMicroseconds()->toString()); + } + + /** + * @return array + */ + public function provideConvertTime(): array + { + return [ + [ + 'uuidTimestamp' => new Hexadecimal('017F22E279B0'), + 'unixTimestamp' => '1645557742', + 'microseconds' => '0', + ], + [ + 'uuidTimestamp' => new Hexadecimal('01384fc480fb'), + 'unixTimestamp' => '1341368074', + 'microseconds' => '491000', + ], + [ + 'uuidTimestamp' => new Hexadecimal('016f8ca10161'), + 'unixTimestamp' => '1578612359', + 'microseconds' => '521000', + ], + [ + 'uuidTimestamp' => new Hexadecimal('5dbe85111a5f'), + 'unixTimestamp' => '103072857659', + 'microseconds' => '999000', + ], + + // This is the last possible time supported by v7 UUIDs. + // 10889-08-02 05:31:50.655 +00:00 + [ + 'uuidTimestamp' => new Hexadecimal('ffffffffffff'), + 'unixTimestamp' => '281474976710', + 'microseconds' => '655000', + ], + + // This is the earliest possible date supported by v7 UUIDs. + // It is the Unix Epoch (big surprise!). + // 1970-01-01 00:00:00.0 +00:00 + [ + 'uuidTimestamp' => new Hexadecimal('000000000000'), + 'unixTimestamp' => '0', + 'microseconds' => '0', + ], + + [ + 'uuidTimestamp' => new Hexadecimal('000000000001'), + 'unixTimestamp' => '0', + 'microseconds' => '1000', + ], + [ + 'uuidTimestamp' => new Hexadecimal('00000000000f'), + 'unixTimestamp' => '0', + 'microseconds' => '15000', + ], + [ + 'uuidTimestamp' => new Hexadecimal('000000000064'), + 'unixTimestamp' => '0', + 'microseconds' => '100000', + ], + [ + 'uuidTimestamp' => new Hexadecimal('0000000003e7'), + 'unixTimestamp' => '0', + 'microseconds' => '999000', + ], + [ + 'uuidTimestamp' => new Hexadecimal('0000000003e8'), + 'unixTimestamp' => '1', + 'microseconds' => '0', + ], + [ + 'uuidTimestamp' => new Hexadecimal('0000000003e9'), + 'unixTimestamp' => '1', + 'microseconds' => '1000', + ], + ]; + } + + /** + * @dataProvider provideCalculateTime + */ + public function testCalculateTime(string $seconds, string $microseconds, string $expected): void + { + $calculator = new BrickMathCalculator(); + $converter = new UnixTimeConverter($calculator); + + $result = $converter->calculateTime($seconds, $microseconds); + + $this->assertSame($expected, $result->toString()); + } + + /** + * @return array + */ + public function provideCalculateTime(): array + { + return [ + [ + 'seconds' => '1645557742', + 'microseconds' => '0', + 'expected' => '017f22e279b0', + ], + [ + 'seconds' => '1341368074', + 'microseconds' => '491000', + 'expected' => '01384fc480fb', + ], + [ + 'seconds' => '1578612359', + 'microseconds' => '521023', + 'expected' => '016f8ca10161', + ], + [ + 'seconds' => '103072857659', + 'microseconds' => '999499', + 'expected' => '5dbe85111a5f', + ], + [ + 'seconds' => '103072857659', + 'microseconds' => '999999', + 'expected' => '5dbe85111a5f', + ], + + // This is the earliest possible date supported by v7 UUIDs. + // It is the Unix Epoch (big surprise!): 1970-01-01 00:00:00.0 +00:00 + [ + 'seconds' => '0', + 'microseconds' => '0', + 'expected' => '000000000000', + ], + + // This is the last possible time supported by v7 UUIDs: + // 10889-08-02 05:31:50.655 +00:00 + [ + 'seconds' => '281474976710', + 'microseconds' => '655000', + 'expected' => 'ffffffffffff', + ], + + [ + 'seconds' => '0', + 'microseconds' => '1000', + 'expected' => '000000000001', + ], + [ + 'seconds' => '0', + 'microseconds' => '15000', + 'expected' => '00000000000f', + ], + [ + 'seconds' => '0', + 'microseconds' => '100000', + 'expected' => '000000000064', + ], + [ + 'seconds' => '0', + 'microseconds' => '999000', + 'expected' => '0000000003e7', + ], + [ + 'seconds' => '1', + 'microseconds' => '0', + 'expected' => '0000000003e8', + ], + [ + 'seconds' => '1', + 'microseconds' => '1000', + 'expected' => '0000000003e9', + ], + ]; + } +} diff --git a/tests/FeatureSetTest.php b/tests/FeatureSetTest.php index 407322d..62b8269 100644 --- a/tests/FeatureSetTest.php +++ b/tests/FeatureSetTest.php @@ -10,6 +10,7 @@ use Ramsey\Uuid\Converter\TimeConverterInterface; use Ramsey\Uuid\FeatureSet; use Ramsey\Uuid\Generator\DefaultNameGenerator; use Ramsey\Uuid\Generator\PeclUuidTimeGenerator; +use Ramsey\Uuid\Generator\UnixTimeGenerator; use Ramsey\Uuid\Guid\GuidBuilder; use Ramsey\Uuid\Math\BrickMathCalculator; use Ramsey\Uuid\Provider\NodeProviderInterface; @@ -77,4 +78,11 @@ class FeatureSetTest extends TestCase $this->assertSame($nodeProvider, $featureSet->getNodeProvider()); } + + public function testGetUnixTimeGenerator(): void + { + $featureSet = new FeatureSet(); + + $this->assertInstanceOf(UnixTimeGenerator::class, $featureSet->getUnixTimeGenerator()); + } } diff --git a/tests/Generator/UnixTimeGeneratorTest.php b/tests/Generator/UnixTimeGeneratorTest.php new file mode 100644 index 0000000..1b7a93d --- /dev/null +++ b/tests/Generator/UnixTimeGeneratorTest.php @@ -0,0 +1,39 @@ + new Time('1578612359', '521023'), + ]); + + /** @var RandomGeneratorInterface&MockInterface $randomGenerator */ + $randomGenerator = Mockery::mock(RandomGeneratorInterface::class); + $randomGenerator->expects()->generate(10)->andReturns("\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00"); + + $unixTimeGenerator = new UnixTimeGenerator($unixTimeConverter, $timeProvider, $randomGenerator); + + $this->assertSame( + "\x01\x6f\x8c\xa1\x01\x61\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00", + $unixTimeGenerator->generate(), + ); + } +} diff --git a/tests/Guid/FieldsTest.php b/tests/Guid/FieldsTest.php index 5c363db..d0f0689 100644 --- a/tests/Guid/FieldsTest.php +++ b/tests/Guid/FieldsTest.php @@ -89,9 +89,13 @@ class FieldsTest extends TestCase // representations, which are never in GUID byte order. return [ ['b08c6fff7dc5e1018b210800200c9a66'], - ['b08c6fff7dc5e1719b210800200c9a66'], - ['b08c6fff7dc5e181ab210800200c9a66'], ['b08c6fff7dc5e191bb210800200c9a66'], + ['b08c6fff7dc5e1a19b210800200c9a66'], + ['b08c6fff7dc5e1b1ab210800200c9a66'], + ['b08c6fff7dc5e1c1ab210800200c9a66'], + ['b08c6fff7dc5e1d1ab210800200c9a66'], + ['b08c6fff7dc5e1e1ab210800200c9a66'], + ['b08c6fff7dc5e1f1ab210800200c9a66'], ]; } diff --git a/tests/Rfc4122/FieldsTest.php b/tests/Rfc4122/FieldsTest.php index b34a3d3..97f7497 100644 --- a/tests/Rfc4122/FieldsTest.php +++ b/tests/Rfc4122/FieldsTest.php @@ -84,9 +84,13 @@ class FieldsTest extends TestCase { return [ ['ff6f8cb0-c57d-01e1-8b21-0800200c9a66'], - ['ff6f8cb0-c57d-71e1-9b21-0800200c9a66'], - ['ff6f8cb0-c57d-81e1-ab21-0800200c9a66'], ['ff6f8cb0-c57d-91e1-bb21-0800200c9a66'], + ['ff6f8cb0-c57d-a1e1-9b21-0800200c9a66'], + ['ff6f8cb0-c57d-b1e1-ab21-0800200c9a66'], + ['ff6f8cb0-c57d-c1e1-ab21-0800200c9a66'], + ['ff6f8cb0-c57d-d1e1-ab21-0800200c9a66'], + ['ff6f8cb0-c57d-e1e1-ab21-0800200c9a66'], + ['ff6f8cb0-c57d-f1e1-ab21-0800200c9a66'], ]; } @@ -198,6 +202,18 @@ class FieldsTest extends TestCase ['000001f5-5cde-21ea-8400-0242ac130003', 'getVariant', 2], ['000001f5-5cde-21ea-8400-0242ac130003', 'getVersion', 2], ['000001f5-5cde-21ea-8400-0242ac130003', 'isNil', false], + + ['018339f0-1b83-71e1-9b21-0800200c9a66', 'getClockSeq', '1b21'], + ['018339f0-1b83-71e1-9b21-0800200c9a66', 'getClockSeqHiAndReserved', '9b'], + ['018339f0-1b83-71e1-9b21-0800200c9a66', 'getClockSeqLow', '21'], + ['018339f0-1b83-71e1-9b21-0800200c9a66', 'getNode', '0800200c9a66'], + ['018339f0-1b83-71e1-9b21-0800200c9a66', 'getTimeHiAndVersion', '71e1'], + ['018339f0-1b83-71e1-9b21-0800200c9a66', 'getTimeLow', '018339f0'], + ['018339f0-1b83-71e1-9b21-0800200c9a66', 'getTimeMid', '1b83'], + ['018339f0-1b83-71e1-9b21-0800200c9a66', 'getTimestamp', '000018339f01b83'], + ['018339f0-1b83-71e1-9b21-0800200c9a66', 'getVariant', 2], + ['018339f0-1b83-71e1-9b21-0800200c9a66', 'getVersion', 7], + ['018339f0-1b83-71e1-9b21-0800200c9a66', 'isNil', false], ]; } diff --git a/tests/Rfc4122/UuidBuilderTest.php b/tests/Rfc4122/UuidBuilderTest.php index e234fc3..bf8a816 100644 --- a/tests/Rfc4122/UuidBuilderTest.php +++ b/tests/Rfc4122/UuidBuilderTest.php @@ -20,6 +20,7 @@ use Ramsey\Uuid\Rfc4122\UuidV3; use Ramsey\Uuid\Rfc4122\UuidV4; use Ramsey\Uuid\Rfc4122\UuidV5; use Ramsey\Uuid\Rfc4122\UuidV6; +use Ramsey\Uuid\Rfc4122\UuidV7; use Ramsey\Uuid\Test\TestCase; use function hex2bin; @@ -95,6 +96,12 @@ class UuidBuilderTest extends TestCase 'expectedClass' => NonstandardUuidV6::class, 'expectedVersion' => 6, ], + + [ + 'uuid' => 'ff6f8cb0-c57d-71e1-9b21-0800200c9a66', + 'expectedClass' => UuidV7::class, + 'expectedVersion' => 7, + ], ]; } diff --git a/tests/Rfc4122/UuidV7Test.php b/tests/Rfc4122/UuidV7Test.php new file mode 100644 index 0000000..fd3e5a4 --- /dev/null +++ b/tests/Rfc4122/UuidV7Test.php @@ -0,0 +1,132 @@ + $version, + ]); + + $numberConverter = Mockery::mock(NumberConverterInterface::class); + $codec = Mockery::mock(CodecInterface::class); + $timeConverter = Mockery::mock(TimeConverterInterface::class); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage( + 'Fields used to create a UuidV7 must represent a ' + . 'version 7 (Unix Epoch time) UUID' + ); + + new UuidV7($fields, $numberConverter, $codec, $timeConverter); + } + + /** + * @return array + */ + public function provideTestVersions(): array + { + return [ + ['version' => 0], + ['version' => 1], + ['version' => 2], + ['version' => 3], + ['version' => 4], + ['version' => 5], + ['version' => 6], + ['version' => 8], + ['version' => 9], + ]; + } + + /** + * @param non-empty-string $uuid + * + * @dataProvider provideUuidV7WithMicroseconds + */ + public function testGetDateTimeProperlyHandlesMicroseconds(string $uuid, string $expected): void + { + /** @var UuidV7 $object */ + $object = Uuid::fromString($uuid); + + $date = $object->getDateTime(); + + $this->assertInstanceOf(DateTimeImmutable::class, $date); + $this->assertSame($expected, $date->format('U.u')); + } + + /** + * @return array + */ + public function provideUuidV7WithMicroseconds(): array + { + return [ + [ + 'uuid' => '00000000-0001-71b2-9669-00007ffffffe', + 'expected' => '0.001000', + ], + [ + 'uuid' => '00000000-000f-71b2-9669-00007ffffffe', + 'expected' => '0.015000', + ], + [ + 'uuid' => '00000000-0064-71b2-9669-00007ffffffe', + 'expected' => '0.100000', + ], + [ + 'uuid' => '00000000-03e7-71b2-9669-00007ffffffe', + 'expected' => '0.999000', + ], + [ + 'uuid' => '00000000-03e8-71b2-9669-00007ffffffe', + 'expected' => '1.000000', + ], + [ + 'uuid' => '00000000-03e9-71b2-9669-00007ffffffe', + 'expected' => '1.001000', + ], + ]; + } + + public function testGetDateTimeThrowsException(): void + { + $fields = Mockery::mock(FieldsInterface::class, [ + 'getVersion' => 7, + 'getTimestamp' => new Hexadecimal('0'), + ]); + + $numberConverter = Mockery::mock(NumberConverterInterface::class); + $codec = Mockery::mock(CodecInterface::class); + + $timeConverter = Mockery::mock(TimeConverterInterface::class, [ + 'convertTime' => new Time('0', '1234567'), + ]); + + $uuid = new UuidV7($fields, $numberConverter, $codec, $timeConverter); + + $this->expectException(DateTimeException::class); + + $uuid->getDateTime(); + } +} diff --git a/tests/UuidFactoryTest.php b/tests/UuidFactoryTest.php index 0102b18..19ec914 100644 --- a/tests/UuidFactoryTest.php +++ b/tests/UuidFactoryTest.php @@ -67,6 +67,7 @@ class UuidFactoryTest extends TestCase $randomGenerator = Mockery::mock(RandomGeneratorInterface::class); $timeConverter = Mockery::mock(TimeConverterInterface::class); $timeGenerator = Mockery::mock(TimeGeneratorInterface::class); + $unixTimeGenerator = Mockery::mock(TimeGeneratorInterface::class); $nameGenerator = Mockery::mock(NameGeneratorInterface::class); $dceSecurityGenerator = Mockery::mock(DceSecurityGeneratorInterface::class); $numberConverter = Mockery::mock(NumberConverterInterface::class); @@ -84,6 +85,7 @@ class UuidFactoryTest extends TestCase 'getNumberConverter' => $numberConverter, 'getBuilder' => $builder, 'getValidator' => $validator, + 'getUnixTimeGenerator' => $unixTimeGenerator, ]); $uuidFactory = new UuidFactory($featureSet); diff --git a/tests/UuidTest.php b/tests/UuidTest.php index c2313b7..4be8c72 100644 --- a/tests/UuidTest.php +++ b/tests/UuidTest.php @@ -8,6 +8,7 @@ use Brick\Math\BigDecimal; use Brick\Math\RoundingMode; use DateTimeInterface; use Mockery; +use Mockery\MockInterface; use PHPUnit\Framework\MockObject\MockObject; use Ramsey\Uuid\Builder\DefaultUuidBuilder; use Ramsey\Uuid\Codec\StringCodec; @@ -33,6 +34,7 @@ use Ramsey\Uuid\Type\Hexadecimal; use Ramsey\Uuid\Type\Time; use Ramsey\Uuid\Uuid; use Ramsey\Uuid\UuidFactory; +use Ramsey\Uuid\UuidFactoryInterface; use Ramsey\Uuid\UuidInterface; use Ramsey\Uuid\Validator\GenericValidator; use Ramsey\Uuid\Validator\ValidatorInterface; @@ -705,6 +707,27 @@ class UuidTest extends TestCase $this->assertSame(6, $uuid->getVersion()); } + public function testUuid7(): void + { + $uuid = Uuid::uuid7(); + $this->assertInstanceOf(DateTimeInterface::class, $uuid->getDateTime()); + $this->assertSame(2, $uuid->getVariant()); + $this->assertSame(7, $uuid->getVersion()); + } + + public function testUuid7ThrowsExceptionForUnsupportedFactory(): void + { + /** @var UuidFactoryInterface&MockInterface $factory */ + $factory = Mockery::mock(UuidFactoryInterface::class); + + Uuid::setFactory($factory); + + $this->expectException(UnsupportedOperationException::class); + $this->expectExceptionMessage('The provided factory does not support the uuid7() method'); + + Uuid::uuid7(); + } + /** * Tests known version-3 UUIDs * @@ -1649,6 +1672,13 @@ class UuidTest extends TestCase { $uuid = Uuid::fromString('886313e1-3b8a-6372-9b90-0c9aee199e5d'); $this->assertSame($uuid->getVersion(), Uuid::UUID_TYPE_PEABODY); + $this->assertSame($uuid->getVersion(), Uuid::UUID_TYPE_REORDERED_TIME); + } + + public function testUuidVersionConstantForVersion7(): void + { + $uuid = Uuid::fromString('886313e1-3b8a-7372-9b90-0c9aee199e5d'); + $this->assertSame($uuid->getVersion(), Uuid::UUID_TYPE_UNIX_TIME); } public function testGetDateTimeThrowsExceptionWhenDateTimeCannotParseDate(): void diff --git a/tests/benchmark/UuidGenerationBench.php b/tests/benchmark/UuidGenerationBench.php index c547a9f..c3838e1 100644 --- a/tests/benchmark/UuidGenerationBench.php +++ b/tests/benchmark/UuidGenerationBench.php @@ -99,4 +99,9 @@ final class UuidGenerationBench { Uuid::uuid6($this->node, $this->clockSequence); } + + public function benchUuid7Generation(): void + { + Uuid::uuid7(); + } }