Add getDateTime() to UuidV2

This commit is contained in:
Ben Ramsey
2020-03-02 22:55:34 -06:00
parent 7289d33396
commit 1f1329433f
5 changed files with 201 additions and 13 deletions
+40 -11
View File
@@ -123,20 +123,49 @@ final class Fields implements FieldsInterface
return new Hexadecimal(bin2hex(substr($this->bytes, 4, 2)));
}
/**
* Returns the full 60-bit timestamp, 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).
*
* 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.
*/
public function getTimestamp(): Hexadecimal
{
$timestamp = sprintf(
'%03x%04s%08s',
hexdec($this->getTimeHiAndVersion()->toString()) & 0x0fff,
$this->getTimeMid()->toString(),
$this->getTimeLow()->toString()
);
switch ($this->getVersion()) {
case Uuid::UUID_TYPE_DCE_SECURITY:
$timestamp = sprintf(
'%03x%04s%08s',
hexdec($this->getTimeHiAndVersion()->toString()) & 0x0fff,
$this->getTimeMid()->toString(),
''
);
// Put the timestamp into the correct order, if this is a v6 UUID.
if ($this->getVersion() === Uuid::UUID_TYPE_PEABODY) {
$timestamp = substr($timestamp, 7)
. substr($timestamp, 3, 4)
. substr($timestamp, 0, 3);
break;
case Uuid::UUID_TYPE_PEABODY:
$timestamp = sprintf(
'%08s%04s%03x',
$this->getTimeLow()->toString(),
$this->getTimeMid()->toString(),
hexdec($this->getTimeHiAndVersion()->toString()) & 0x0fff
);
break;
default:
$timestamp = sprintf(
'%03x%04s%08s',
hexdec($this->getTimeHiAndVersion()->toString()) & 0x0fff,
$this->getTimeMid()->toString(),
$this->getTimeLow()->toString()
);
}
return new Hexadecimal($timestamp);
+42
View File
@@ -14,15 +14,22 @@ declare(strict_types=1);
namespace Ramsey\Uuid\Rfc4122;
use DateTimeImmutable;
use DateTimeInterface;
use Ramsey\Uuid\Codec\CodecInterface;
use Ramsey\Uuid\Converter\NumberConverterInterface;
use Ramsey\Uuid\Converter\TimeConverterInterface;
use Ramsey\Uuid\Exception\DateTimeException;
use Ramsey\Uuid\Exception\InvalidArgumentException;
use Ramsey\Uuid\Rfc4122\FieldsInterface as Rfc4122FieldsInterface;
use Ramsey\Uuid\Type\Integer as IntegerObject;
use Ramsey\Uuid\Uuid;
use Throwable;
use function hexdec;
use function str_pad;
use const STR_PAD_LEFT;
/**
* DCE Security version, or version 2, UUIDs include local domain identifier,
@@ -65,6 +72,41 @@ final class UuidV2 extends Uuid implements UuidInterface
parent::__construct($fields, $numberConverter, $codec, $timeConverter);
}
/**
* Returns a DateTimeInterface object representing the timestamp associated
* with the UUID
*
* It is important to note that a version 2 UUID suffers from some loss of
* fidelity of the timestamp, due to replacing the time_low field with the
* local identifier. When constructing the timestamp value for date
* purposes, we replace the local identifier bits with zeros. As a result,
* the timestamp can be off by a range of 0 to 429.4967295 seconds (or 7
* minutes, 9 seconds, and 496730 microseconds).
*
* Astute observers might note this value directly corresponds to 2^32, or
* 0xffffffff. The local identifier is 32-bits, and we have set each of
* these bits to 0, so the maximum range of timestamp drift is 0x00000000
* to 0xffffffff (counted in 100-nanosecond intervals).
*
* @return DateTimeImmutable A PHP DateTimeImmutable instance representing
* the timestamp of a version 2 UUID
*/
public function getDateTime(): DateTimeInterface
{
$time = $this->timeConverter->convertTime($this->fields->getTimestamp());
try {
return new DateTimeImmutable(
'@'
. $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);
}
}
/**
* Returns the local domain used to create this version 2 UUID
*/
+19
View File
@@ -185,6 +185,25 @@ class UuidFactory implements UuidFactoryInterface
$this->timeGenerator = $generator;
}
/**
* Returns the DCE Security generator used by this factory
*/
public function getDceSecurityGenerator(): DceSecurityGeneratorInterface
{
return $this->dceSecurityGenerator;
}
/**
* Sets the DCE Security generator to use for this factory
*
* @param DceSecurityGeneratorInterface $generator A generator to generate
* binary data, based on a local domain and local identifier
*/
public function setDceSecurityGenerator(DceSecurityGeneratorInterface $generator): void
{
$this->dceSecurityGenerator = $generator;
}
/**
* Returns the number converter used by this factory
*/
+12
View File
@@ -187,6 +187,18 @@ class FieldsTest extends TestCase
['00000000-0000-0000-0000-000000000000', 'getVariant', 0],
['00000000-0000-0000-0000-000000000000', 'getVersion', null],
['00000000-0000-0000-0000-000000000000', 'isNil', true],
['000001f5-5cde-21ea-8400-0242ac130003', 'getClockSeq', '0400'],
['000001f5-5cde-21ea-8400-0242ac130003', 'getClockSeqHiAndReserved', '84'],
['000001f5-5cde-21ea-8400-0242ac130003', 'getClockSeqLow', '00'],
['000001f5-5cde-21ea-8400-0242ac130003', 'getNode', '0242ac130003'],
['000001f5-5cde-21ea-8400-0242ac130003', 'getTimeHiAndVersion', '21ea'],
['000001f5-5cde-21ea-8400-0242ac130003', 'getTimeLow', '000001f5'],
['000001f5-5cde-21ea-8400-0242ac130003', 'getTimeMid', '5cde'],
['000001f5-5cde-21ea-8400-0242ac130003', 'getTimestamp', '1ea5cde00000000'],
['000001f5-5cde-21ea-8400-0242ac130003', 'getVariant', 2],
['000001f5-5cde-21ea-8400-0242ac130003', 'getVersion', 2],
['000001f5-5cde-21ea-8400-0242ac130003', 'isNil', false],
];
}
+88 -2
View File
@@ -4,16 +4,28 @@ declare(strict_types=1);
namespace Ramsey\Uuid\Test\Rfc4122;
use DateTimeInterface;
use Mockery;
use Ramsey\Uuid\Codec\CodecInterface;
use Ramsey\Uuid\Converter\Number\GenericNumberConverter;
use Ramsey\Uuid\Converter\NumberConverterInterface;
use Ramsey\Uuid\Converter\Time\GenericTimeConverter;
use Ramsey\Uuid\Converter\TimeConverterInterface;
use Ramsey\Uuid\Exception\InvalidArgumentException;
use Ramsey\Uuid\Generator\DceSecurityGenerator;
use Ramsey\Uuid\Generator\DefaultTimeGenerator;
use Ramsey\Uuid\Math\BrickMathCalculator;
use Ramsey\Uuid\Provider\Dce\SystemDceSecurityProvider;
use Ramsey\Uuid\Provider\Node\StaticNodeProvider;
use Ramsey\Uuid\Provider\Time\FixedTimeProvider;
use Ramsey\Uuid\Rfc4122\FieldsInterface;
use Ramsey\Uuid\Rfc4122\UuidV2;
use Ramsey\Uuid\Test\TestCase;
use Ramsey\Uuid\Type\Hexadecimal;
use Ramsey\Uuid\Type\Integer;
use Ramsey\Uuid\Type\Time;
use Ramsey\Uuid\Uuid;
use Ramsey\Uuid\UuidFactory;
class UuidV2Test extends TestCase
{
@@ -63,17 +75,40 @@ class UuidV2Test extends TestCase
public function testGetLocalDomainAndIdentifier(
int $domain,
Integer $identifier,
Time $time,
int $expectedDomain,
string $expectedDomainName,
string $expectedIdentifier
string $expectedIdentifier,
string $expectedTimestamp,
string $expectedTime
): void {
$calculator = new BrickMathCalculator();
$genericConverter = new GenericTimeConverter($calculator);
$numberConverter = new GenericNumberConverter($calculator);
$nodeProvider = new StaticNodeProvider(new Hexadecimal('1234567890ab'));
$timeProvider = new FixedTimeProvider($time);
$timeGenerator = new DefaultTimeGenerator($nodeProvider, $genericConverter, $timeProvider);
$dceProvider = new SystemDceSecurityProvider();
$dceGenerator = new DceSecurityGenerator($numberConverter, $timeGenerator, $dceProvider);
$factory = new UuidFactory();
$factory->setTimeGenerator($timeGenerator);
$factory->setDceSecurityGenerator($dceGenerator);
/** @var UuidV2 $uuid */
$uuid = Uuid::uuid2($domain, $identifier);
$uuid = $factory->uuid2($domain, $identifier);
/** @var FieldsInterface $fields */
$fields = $uuid->getFields();
$this->assertSame($expectedDomain, $uuid->getLocalDomain());
$this->assertSame($expectedDomainName, $uuid->getLocalDomainName());
$this->assertInstanceOf(Integer::class, $uuid->getLocalIdentifier());
$this->assertSame($expectedIdentifier, $uuid->getLocalIdentifier()->toString());
$this->assertSame($expectedTimestamp, $fields->getTimestamp()->toString());
$this->assertInstanceOf(DateTimeInterface::class, $uuid->getDateTime());
$this->assertSame($expectedTime, $uuid->getDateTime()->format('U.u'));
$this->assertSame('1334567890ab', $fields->getNode()->toString());
}
/**
@@ -85,37 +120,88 @@ class UuidV2Test extends TestCase
[
'domain' => Uuid::DCE_DOMAIN_PERSON,
'identifier' => new Integer('12345678'),
'time' => new Time(0, 0),
'expectedDomain' => 0,
'expectedDomainName' => 'person',
'expectedIdentifier' => '12345678',
'expectedTimestamp' => '1b21dd200000000',
'expectedTime' => '-32.723763',
],
[
'domain' => Uuid::DCE_DOMAIN_GROUP,
'identifier' => new Integer('87654321'),
'time' => new Time(0, 0),
'expectedDomain' => 1,
'expectedDomainName' => 'group',
'expectedIdentifier' => '87654321',
'expectedTimestamp' => '1b21dd200000000',
'expectedTime' => '-32.723763',
],
[
'domain' => Uuid::DCE_DOMAIN_ORG,
'identifier' => new Integer('1'),
'time' => new Time(0, 0),
'expectedDomain' => 2,
'expectedDomainName' => 'org',
'expectedIdentifier' => '1',
'expectedTimestamp' => '1b21dd200000000',
'expectedTime' => '-32.723763',
],
[
'domain' => Uuid::DCE_DOMAIN_PERSON,
'identifier' => new Integer('0'),
'time' => new Time(1583208664, 444109),
'expectedDomain' => 0,
'expectedDomainName' => 'person',
'expectedIdentifier' => '0',
'expectedTimestamp' => '1ea5d0500000000',
'expectedTime' => '1583208664.444109',
],
[
'domain' => Uuid::DCE_DOMAIN_PERSON,
'identifier' => new Integer('2147483647'),
'time' => new Time(1583208879, 500000),
'expectedDomain' => 0,
'expectedDomainName' => 'person',
'expectedIdentifier' => '2147483647',
// This time is the same as in the previous test because of the
// loss of precision by setting the lowest 32 bits to zeros.
'expectedTimestamp' => '1ea5d0500000000',
'expectedTime' => '1583208664.444109',
],
[
'domain' => Uuid::DCE_DOMAIN_PERSON,
'identifier' => new Integer('4294967295'),
'time' => new Time(1583208879, 500000),
'expectedDomain' => 0,
'expectedDomainName' => 'person',
'expectedIdentifier' => '4294967295',
// This time is the same as in the previous test because of the
// loss of precision by setting the lowest 32 bits to zeros.
'expectedTimestamp' => '1ea5d0500000000',
'expectedTime' => '1583208664.444109',
],
[
'domain' => Uuid::DCE_DOMAIN_PERSON,
'identifier' => new Integer('4294967295'),
'time' => new Time(1583209093, 940838),
'expectedDomain' => 0,
'expectedDomainName' => 'person',
'expectedIdentifier' => '4294967295',
// This time is the same as in the previous test because of the
// loss of precision by setting the lowest 32 bits to zeros.
'expectedTimestamp' => '1ea5d0500000000',
'expectedTime' => '1583208664.444109',
],
[
'domain' => Uuid::DCE_DOMAIN_PERSON,
'identifier' => new Integer('4294967295'),
'time' => new Time(1583209093, 940839),
'expectedDomain' => 0,
'expectedDomainName' => 'person',
'expectedIdentifier' => '4294967295',
'expectedTimestamp' => '1ea5d0600000000',
'expectedTime' => '1583209093.940838',
],
];
}