Skip to content

Commit

Permalink
Draft 03 (#47)
Browse files Browse the repository at this point in the history
  • Loading branch information
oittaa authored Apr 5, 2022
1 parent 1f24336 commit e7860f8
Show file tree
Hide file tree
Showing 3 changed files with 47 additions and 44 deletions.
46 changes: 22 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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()`.

Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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');
Expand All @@ -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
25 changes: 15 additions & 10 deletions src/UUID.php
Original file line number Diff line number Diff line change
Expand Up @@ -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})'
Expand Down Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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;
Expand Down
20 changes: 10 additions & 10 deletions tests/UuidTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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);
}
}

0 comments on commit e7860f8

Please sign in to comment.