diff --git a/README.md b/README.md index 720f62f..24e5194 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ # uuid-php -A small PHP class for generating [RFC 4122][RFC 4122] version 3, 4, and 5 universally unique identifiers (UUID). Additionally supports [draft][draft 02] versions 6 and 7. +A small PHP class for generating [RFC 4122][RFC 4122] version 3, 4, and 5 universally unique identifiers (UUID). Additionally supports [draft][draft 03] versions 6 and 7. If all you want is a unique ID, you should call `uuid4()`. @@ -54,13 +54,13 @@ echo $uuid5 . "\n"; // c4a760a8-dbcf-5254-a0d9-6a4474bd1b62 // Generate a version 6 (lexicographically sortable) UUID $uuid6_first = UUID::uuid6(); -echo $uuid6_first . "\n"; // e.g. 1ebacf4f-a4a8-68ee-b4ec-618c14d005d5 +echo $uuid6_first . "\n"; // e.g. 1ec9414c-232a-6b00-b3c8-9e6bdeced846 $uuid6_second = UUID::uuid6(); var_dump($uuid6_first < $uuid6_second); // bool(true) // Generate a version 7 (lexicographically sortable) UUID $uuid7_first = UUID::uuid7(); -echo $uuid7_first . "\n"; // e.g. 061d0edc-bea0-75cc-9892-f6295fd7d295 +echo $uuid7_first . "\n"; // e.g. 017f21cf-d130-7cc3-98c4-dc0c0c07398f $uuid7_second = UUID::uuid7(); var_dump($uuid7_first < $uuid7_second); // bool(true) @@ -112,10 +112,10 @@ $cmp3 = UUID::cmp( var_dump($cmp3 === 0); // bool(true) // Extract Unix time from versions 6 and 7 as a string. -$uuid6_time = UUID::getTime('1ebacf4f-a4a8-68ee-b4ec-618c14d005d5'); -var_dump($uuid6_time); // string(18) "1620145373.6118510" -$uuid7_time = UUID::getTime('061d0edc-bea0-75cc-9892-f6295fd7d295'); -var_dump($uuid7_time); // string(18) "1641082315.9141510" +$uuid6_time = UUID::getTime('1ec9414c-232a-6b00-b3c8-9e6bdeced846'); +var_dump($uuid6_time); // string(18) "1645557742.0000000" +$uuid7_time = UUID::getTime('017f21cf-d130-7cc3-98c4-dc0c0c07398f'); +var_dump($uuid7_time); // string(18) "1645539742.0001995" // Extract the UUID version. $uuid_version = UUID::getVersion('2140a926-4a47-465c-b622-4571ad9bb378'); @@ -127,28 +127,26 @@ var_dump($uuid_version); // int(4) ``` 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | unixts | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - |unixts | subsec_a | ver | subsec_b | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - |var| rand | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | rand | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | unix_ts_ms | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | unix_ts_ms | ver | subsec | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |var|sub| rand | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | rand | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ``` -- `unixts`: 36 bit big-endian unsigned Unix Timestamp value -- `subsec_a`: 12 bits allocated to sub-second precision values +- `unix_ts_ms`: 48 bit big-endian unsigned number of Unix epoch timestamp with millisecond level of precision - `ver`: The 4 bit UUIDv7 version (0111) -- `subsec_b`: 12 bits allocated to sub-second precision values +- `subsec`: 12 bits allocated to sub-second precision values - `var`: 2 bit UUID variant (10) -- `rand`: The remaining 62 bits are filled with pseudo-random data +- `sub`: 2 bits allocated to sub-second precision values +- `rand`: The remaining 60 bits are filled with pseudo-random data -24 bits dedicated to sub-second precision provide 100 nanosecond resolution. The `unixts` and `subsec` fields guarantee the order of UUIDs generated within the same timestamp by monotonically incrementing the timer. - -This implementation does not include a clock sequence counter as defined in the draft RFC. +14 bits dedicated to sub-second precision provide 100 nanosecond resolution. The `unix_ts` and `subsec` fields guarantee the order of UUIDs generated within the same timestamp by monotonically incrementing the timer. [RFC 4122]: http://tools.ietf.org/html/rfc4122 -[draft 02]: https://datatracker.ietf.org/doc/html/draft-peabody-dispatch-new-uuid-format-02 +[draft 03]: https://datatracker.ietf.org/doc/html/draft-peabody-dispatch-new-uuid-format-03 [stackoverflow uuid4]: https://stackoverflow.com/a/15875555 diff --git a/src/UUID.php b/src/UUID.php index 54d0ce8..9bb46ac 100644 --- a/src/UUID.php +++ b/src/UUID.php @@ -62,7 +62,7 @@ class UUID private const INT_1E7 = 10_000_000; /** @internal */ - private const SUBSEC_BITS = 24; + private const SUBSEC_BITS = 14; /** @internal */ private const UUID_REGEX = '/^(?:urn:)?(?:uuid:)?(\{)?([0-9a-f]{8})\-?([0-9a-f]{4})' @@ -135,13 +135,13 @@ private static function uuidFromHex(string $uhex, int $version): string /** @internal */ private static function encodeSubsec(int $value): int { - return intdiv($value << self::SUBSEC_BITS, self::INT_1E7); + return intdiv($value << self::SUBSEC_BITS, 10000); } /** @internal */ private static function decodeSubsec(int $value): int { - return -(-$value * self::INT_1E7 >> self::SUBSEC_BITS); + return -(-$value * 10000 >> self::SUBSEC_BITS); } /** @@ -211,10 +211,15 @@ public static function uuid6(): string public static function uuid7(): string { [$unixts, $subsec] = self::getUnixTime(); - $subsec = self::encodeSubsec($subsec); - $uhex = substr(str_pad(dechex($unixts), 9, '0', \STR_PAD_LEFT), -9); - $uhex .= substr_replace(str_pad(dechex($subsec), 6, '0', \STR_PAD_LEFT), '7', -3, 0); - $uhex .= bin2hex(random_bytes(8)); + $unixtsms = $unixts * 1000 + intdiv($subsec, 10000); + $subsec = self::encodeSubsec($subsec % 10000); + $subsecA = $subsec >> 2; + $subsecB = $subsec & 0x03; + $randB = random_bytes(8); + $randB[0] = chr(ord($randB[0]) & 0x0f | $subsecB << 4); + $uhex = substr(str_pad(dechex($unixtsms), 12, '0', \STR_PAD_LEFT), -12); + $uhex .= '7' . str_pad(dechex($subsecA), 3, '0', \STR_PAD_LEFT); + $uhex .= bin2hex($randB); return self::uuidFromHex($uhex, 7); } @@ -262,9 +267,9 @@ public static function getTime(string $uuid): ?string } $retval .= substr_replace(str_pad(strval($ts), 8, '0', \STR_PAD_LEFT), '.', -7, 0); } elseif ($version === 7) { - $unixts = hexdec(substr($timehex, 0, 10)); - $subsec = self::decodeSubsec(hexdec(substr($timehex, 10))); - $retval = strval($unixts * self::INT_1E7 + $subsec); + $unixts = hexdec(substr($timehex, 0, 13)); + $subsec = self::decodeSubsec(hexdec(substr($timehex, 13)) + (hexdec(substr($uuid, 16, 1)) >> 4 & 0x03)); + $retval = strval($unixts * 10000 + $subsec); $retval = substr_replace(str_pad($retval, 8, '0', \STR_PAD_LEFT), '.', -7, 0); } return $retval; diff --git a/tests/UuidTest.php b/tests/UuidTest.php index 78e9b20..649169f 100644 --- a/tests/UuidTest.php +++ b/tests/UuidTest.php @@ -189,10 +189,10 @@ public function testCanUseAliases() public function testKnownGetTime() { - $uuid6_time = UUID::getTime('1ebacf4f-a4a8-68ee-b4ec-618c14d005d5'); - $this->assertSame($uuid6_time, '1620145373.6118510'); - $uuid7_time = UUID::getTime('061d0edc-bea0-75cc-9892-f6295fd7d295'); - $this->assertSame($uuid7_time, '1641082315.9141510'); + $uuid6_time = UUID::getTime('1EC9414C-232A-6B00-B3C8-9E6BDECED846'); + $this->assertSame('1645557742.0000000', $uuid6_time); + $uuid7_time = UUID::getTime('017F21CF-D130-7CC3-98C4-DC0C0C07398F'); + $this->assertSame('1645539742.000', substr($uuid7_time, 0, -4)); } public function testGetTimeValid() @@ -216,28 +216,28 @@ public function testGetTimeNull() public function testGetTimeNearEpoch() { $uuid6_time = UUID::getTime('1b21dd21-3814-6001-b6fa-54fb559c5fcd'); - $this->assertSame($uuid6_time, '0.0000001'); + $this->assertSame('0.0000001', $uuid6_time); } public function testGetTimeNegativeNearEpoch() { $uuid6_time = UUID::getTime('1b21dd21-3813-6fff-b678-1556dde9b80e'); - $this->assertSame($uuid6_time, '-0.0000001'); + $this->assertSame('-0.0000001', $uuid6_time); } public function testGetTimeZero() { $uuid6_time = UUID::getTime('00000000-0000-6000-8000-000000000000'); - $this->assertSame($uuid6_time, '-12219292800.0000000'); + $this->assertSame('-12219292800.0000000', $uuid6_time); $uuid7_time = UUID::getTime('00000000-0000-7000-8000-000000000000'); - $this->assertSame($uuid7_time, '0.0000000'); + $this->assertSame('0.0000000', $uuid7_time); } public function testGetTimeMax() { $uuid6_time = UUID::getTime('ffffffff-ffff-6fff-bfff-ffffffffffff'); - $this->assertSame($uuid6_time, '103072857660.6846975'); + $this->assertSame('103072857660.6846975', $uuid6_time); $uuid7_time = UUID::getTime('ffffffff-ffff-7fff-bfff-ffffffffffff'); - $this->assertSame($uuid7_time, '68719476736.0000000'); + $this->assertSame('281474976710.6552500', $uuid7_time); } }