From ad6f0747bd3f00c16faebe3bba17408548a7e3bf Mon Sep 17 00:00:00 2001 From: Alex Farcas Date: Wed, 16 Mar 2016 15:36:38 +0200 Subject: [PATCH] Implemented timestamp first and timestamp last comb generators --- src/Codec/TimestampFirstCombCodec.php | 105 ++++++++++++++++++ src/Codec/TimestampLastCombCodec.php | 23 ++++ src/Generator/CombGenerator.php | 24 ++-- src/UuidFactory.php | 20 ++++ .../Encoder/TimestampFirstCombCodecTest.php | 84 ++++++++++++++ .../Encoder/TimestampLastCombCodecTest.php | 84 ++++++++++++++ tests/src/UuidTest.php | 49 +++++++- 7 files changed, 368 insertions(+), 21 deletions(-) create mode 100644 src/Codec/TimestampFirstCombCodec.php create mode 100644 src/Codec/TimestampLastCombCodec.php create mode 100644 tests/src/Encoder/TimestampFirstCombCodecTest.php create mode 100644 tests/src/Encoder/TimestampLastCombCodecTest.php diff --git a/src/Codec/TimestampFirstCombCodec.php b/src/Codec/TimestampFirstCombCodec.php new file mode 100644 index 0000000..c443438 --- /dev/null +++ b/src/Codec/TimestampFirstCombCodec.php @@ -0,0 +1,105 @@ + + * @license http://opensource.org/licenses/MIT MIT + * @link https://benramsey.com/projects/ramsey-uuid/ Documentation + * @link https://packagist.org/packages/ramsey/uuid Packagist + * @link https://github.com/ramsey/uuid GitHub + */ +namespace Ramsey\Uuid\Codec; + +use Ramsey\Uuid\UuidInterface; + +/** + * TimestampLastCombCodec encodes and decodes COMB UUIDs which have the timestamp as the first 48 bits. + * To be used with MySQL, PostgreSQL, Oracle. + */ +class TimestampFirstCombCodec extends StringCodec +{ + /** + * Encodes a UuidInterface as a string representation of a timestamp first COMB UUID + * + * @param UuidInterface $uuid + * + * @return string Hexadecimal string representation of a GUID + */ + public function encode(UuidInterface $uuid) + { + $sixPieceComponents = array_values($uuid->getFieldsHex()); + + $this->swapTimestampAndRandomBits($sixPieceComponents); + + return vsprintf( + '%08s-%04s-%04s-%02s%02s-%012s', + $sixPieceComponents + ); + } + + /** + * Encodes a UuidInterface as a binary representation of timestamp first COMB UUID + * + * @param UuidInterface $uuid + * + * @return string Binary string representation of timestamp first COMB UUID + */ + public function encodeBinary(UuidInterface $uuid) + { + $stringEncoding = $this->encode($uuid); + + return hex2bin(str_replace('-', '', $stringEncoding)); + } + + /** + * Decodes a string representation of timestamp first COMB UUID into a UuidInterface object instance + * + * @param string $encodedUuid + * + * @return UuidInterface + */ + public function decode($encodedUuid) + { + $fivePieceComponents = $this->extractComponents($encodedUuid); + + $this->swapTimestampAndRandomBits($fivePieceComponents); + + return $this->getBuilder()->build($this, $this->getFields($fivePieceComponents)); + } + + /** + * Decodes a binary representation of timestamp first COMB UUID into a UuidInterface object instance + * + * @param string $bytes + * + * @return UuidInterface + */ + public function decodeBytes($bytes) + { + return $this->decode(bin2hex($bytes)); + } + + /** + * Swaps the first 48 bits with the last 48 bits + * + * @param array $components An array of UUID components (the UUID exploded on its dashes) + * + * @return void + */ + protected function swapTimestampAndRandomBits(array &$components) + { + $last48Bits = $components[4]; + if (count($components) == 6) { + $last48Bits = $components[5]; + $components[5] = $components[0] . $components[1]; + } else { + $components[4] = $components[0] . $components[1]; + } + + $components[0] = substr($last48Bits, 0, 8); + $components[1] = substr($last48Bits, 8, 4); + } +} diff --git a/src/Codec/TimestampLastCombCodec.php b/src/Codec/TimestampLastCombCodec.php new file mode 100644 index 0000000..0cdd009 --- /dev/null +++ b/src/Codec/TimestampLastCombCodec.php @@ -0,0 +1,23 @@ + + * @license http://opensource.org/licenses/MIT MIT + * @link https://benramsey.com/projects/ramsey-uuid/ Documentation + * @link https://packagist.org/packages/ramsey/uuid Packagist + * @link https://github.com/ramsey/uuid GitHub + */ +namespace Ramsey\Uuid\Codec; + +/** + * TimestampLastCombCodec encodes and decodes COMB UUIDs which have the timestamp as the last 48 bits. + * To be used with MSSQL. + */ +class TimestampLastCombCodec extends StringCodec +{ + +} diff --git a/src/Generator/CombGenerator.php b/src/Generator/CombGenerator.php index 77cefb5..1a42776 100644 --- a/src/Generator/CombGenerator.php +++ b/src/Generator/CombGenerator.php @@ -24,6 +24,8 @@ use Ramsey\Uuid\Converter\NumberConverterInterface; */ class CombGenerator implements RandomGeneratorInterface { + const TIMESTAMP_BYTES = 6; + /** * @var RandomGeneratorInterface */ @@ -34,11 +36,6 @@ class CombGenerator implements RandomGeneratorInterface */ private $converter; - /** - * @var integer - */ - private $timestampBytes; - /** * Constructs a `CombGenerator` using a random-number generator and a number converter * @@ -49,7 +46,6 @@ class CombGenerator implements RandomGeneratorInterface { $this->converter = $numberConverter; $this->randomGenerator = $generator; - $this->timestampBytes = 6; } /** @@ -60,29 +56,25 @@ class CombGenerator implements RandomGeneratorInterface */ public function generate($length) { - if ($length < $this->timestampBytes || $length < 0) { + if ($length < self::TIMESTAMP_BYTES || $length < 0) { throw new \InvalidArgumentException('Length must be a positive integer.'); } $hash = ''; - if ($this->timestampBytes > 0 && $length > $this->timestampBytes) { - $hash = $this->randomGenerator->generate($length - $this->timestampBytes); + if (self::TIMESTAMP_BYTES > 0 && $length > self::TIMESTAMP_BYTES) { + $hash = $this->randomGenerator->generate($length - self::TIMESTAMP_BYTES); } - $lsbTime = str_pad($this->converter->toHex($this->timestamp()), $this->timestampBytes * 2, '0', STR_PAD_LEFT); + $lsbTime = str_pad($this->converter->toHex($this->timestamp()), self::TIMESTAMP_BYTES * 2, '0', STR_PAD_LEFT); - if ($this->timestampBytes > 0 && strlen($lsbTime) > $this->timestampBytes * 2) { - $lsbTime = substr($lsbTime, 0 - ($this->timestampBytes * 2)); - } - - return hex2bin(str_pad(bin2hex($hash), $length - $this->timestampBytes, '0')) . hex2bin($lsbTime); + return hex2bin(str_pad(bin2hex($hash), $length - self::TIMESTAMP_BYTES, '0') . $lsbTime); } /** * Returns current timestamp as integer, precise to 0.00001 seconds * - * @return integer + * @return string */ private function timestamp() { diff --git a/src/UuidFactory.php b/src/UuidFactory.php index 57730c6..b9195a4 100644 --- a/src/UuidFactory.php +++ b/src/UuidFactory.php @@ -80,6 +80,16 @@ class UuidFactory implements UuidFactoryInterface return $this->codec; } + /** + * Sets the UUID coder-decoder used by this factory + * + * @param CodecInterface $codec + */ + public function setCodec(CodecInterface $codec) + { + $this->codec = $codec; + } + /** * Returns the system node ID provider used by this factory * @@ -150,6 +160,16 @@ class UuidFactory implements UuidFactoryInterface $this->numberConverter = $converter; } + /** + * Returns the UUID builder this factory uses when creating `Uuid` instances + * + * @return UuidBuilderInterface $builder + */ + public function getUuidBuilder() + { + return $this->uuidBuilder; + } + /** * Sets the UUID builder this factory will use when creating `Uuid` instances * diff --git a/tests/src/Encoder/TimestampFirstCombCodecTest.php b/tests/src/Encoder/TimestampFirstCombCodecTest.php new file mode 100644 index 0000000..cf14a32 --- /dev/null +++ b/tests/src/Encoder/TimestampFirstCombCodecTest.php @@ -0,0 +1,84 @@ +builderMock = $this->getMock('Ramsey\Uuid\Builder\UuidBuilderInterface'); + $this->codec = new TimestampFirstCombCodec($this->builderMock); + } + + public function testEncoding() + { + $uuidMock = $this->getMock('Ramsey\Uuid\UuidInterface'); + $uuidMock->expects($this->any()) + ->method('getFieldsHex') + ->willReturn(array('ff6f8cb0', 'c57d', '11e1', '9b', '21', '0800200c9a66')); + $encodedUuid = $this->codec->encode($uuidMock); + + $this->assertSame('0800200c-9a66-11e1-9b21-ff6f8cb0c57d', $encodedUuid); + } + + public function testBinaryEncoding() + { + $uuidMock = $this->getMock('Ramsey\Uuid\UuidInterface'); + $uuidMock->expects($this->any()) + ->method('getFieldsHex') + ->willReturn(array('ff6f8cb0', 'c57d', '11e1', '9b', '21', '0800200c9a66')); + $encodedUuid = $this->codec->encodeBinary($uuidMock); + + $this->assertSame(hex2bin('0800200c9a6611e19b21ff6f8cb0c57d'), $encodedUuid); + } + + public function testDecoding() + { + $this->builderMock->expects($this->exactly(1)) + ->method('build') + ->with( + $this->codec, + array( + 'time_low' => 'ff6f8cb0', + 'time_mid' => 'c57d', + 'time_hi_and_version' => '11e1', + 'clock_seq_hi_and_reserved' => '9b', + 'clock_seq_low' => '21', + 'node' => '0800200c9a66' + ) + ); + $this->codec->decode('0800200c-9a66-11e1-9b21-ff6f8cb0c57d'); + } + + public function testBinaryDecoding() + { + $this->builderMock->expects($this->exactly(1)) + ->method('build') + ->with( + $this->codec, + array( + 'time_low' => 'ff6f8cb0', + 'time_mid' => 'c57d', + 'time_hi_and_version' => '11e1', + 'clock_seq_hi_and_reserved' => '9b', + 'clock_seq_low' => '21', + 'node' => '0800200c9a66' + ) + ); + $this->codec->decodeBytes(hex2bin('0800200c9a6611e19b21ff6f8cb0c57d')); + } +} diff --git a/tests/src/Encoder/TimestampLastCombCodecTest.php b/tests/src/Encoder/TimestampLastCombCodecTest.php new file mode 100644 index 0000000..219a9fe --- /dev/null +++ b/tests/src/Encoder/TimestampLastCombCodecTest.php @@ -0,0 +1,84 @@ +builderMock = $this->getMock('Ramsey\Uuid\Builder\UuidBuilderInterface'); + $this->codec = new TimestampLastCombCodec($this->builderMock); + } + + public function testEncoding() + { + $uuidMock = $this->getMock('Ramsey\Uuid\UuidInterface'); + $uuidMock->expects($this->any()) + ->method('getFieldsHex') + ->willReturn(array('0800200c', '9a66', '11e1', '9b', '21', 'ff6f8cb0c57d')); + $encodedUuid = $this->codec->encode($uuidMock); + + $this->assertSame('0800200c-9a66-11e1-9b21-ff6f8cb0c57d', $encodedUuid); + } + + public function testBinaryEncoding() + { + $uuidMock = $this->getMock('Ramsey\Uuid\UuidInterface'); + $uuidMock->expects($this->any()) + ->method('getHex') + ->willReturn('0800200c9a6611e19b21ff6f8cb0c57d'); + $encodedUuid = $this->codec->encodeBinary($uuidMock); + + $this->assertSame(hex2bin('0800200c9a6611e19b21ff6f8cb0c57d'), $encodedUuid); + } + + public function testDecoding() + { + $this->builderMock->expects($this->exactly(1)) + ->method('build') + ->with( + $this->codec, + array( + 'time_low' => '0800200c', + 'time_mid' => '9a66', + 'time_hi_and_version' => '11e1', + 'clock_seq_hi_and_reserved' => '9b', + 'clock_seq_low' => '21', + 'node' => 'ff6f8cb0c57d' + ) + ); + $this->codec->decode('0800200c-9a66-11e1-9b21-ff6f8cb0c57d'); + } + + public function testBinaryDecoding() + { + $this->builderMock->expects($this->exactly(1)) + ->method('build') + ->with( + $this->codec, + array( + 'time_low' => '0800200c', + 'time_mid' => '9a66', + 'time_hi_and_version' => '11e1', + 'clock_seq_hi_and_reserved' => '9b', + 'clock_seq_low' => '21', + 'node' => 'ff6f8cb0c57d' + ) + ); + $this->codec->decodeBytes(hex2bin('0800200c9a6611e19b21ff6f8cb0c57d')); + } +} diff --git a/tests/src/UuidTest.php b/tests/src/UuidTest.php index 1898889..24fc798 100644 --- a/tests/src/UuidTest.php +++ b/tests/src/UuidTest.php @@ -2,10 +2,12 @@ namespace Ramsey\Uuid\Test; +use Ramsey\Uuid\Codec\TimestampFirstCombCodec; +use Ramsey\Uuid\Codec\TimestampLastCombCodec; use Ramsey\Uuid\FeatureSet; +use Ramsey\Uuid\Generator\CombGenerator; use Ramsey\Uuid\Provider\Time\SystemTimeProvider; use Ramsey\Uuid\Provider\Time\FixedTimeProvider; -use Ramsey\Uuid\Generator\CombGenerator; use Ramsey\Uuid\Generator\RandomGeneratorFactory; use Ramsey\Uuid\Uuid; use Ramsey\Uuid\UuidFactory; @@ -817,23 +819,57 @@ class UuidTest extends TestCase } /** - * Tests that generated UUID's using COMB are sequential + * Tests that generated UUID's using timestamp last COMB are sequential * @return string */ - public function testUuid4Comb() + public function testUuid4TimestampLastComb() { $mock = $this->getMock('Ramsey\Uuid\Generator\RandomGeneratorInterface'); $mock->expects($this->any()) ->method('generate') ->willReturnCallback(function ($length) { - + // Makes first fields of UUIDs equal return str_pad('', $length, '0'); }); $factory = new UuidFactory(); $generator = new CombGenerator($mock, $factory->getNumberConverter()); + $codec = new TimestampLastCombCodec($factory->getUuidBuilder()); $factory->setRandomGenerator($generator); + $factory->setCodec($codec); + + $previous = $factory->uuid4(); + + for ($i = 0; $i < 1000; $i ++) { + usleep(10); + $uuid = $factory->uuid4(); + $this->assertGreaterThan($previous->toString(), $uuid->toString()); + + $previous = $uuid; + } + } + + /** + * Tests that generated UUID's using timestamp first COMB are sequential + * @return string + */ + public function testUuid4TimestampFirstComb() + { + $mock = $this->getMock('Ramsey\Uuid\Generator\RandomGeneratorInterface'); + $mock->expects($this->any()) + ->method('generate') + ->willReturnCallback(function ($length) { + + // Makes first fields of UUIDs equal + return str_pad('', $length, '0'); + }); + + $factory = new UuidFactory(); + $generator = new CombGenerator($mock, $factory->getNumberConverter()); + $codec = new TimestampFirstCombCodec($factory->getUuidBuilder()); + $factory->setRandomGenerator($generator); + $factory->setCodec($codec); $previous = $factory->uuid4(); @@ -852,7 +888,10 @@ class UuidTest extends TestCase public function testUuid4CombVersion() { $factory = new UuidFactory(); - $generator = new CombGenerator(RandomGeneratorFactory::getGenerator(), $factory->getNumberConverter()); + $generator = new CombGenerator( + RandomGeneratorFactory::getGenerator(), + $factory->getNumberConverter() + ); $factory->setRandomGenerator($generator);