Skip to content

Commit

Permalink
feat: add time-based UUIDs
Browse files Browse the repository at this point in the history
  • Loading branch information
ramsey committed Sep 11, 2022
1 parent 7ecd581 commit 3d271ce
Show file tree
Hide file tree
Showing 9 changed files with 1,148 additions and 3 deletions.
74 changes: 74 additions & 0 deletions src/Uuid/TimeBasedUuid.php
@@ -0,0 +1,74 @@
<?php

/**
* This file is part of ramsey/identifier
*
* ramsey/identifier is open source software: you can distribute
* it and/or modify it under the terms of the MIT License
* (the "License"). You may not use this file except in
* compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
* implied. See the License for the specific language governing
* permissions and limitations under the License.
*
* @copyright Copyright (c) Ben Ramsey <ben@benramsey.com>
* @license https://opensource.org/licenses/MIT MIT License
*/

declare(strict_types=1);

namespace Ramsey\Identifier\Uuid;

use Brick\Math\BigInteger;
use Brick\Math\RoundingMode;
use DateTimeImmutable;
use Exception;

use function explode;
use function str_pad;

use const STR_PAD_LEFT;

/**
* @psalm-immutable
*/
trait TimeBasedUuid
{
use StandardUuid;

/**
* The number of 100-nanosecond intervals from the Gregorian calendar epoch
* to the Unix epoch.
*/
private string $gregorianToUnixIntervals = '122192928000000000';

/**
* The number of 100-nanosecond intervals in one second.
*/
private string $secondIntervals = '10000000';

/**
* Returns the full 60-bit timestamp as a hexadecimal string, without the version
*/
abstract protected function getTimestamp(): string;

/**
* @throws Exception When unable to create a DateTimeImmutable instance.
*/
public function getDateTime(): DateTimeImmutable
{
$epochNanoseconds = BigInteger::fromBase($this->getTimestamp(), 16)->minus($this->gregorianToUnixIntervals);
$unixTimestamp = $epochNanoseconds->dividedBy($this->secondIntervals, RoundingMode::HALF_UP);
$split = explode('.', (string) $unixTimestamp, 2);

return new DateTimeImmutable(
'@'
. $split[0]
. '.'
. str_pad($split[1] ?? '0', 6, '0', STR_PAD_LEFT),
);
}
}
60 changes: 60 additions & 0 deletions src/Uuid/UuidV1.php
@@ -0,0 +1,60 @@
<?php

/**
* This file is part of ramsey/identifier
*
* ramsey/identifier is open source software: you can distribute
* it and/or modify it under the terms of the MIT License
* (the "License"). You may not use this file except in
* compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
* implied. See the License for the specific language governing
* permissions and limitations under the License.
*
* @copyright Copyright (c) Ben Ramsey <ben@benramsey.com>
* @license https://opensource.org/licenses/MIT MIT License
*/

declare(strict_types=1);

namespace Ramsey\Identifier\Uuid;

use Identifier\TimeBasedUuidInterface;
use Identifier\Uuid\Version;

use function explode;
use function hexdec;
use function sprintf;

/**
* @psalm-immutable
*/
final class UuidV1 implements TimeBasedUuidInterface
{
use TimeBasedUuid;

public function getVersion(): Version
{
return Version::GregorianTime;
}

protected function getValidationPattern(): string
{
return '/^[0-9a-f]{8}-[0-9a-f]{4}-1[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/Di';
}

protected function getTimestamp(): string
{
$fields = explode('-', $this->uuid);

return sprintf(
'%03x%04s%08s',
hexdec($fields[2]) & 0x0fff,
$fields[1],
$fields[0],
);
}
}
32 changes: 29 additions & 3 deletions src/Uuid/UuidV2.php
Expand Up @@ -22,15 +22,19 @@

namespace Ramsey\Identifier\Uuid;

use Identifier\TimeBasedUuidInterface;
use Identifier\Uuid\Version;
use Identifier\UuidInterface;

use function explode;
use function hexdec;
use function sprintf;

/**
* @psalm-immutable
*/
final class UuidV2 implements UuidInterface
final class UuidV2 implements TimeBasedUuidInterface
{
use StandardUuid;
use TimeBasedUuid;

public function getVersion(): Version
{
Expand All @@ -41,4 +45,26 @@ protected function getValidationPattern(): string
{
return '/^[0-9a-f]{8}-[0-9a-f]{4}-2[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/Di';
}

/**
* Returns the full 60-bit timestamp as a hexadecimal string, without the version
*
* For version 2 UUIDs, the time_low field is the local identifier and
* should not be returned as part of the time. For this reason, we set the
* bottom 32 bits of the timestamp to 0's. As a result, there is some loss
* of fidelity of the timestamp, for version 2 UUIDs. The timestamp can be
* off by a range of 0 to 429.4967295 seconds (or 7 minutes, 9 seconds, and
* 496730 microseconds).
*/
protected function getTimestamp(): string
{
$fields = explode('-', $this->uuid);

return sprintf(
'%03x%04s%08s',
hexdec($fields[2]) & 0x0fff,
$fields[1],
'',
);
}
}
68 changes: 68 additions & 0 deletions src/Uuid/UuidV6.php
@@ -0,0 +1,68 @@
<?php

/**
* This file is part of ramsey/identifier
*
* ramsey/identifier is open source software: you can distribute
* it and/or modify it under the terms of the MIT License
* (the "License"). You may not use this file except in
* compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
* implied. See the License for the specific language governing
* permissions and limitations under the License.
*
* @copyright Copyright (c) Ben Ramsey <ben@benramsey.com>
* @license https://opensource.org/licenses/MIT MIT License
*/

declare(strict_types=1);

namespace Ramsey\Identifier\Uuid;

use Identifier\TimeBasedUuidInterface;
use Identifier\Uuid\Version;

use function explode;
use function hexdec;
use function sprintf;

/**
* @psalm-immutable
*/
final class UuidV6 implements TimeBasedUuidInterface
{
use TimeBasedUuid;

public function getVersion(): Version
{
return Version::ReorderedGregorianTime;
}

protected function getValidationPattern(): string
{
return '/^[0-9a-f]{8}-[0-9a-f]{4}-6[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/Di';
}

/**
* Returns the full 60-bit timestamp as a hexadecimal string, without the version
*
* For version 6 UUIDs, the timestamp order is reversed from the typical RFC
* 4122 order (the time bits are in the correct bit order, so that it is
* monotonically increasing). In returning the timestamp value, we put the
* bits in the order: time_low + time_mid + time_hi.
*/
protected function getTimestamp(): string
{
$fields = explode('-', $this->uuid);

return sprintf(
'%08s%04s%03x',
$fields[0],
$fields[1],
hexdec($fields[2]) & 0x0fff,
);
}
}
72 changes: 72 additions & 0 deletions src/Uuid/UuidV7.php
@@ -0,0 +1,72 @@
<?php

/**
* This file is part of ramsey/identifier
*
* ramsey/identifier is open source software: you can distribute
* it and/or modify it under the terms of the MIT License
* (the "License"). You may not use this file except in
* compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
* implied. See the License for the specific language governing
* permissions and limitations under the License.
*
* @copyright Copyright (c) Ben Ramsey <ben@benramsey.com>
* @license https://opensource.org/licenses/MIT MIT License
*/

declare(strict_types=1);

namespace Ramsey\Identifier\Uuid;

use DateTimeImmutable;
use Exception;
use Identifier\TimeBasedUuidInterface;
use Identifier\Uuid\Version;

use function explode;
use function hexdec;
use function number_format;
use function sprintf;

/**
* @psalm-immutable
*/
final class UuidV7 implements TimeBasedUuidInterface
{
use TimeBasedUuid;

/**
* @throws Exception When unable to create a DateTimeImmutable instance.
*/
public function getDateTime(): DateTimeImmutable
{
$unixTimestamp = number_format(hexdec($this->getTimestamp()) / 1000, 6, '.', '');

return new DateTimeImmutable('@' . $unixTimestamp);
}

public function getVersion(): Version
{
return Version::UnixTime;
}

protected function getValidationPattern(): string
{
return '/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/Di';
}

/**
* Returns a 48-bit timestamp as a hexadecimal string representing the Unix
* Epoch in milliseconds
*/
protected function getTimestamp(): string
{
$fields = explode('-', $this->uuid);

return sprintf('%08s%04s', $fields[0], $fields[1]);
}
}

0 comments on commit 3d271ce

Please sign in to comment.