Page Menu
Home
DevCentral
Search
Configure Global Search
Log In
Files
F3779726
D2660.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
34 KB
Referenced Files
None
Subscribers
None
D2660.diff
View Options
diff --git a/omnitools/src/Collections/BaseVector.php b/omnitools/src/Collections/BaseVector.php
--- a/omnitools/src/Collections/BaseVector.php
+++ b/omnitools/src/Collections/BaseVector.php
@@ -228,6 +228,35 @@
return new OmniString(implode($delimiter, $this->items));
}
+ public function bigrams () : Vector {
+ return $this->ngrams(2);
+ }
+
+ public function trigrams () : Vector {
+ return $this->ngrams(3);
+ }
+
+ public function ngrams (int $n) : Vector {
+ if ($n < 1) {
+ throw new InvalidArgumentException(
+ "n-grams must have a n strictly positive"
+ );
+ }
+
+ if ($n == 1) {
+ return Vector::from($this->map(fn ($value) => [$value]));
+ }
+
+ $len = $this->count();
+ if ($len <= $n) {
+ // We only have one slice.
+ return Vector::from([$this->items]);
+ }
+
+ return Vector::range(0, $len - $n)
+ ->map(fn($i) => array_slice($this->items, $i, $n));
+ }
+
///
/// ArrayAccess
/// Interface to provide accessing objects as arrays.
diff --git a/omnitools/src/Collections/Vector.php b/omnitools/src/Collections/Vector.php
--- a/omnitools/src/Collections/Vector.php
+++ b/omnitools/src/Collections/Vector.php
@@ -46,6 +46,10 @@
->explode($delimiter, $limit);
}
+ public static function range (int $start, int $end, int $step = 1) : self {
+ return new Vector(range($start, $end, $step));
+ }
+
///
/// HOF :: specialized
///
diff --git a/omnitools/src/DateTime/UUIDv1TimeStamp.php b/omnitools/src/DateTime/UUIDv1TimeStamp.php
new file mode 100644
--- /dev/null
+++ b/omnitools/src/DateTime/UUIDv1TimeStamp.php
@@ -0,0 +1,129 @@
+<?php
+
+namespace Keruald\OmniTools\DateTime;
+
+use Keruald\OmniTools\Collections\BitsVector;
+
+use DateTimeInterface;
+use InvalidArgumentException;
+
+class UUIDv1TimeStamp {
+
+ private BitsVector $bits;
+
+ ///
+ /// Constants
+ ///
+
+ const OFFSET_BETWEEN_GREGORIAN_AND_UNIX_EPOCH = 0x01B21DD213814000;
+
+ private function __construct (?BitsVector $bits = null) {
+ $this->bits = $bits ?: BitsVector::new(60);
+ }
+
+ public static function fromUUIDv1 (string $uuid) : self {
+ $uuidBits = BitsVector::fromDecoratedHexString($uuid);
+ $timestamp = new self();
+
+ // Reads 60 bits timestamp from UUIDv1
+ // UUIDv1 Timestamp
+ // time_low 0-31 28-59
+ // time_mid 32-47 12-27
+ // time_high 52-63 0-11
+ $timestamp->bits
+ ->replace($uuidBits->slice(52, 12), 0, 12)
+ ->replace($uuidBits->slice(32, 16), 12, 16)
+ ->replace($uuidBits, 28, 32);
+
+ return $timestamp;
+ }
+
+ public static function fromUUIDv6 (string $uuid) : self {
+ $uuidBits = BitsVector::fromDecoratedHexString($uuid);
+ $timestamp = new self();
+
+ // Timestamp UUIv6
+ // time_high 0-31 0-31
+ // time_mid 32-47 32-47
+ // time_low 48-60 52-63
+ $timestamp->bits
+ ->replace($uuidBits, 0, 32)
+ ->replace($uuidBits->slice(32, 16), 32, 16)
+ ->replace($uuidBits->slice(52, 12), 48, 12);
+
+ return $timestamp;
+ }
+
+ public static function fromBits (BitsVector $bits) : self {
+ if ($bits->count() !== 60) {
+ throw new InvalidArgumentException("Timestamp must be 60 bits.");
+ }
+
+ return new self($bits);
+ }
+
+ public static function fromTimeStamp (int $timestamp) : self {
+ $bits = BitsVector::new(60)
+ ->copyInteger($timestamp, 0, 60);
+
+ return new self($bits);
+ }
+
+ public static function fromUnixTime (float|int $time) : self {
+ $timestamp = (int)($time * 1E7)
+ + self::OFFSET_BETWEEN_GREGORIAN_AND_UNIX_EPOCH;
+
+ return self::fromTimeStamp($timestamp);
+ }
+
+ public static function fromDateTime(DateTimeInterface $dateTime) : self {
+ return self::fromUnixTime($dateTime->getTimestamp());
+ }
+
+ public static function now () : self {
+ return self::fromUnixTime(microtime(true));
+ }
+
+ ///
+ /// Helper methods
+ ///
+
+ public function writeToUUIDv1 (BitsVector $uuidBits) : void {
+ // Write 60 bits timestamp to UUIDv1
+ // UUIDv1 Timestamp
+ // time_low 0-31 28-59
+ // time_mid 32-47 12-27
+ // time_high 52-63 0-11
+ $uuidBits
+ ->replace($this->bits->slice(28, 32), 0, 32)
+ ->replace($this->bits->slice(12, 16), 32, 16)
+ ->replace($this->bits, 52, 12);
+ }
+
+ public function writeToUUIDv6 (BitsVector $uuidBits) : void {
+ // Write 60 bits timestamp to UUIDv6
+ // Timestamp UUIv6
+ // time_high 0-31 0-31
+ // time_mid 32-47 32-47
+ // time_low 48-59 52-63
+ $uuidBits
+ ->replace($this->bits, 0, 32)
+ ->replace($this->bits->slice(32, 16), 32, 16)
+ ->replace($this->bits->slice(48, 12), 52, 12);
+ }
+
+ ///
+ /// Properties
+ ///
+
+ public function toBitsVector () : BitsVector {
+ return $this->bits;
+ }
+
+ public function toUnixTime () : int {
+ return (int)floor(
+ ($this->bits->toInteger() - self::OFFSET_BETWEEN_GREGORIAN_AND_UNIX_EPOCH) / 1E7
+ );
+ }
+
+}
diff --git a/omnitools/src/DateTime/UUIDv7TimeStamp.php b/omnitools/src/DateTime/UUIDv7TimeStamp.php
new file mode 100644
--- /dev/null
+++ b/omnitools/src/DateTime/UUIDv7TimeStamp.php
@@ -0,0 +1,89 @@
+<?php
+
+namespace Keruald\OmniTools\DateTime;
+
+use Keruald\OmniTools\Collections\BitsVector;
+
+use DateTimeInterface;
+use InvalidArgumentException;
+
+class UUIDv7TimeStamp {
+
+ private BitsVector $bits;
+
+ ///
+ /// Constructors
+ ///
+
+ private function __construct (?BitsVector $bits = null) {
+ $this->bits = $bits ?: BitsVector::new(48);
+ }
+
+ public static function fromUUIDv7 (string $uuid) : self {
+ $uuidBits = BitsVector::fromDecoratedHexString($uuid);
+
+ return new self($uuidBits->slice(0, 48));
+ }
+
+ public static function fromBits (BitsVector $bits) : self {
+ if ($bits->count() !== 48) {
+ throw new InvalidArgumentException("UUIDv7 timestamps must be 48 bits long, 32 for unixtime, 16 for milliseconds.");
+ }
+
+ return new self($bits);
+ }
+
+ public static function fromInteger (int $value) : self {
+ $bits = self::generateBitsVector($value);
+
+ return new self($bits);
+ }
+
+ public static function fromUnixTime (int $time, int $ms = 0) : self {
+ $value = self::computeIntegerValue($time, $ms);
+ return self::fromInteger($value);
+ }
+
+ public static function fromDateTime(DateTimeInterface $dateTime) : self {
+ return self::fromUnixTime($dateTime->getTimestamp());
+ }
+
+ public static function now () : self {
+ [$micro, $time] = explode(" ", microtime());
+ return self::fromUnixTime($time, floor($micro * 1000));
+ }
+
+ ///
+ /// Helper methods to build a timestamp
+ ///
+
+ private static function computeIntegerValue (int $time, int $ms) : int {
+ return $time * 1000 + $ms;
+ }
+
+ private static function generateBitsVector (int $time): BitsVector {
+ $timeBits = BitsVector::fromInteger($time);
+ $len = $timeBits->count();
+
+ if ($len == 48) {
+ return $timeBits;
+ }
+
+ if ($len > 48) {
+ trigger_error("Timestamp is truncated to the least significative 48 bits.", E_USER_WARNING);
+ return $timeBits->slice($len - 48, 48);
+ }
+
+ return BitsVector::new(48)
+ ->replace($timeBits, 48 - $len, 48);
+ }
+
+ ///
+ /// Properties
+ ///
+
+ public function toBitsVector () : BitsVector {
+ return $this->bits;
+ }
+
+}
diff --git a/omnitools/src/Identifiers/Random.php b/omnitools/src/Identifiers/Random.php
--- a/omnitools/src/Identifiers/Random.php
+++ b/omnitools/src/Identifiers/Random.php
@@ -135,4 +135,33 @@
throw new InvalidArgumentException();
}
+ /**
+ * @throw InvalidArgumentException if [$min, $max] doesn't have at least $count elements.
+ * @throws Exception if an appropriate source of randomness cannot be found.
+ */
+ public static function generateIntegerMonotonicSeries (
+ int $min, int $max, int $count
+ ) : array {
+ if ($max - $min < $count) {
+ throw new InvalidArgumentException("Can't build a monotonic series of n elements if the range has fewer elements.");
+ }
+
+ $series = [];
+
+ $n = 0;
+ while ($n < $count) {
+ $candidate = random_int($min, $max);
+
+ if (in_array($candidate, $series)) {
+ continue;
+ }
+
+ $series[] = $candidate;
+ $n++;
+ }
+
+ sort($series);
+ return $series;
+
+ }
}
diff --git a/omnitools/src/Identifiers/UUID.php b/omnitools/src/Identifiers/UUID.php
--- a/omnitools/src/Identifiers/UUID.php
+++ b/omnitools/src/Identifiers/UUID.php
@@ -4,11 +4,88 @@
namespace Keruald\OmniTools\Identifiers;
use Exception;
+use InvalidArgumentException;
+
+use Keruald\OmniTools\Collections\BitsVector;
+use Keruald\OmniTools\Collections\Vector;
+use Keruald\OmniTools\DateTime\UUIDv1TimeStamp;
+use Keruald\OmniTools\DateTime\UUIDv7TimeStamp;
class UUID {
const UUID_REGEXP = "/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/";
+ const MAX_12 = 4095;
+ const MAX_48 = 281_474_976_710_655;
+ const MAX_62 = 4_611_686_018_427_387_903;
+
+ const UUIDV7_QUANTITY_PER_MS = 63;
+
+ ///
+ /// Public constants from RFC 4122 and draft-peabody-dispatch-new-uuid-format-03
+ ///
+
+ public const NIL = "00000000-0000-0000-0000-000000000000";
+ public const MAX = "ffffffff-ffff-ffff-ffff-ffffffffffff";
+
+ ///
+ /// RFC 4122 - UUIDv1
+ ///
+
+ /**
+ * @param int $clk_seq_hi_res
+ * @param int $clk_seq_low
+ * @param string $mac The node information, normally the MAC address ; if
+ * omitted, a random value will be generated.
+ *
+ * @return string
+ * @throws Exception if $mac is not specified, and an appropriate source of randomness cannot be found.
+ * @throws InvalidArgumentException if $mac is specified and doesn't contain exactly 12 hexadecimal characters.
+ */
+ public static function UUIDv1 (
+ string $mac = "",
+ int $clk_seq_hi_res = 0,
+ int $clk_seq_low = 0,
+ ) : string {
+ $node = match ($mac) {
+ "" => BitsVector::random(48),
+ default => BitsVector::fromDecoratedHexString($mac),
+ };
+
+ return self::UUIDv1FromValues(
+ UUIDv1TimeStamp::now(),
+ $clk_seq_hi_res,
+ $clk_seq_low,
+ $node,
+ );
+ }
+
+ public static function UUIDv1FromValues (
+ UUIDv1TimeStamp $timestamp,
+ int $clk_seq_hi_res,
+ int $clk_seq_low,
+ BitsVector $node,
+ ) : string {
+ if ($node->count() !== 48) {
+ throw new InvalidArgumentException("Node information must be 48 bits, ideally from a 12 characters hexadecimal MAC address string.");
+ }
+
+ $bits = BitsVector::new(128);
+
+ $timestamp->writeToUUIDv1($bits);
+ $bits->copyInteger(1, 48, 4); // version 1 from UUIDv1
+ $bits->copyInteger(2, 64, 2); // variant 2
+ $bits->copyInteger($clk_seq_hi_res, 66, 6);
+ $bits->copyInteger($clk_seq_low, 72, 8);
+ $bits->replace($node, 80, 48);
+
+ return self::reformat($bits->toHexString());
+ }
+
+ ///
+ /// RFC 4122 - UUIDv4
+ ///
+
/**
* @return string An RFC 4122 compliant v4 UUID
* @throws Exception if an appropriate source of randomness cannot be found.
@@ -45,8 +122,236 @@
return str_replace("-", "", self::UUIDv4());
}
+ ///
+ /// draft-peabody-dispatch-new-uuid-format-03 - UUIDv6
+ ///
+
+ /**
+ * @param int $clk_seq_hi_res
+ * @param int $clk_seq_low
+ * @param string $mac The node information, normally the MAC address ; if
+ * omitted, a random value will be generated.
+ *
+ * @return string
+ * @throws Exception if $mac is not specified, and an appropriate source of randomness cannot be found.
+ * @throws InvalidArgumentException if $mac is specified and doesn't contain exactly 12 hexadecimal characters.
+ */
+ public static function UUIDv6 (
+ string $mac = "",
+ int $clk_seq_hi_res = 0,
+ int $clk_seq_low = 0,
+ ) : string {
+ $node = match ($mac) {
+ "" => BitsVector::random(48),
+ default => BitsVector::fromDecoratedHexString($mac),
+ };
+
+ return self::UUIDv6FromValues(
+ UUIDv1TimeStamp::now(),
+ $clk_seq_hi_res,
+ $clk_seq_low,
+ $node,
+ );
+ }
+
+ public static function UUIDv6FromValues (
+ UUIDv1TimeStamp $timestamp,
+ int $clk_seq_hi_res,
+ int $clk_seq_low,
+ BitsVector $node,
+ ) : string {
+ if ($node->count() !== 48) {
+ throw new InvalidArgumentException("Node information must be 48 bits, ideally from a 12 characters hexadecimal MAC address string.");
+ }
+
+ $bits = BitsVector::new(128);
+
+ $timestamp->writeToUUIDv6($bits);
+ $bits->copyInteger(6, 48, 4); // version 6 from UUIDv6
+ $bits->copyInteger(2, 64, 2); // variant 2
+ $bits->copyInteger($clk_seq_hi_res, 66, 6);
+ $bits->copyInteger($clk_seq_low, 72, 8);
+ $bits->replace($node, 80, 48);
+
+ return self::reformat($bits->toHexString());
+ }
+
+ public static function UUIDv1ToUUIDv6 (string $uuid) : string {
+ $bits = BitsVector::fromDecoratedHexString($uuid);
+ UUIDv1TimeStamp::fromUUIDv1($uuid)->writeToUUIDv6($bits);
+
+ // Version 6 for UUIDv6, bits 48-51
+ $bits->copyInteger(6, 48, 4);
+
+ return self::reformat($bits->toHexString());
+ }
+
+ public static function UUIDv6ToUUIDv1 (string $uuid) : string {
+ $bits = BitsVector::fromDecoratedHexString($uuid);
+ UUIDv1TimeStamp::fromUUIDv6($uuid)->writeToUUIDv1($bits);
+
+ // Version 1 for UUIDv6, bits 48-51
+ $bits->copyInteger(1, 48, 4);
+
+ return self::reformat($bits->toHexString());
+ }
+
+ ///
+ /// draft-peabody-dispatch-new-uuid-format-03 - UUIDv7
+ ///
+
+ /**
+ * @throws Exception if an appropriate source of randomness cannot be found.
+ *@see UUID::batchOfUUIDv7()
+ */
+ public static function UUIDv7 () : string {
+ return self::UUIDv7FromBits(
+ UUIDv7TimeStamp::now()->toBitsVector(),
+ random_int(0, self::MAX_12),
+ random_int(0, self::MAX_62),
+ );
+ }
+
+ /**
+ * A batch of UUIDv7 with monotonicity warranty.
+ *
+ * @param int $count The number of UUIDv7 to generate
+ *
+ * @return array
+ */
+ public static function batchOfUUIDv7 (int $count) : array {
+ if ($count > self::UUIDV7_QUANTITY_PER_MS) {
+ // We only have 12 bits available in random A.
+ // Divide in smaller batches to avoid to touch random B.
+
+ $batch = [];
+ $stillToGenerateCount = $count;
+ while ($stillToGenerateCount > 0) {
+ $n = min($stillToGenerateCount, self::UUIDV7_QUANTITY_PER_MS);
+ array_push($batch, ...self::batchOfUUIDv7($n));
+
+ $stillToGenerateCount -= self::UUIDV7_QUANTITY_PER_MS;
+ usleep(1000); // That will increment the timestamp.
+ }
+ return $batch;
+ }
+
+ $timestamp = UUIDv7TimeStamp::now()->toBitsVector();
+ return self::getSeriesRandomA($count)
+ ->map(fn($a) => self::UUIDv7FromBits(
+ $timestamp,
+ $a,
+ random_int(0, self::MAX_62),
+ ))
+ ->toArray();
+ }
+
+ private static function getSeriesRandomA (int $count) : Vector {
+ return Vector::from(Random::generateIntegerMonotonicSeries(
+ 0, self::MAX_12, $count
+ ));
+ }
+
+
+ public static function UUIDv7FromBits (
+ BitsVector $unixTimestampMs,
+ int $randA,
+ int $randB
+ ) : string {
+ if ($unixTimestampMs->count() != 48) {
+ throw new InvalidArgumentException("UUIDv7 timestamps MUST be 48 bits long.");
+ }
+
+ $bits = BitsVector::new(128)
+ ->replace($unixTimestampMs, 0, 48)
+ ->copyInteger($randA, 52, 12)
+ ->copyInteger($randB, 66, 62)
+ ->copyInteger(7, 48, 4) // version (bits 48 -> 51)
+ ->copyInteger(2, 64, 2); // variant (bits 64 -> 65)
+
+ return self::reformat($bits->toHexString());
+ }
+
+ public static function UUIDv7FromValues (
+ int $unixTimestampMs,
+ int $randA,
+ int $randB
+ ) : string {
+ $bits = UUIDv7TimeStamp::fromInteger($unixTimestampMs)->toBitsVector();
+
+ return self::UUIDv7FromBits($bits, $randA, $randB);
+ }
+
+ ///
+ /// draft-peabody-dispatch-new-uuid-format-03 - UUIDv6
+ ///
+
+ /**
+ * Generate a UUIDv8 with three custom values.
+ *
+ * The UUIDv8 lets the implementation decide of the bits' layout.
+ * This implementation will write values like big-endian unsigned numbers.
+ */
+ public static function UUIDv8 (int $a, int $b, int $c) : string {
+ if ($a > self::MAX_48) {
+ throw new InvalidArgumentException("custom_a field is limited to 48 bits.");
+ }
+
+ if ($b > self::MAX_12) {
+ throw new InvalidArgumentException("custom_b field is limited to 12 bits.");
+ }
+
+ if ($c > self::MAX_62) {
+ throw new InvalidArgumentException("custom_c field is limited to 62 bits.");
+ }
+
+ $bits = BitsVector::new(128)
+ ->copyInteger($a, 0, 48) // bits 0 -> 47
+ ->copyInteger($b, 52, 12) // bits 52 -> 63
+ ->copyInteger($c, 66, 62); // bits 66 -> 127
+
+ $bits[48] = 1; // bits 48 -> 51 represent version 8 (1000)
+ $bits[64] = 1; // bits 64 -> 65 represent variant 2 (10)
+
+ return self::reformat($bits->toHexString());
+ }
+
+ ///
+ /// Helper methods
+ ///
+
+ public static function reformat (string $uuid) : string {
+ $uuid = strtolower($uuid);
+
+ return match (strlen($uuid)) {
+ 32 => implode("-", [
+ substr($uuid, 0, 8),
+ substr($uuid, 8, 4),
+ substr($uuid, 12, 4),
+ substr($uuid, 16, 4),
+ substr($uuid, 20, 12),
+ ]),
+ 36 => $uuid,
+ default => throw new InvalidArgumentException("UUID must be 32 or 36 characters long."),
+ };
+ }
+
public static function isUUID ($string) : bool {
return (bool)preg_match(self::UUID_REGEXP, $string);
}
+ public static function getVersion (string $uuid) : int {
+ // bits 48 -> 51 represent version
+ return BitsVector::fromDecoratedHexString($uuid)
+ ->slice(48, 4)
+ ->toInteger();
+ }
+
+ public static function getVariant (string $uuid) : int {
+ // bits 64 -> 65 represent variant
+ return BitsVector::fromDecoratedHexString($uuid)
+ ->slice(64, 2)
+ ->toInteger();
+ }
+
}
diff --git a/omnitools/tests/Collections/VectorTest.php b/omnitools/tests/Collections/VectorTest.php
--- a/omnitools/tests/Collections/VectorTest.php
+++ b/omnitools/tests/Collections/VectorTest.php
@@ -249,6 +249,83 @@
$this->assertEquals(["a.b.c"], $actual->toArray());
}
+ ///
+ /// n-grams
+ ///
+
+ public function testBigrams() : void {
+ $expected = Vector::from([
+ [1, 2],
+ [2, 3],
+ [3, 4],
+ [4, 5],
+ ]);
+
+ $this->assertEquals($expected, $this->vector->bigrams());
+ }
+
+ public function testTrigrams() : void {
+ $expected = Vector::from([
+ [1, 2, 3],
+ [2, 3, 4],
+ [3, 4, 5],
+ ]);
+
+ $this->assertEquals($expected, $this->vector->trigrams());
+ }
+
+ public function testNgrams() : void {
+ $expected = Vector::from([
+ [1, 2, 3, 4],
+ [2, 3, 4, 5],
+ ]);
+
+ $this->assertEquals($expected, $this->vector->ngrams(4));
+ }
+
+ public function testNgramsWithN1 () : void {
+ $expected = Vector::from([
+ [1],
+ [2],
+ [3],
+ [4],
+ [5],
+ ]);
+
+ $this->assertEquals($expected, $this->vector->ngrams(1));
+ }
+
+ private function provideLowN () : iterable {
+ yield [0];
+ yield [-1];
+ yield [PHP_INT_MIN];
+ }
+
+ /**
+ * @dataProvider provideLowN
+ */
+ public function testNgramsWithTooLowN ($n) : void {
+ $this->expectException(InvalidArgumentException::class);
+ $this->vector->ngrams($n);
+ }
+
+ private function provideLargeN () : iterable {
+ yield [5];
+ yield [6];
+ yield [PHP_INT_MAX];
+ }
+
+ /**
+ * @dataProvider provideLargeN
+ */
+ public function testNgramsWithTooLargeN ($n) : void {
+ $expected = Vector::from([
+ [1, 2, 3, 4, 5],
+ ]);
+
+ $this->assertEquals($expected, $this->vector->ngrams($n));
+ }
+
///
/// ArrayAccess
///
diff --git a/omnitools/tests/DateTime/UUIDv1TimeStampTest.php b/omnitools/tests/DateTime/UUIDv1TimeStampTest.php
new file mode 100644
--- /dev/null
+++ b/omnitools/tests/DateTime/UUIDv1TimeStampTest.php
@@ -0,0 +1,63 @@
+<?php
+
+namespace Keruald\OmniTools\Tests\DateTime;
+
+use DateTime;
+use InvalidArgumentException;
+
+use Keruald\OmniTools\Collections\BitsVector;
+use Keruald\OmniTools\DateTime\UUIDv1TimeStamp;
+use PHPUnit\Framework\TestCase;
+
+class UUIDv1TimeStampTest extends TestCase {
+
+ public function testToUnixTime() {
+ $time = time();
+
+ $actual = UUIDv1TimeStamp::fromUnixTime($time)->toUnixTime();
+ $this->assertEquals($time, $actual);
+ }
+
+ public function testFromBits() {
+ $bits = BitsVector::fromInteger(0x1EC9414C232AB00)
+ ->shapeCapacity(60);
+ $timestamp = UUIDv1TimeStamp::fromBits($bits);
+
+ $this->assertEquals(1645557742, $timestamp->toUnixTime());
+ }
+
+ public function testFromUnixTime() {
+ $timestamp = UUIDv1TimeStamp::fromUnixTime(1645557742);
+ $this->assertEquals(1645557742, $timestamp->toUnixTime());
+ }
+
+ public function testFromDateTime() {
+ $time = DateTime::createFromFormat(
+ "Y-m-d H:i:s",
+ '2022-02-22 19:22:22'
+ );
+ $timestamp = UUIDv1TimeStamp::fromDateTime($time);
+
+ $this->assertEquals(1645557742, $timestamp->toUnixTime());
+ }
+
+ public function testFromBitsWhenCountIsWrong() {
+ $this->expectException(InvalidArgumentException::class);
+
+ $bits = BitsVector::new(0); // too small, we need 60
+ UUIDv1TimeStamp::fromBits($bits);
+ }
+
+ public function testToBitsVector() {
+ $expected = BitsVector::fromInteger(0x1EC9414C232AB00)
+ ->shapeCapacity(60);
+
+ $timestamp = UUIDv1TimeStamp::fromUnixTime(1645557742);
+
+ $this->assertEquals(
+ $expected->toArray(),
+ $timestamp->toBitsVector()->toArray(),
+ );
+ }
+
+}
diff --git a/omnitools/tests/DateTime/UUIDv7TimeStampTest.php b/omnitools/tests/DateTime/UUIDv7TimeStampTest.php
new file mode 100644
--- /dev/null
+++ b/omnitools/tests/DateTime/UUIDv7TimeStampTest.php
@@ -0,0 +1,82 @@
+<?php
+
+namespace Keruald\OmniTools\Tests\DateTime;
+
+use DateTime;
+use InvalidArgumentException;
+
+use Keruald\OmniTools\Collections\BitsVector;
+use Keruald\OmniTools\DateTime\UUIDv7TimeStamp;
+use PHPUnit\Framework\TestCase;
+
+class UUIDv7TimeStampTest extends TestCase {
+
+ const MAX_48 = 2**48 - 1;
+
+ public function testFromUUIDv7 () {
+ // UUID example from draft-peabody-dispatch-new-uuid-format-03 B.2
+ $uuid = "017F21CF-D130-7CC3-98C4-DC0C0C07398F";
+
+ $expected = BitsVector::fromInteger(0x017F21CFD130)
+ ->shapeCapacity(48);
+
+ $timestamp = UUIDv7TimeStamp::fromUUIDv7($uuid);
+ self::assertEquals(
+ $expected->toArray(),
+ $timestamp->toBitsVector()->toArray(),
+ );
+ }
+
+ public function testFromBits () {
+ $bits = BitsVector::fromInteger(0x017F21CFD130)
+ ->shapeCapacity(48);
+ $timestamp = UUIDv7TimeStamp::fromBits($bits);
+
+ $this->assertEquals(48, $timestamp->toBitsVector()->count());
+ $this->assertSame(
+ $bits->toArray(),
+ $timestamp->toBitsVector()->toArray()
+ );
+ }
+
+ public function testFromBitsWithWrongNumber () {
+ $this->expectException(InvalidArgumentException::class);
+
+ $bits = BitsVector::new(0); // too small, we need 48
+ UUIDv7TimeStamp::fromBits($bits);
+ }
+ public function testFromIntegerWith48Bits() {
+ $timestamp = UUIDv7TimeStamp::fromInteger(self::MAX_48);
+
+ $expected = array_fill(0, 48, 1);
+ $this->assertEquals($expected, $timestamp->toBitsVector()->toArray());
+
+ }
+ public function testFromIntegerWithTruncatedPrecision() {
+ $timestamp = UUIDv7TimeStamp::fromInteger(PHP_INT_MAX);
+
+ $expected = array_fill(0, 48, 1);
+ $this->assertSame($expected, $timestamp->toBitsVector()->toArray());
+ }
+
+ public function testFromDateTime() {
+ $time = DateTime::createFromFormat(
+ "Y-m-d H:i:s",
+ '2022-02-22 14:22:22'
+ );
+ $timestamp = UUIDv7TimeStamp::fromDateTime($time);
+
+ $actual = $timestamp->toBitsVector()->toInteger();
+ $this->assertEquals(0x017F21CFD130, $actual);
+ }
+
+ public function testToUnixTime() {
+ $time = time();
+
+ $actual = UUIDv7TimeStamp::fromUnixTime($time)
+ ->toBitsVector()
+ ->toInteger() / 1000;
+ $this->assertEquals($time, $actual);
+ }
+
+}
diff --git a/omnitools/tests/Identifiers/UUIDTest.php b/omnitools/tests/Identifiers/UUIDTest.php
--- a/omnitools/tests/Identifiers/UUIDTest.php
+++ b/omnitools/tests/Identifiers/UUIDTest.php
@@ -3,11 +3,47 @@
namespace Keruald\OmniTools\Tests\Identifiers;
+use InvalidArgumentException;
+
+use Keruald\OmniTools\Collections\BitsVector;
+use Keruald\OmniTools\Collections\Vector;
+use Keruald\OmniTools\DateTime\UUIDv1TimeStamp;
use Keruald\OmniTools\Identifiers\UUID;
use Phpunit\Framework\TestCase;
class UUIDTest extends TestCase {
+ public function testUUIDv1 () : void {
+ $uuid = UUID::UUIDv1();
+
+ $this->assertEquals(
+ 36, strlen($uuid),
+ "UUID size must be 36 characters."
+ );
+
+ $re = "/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/";
+ $this->assertMatchesRegularExpression($re, $uuid);
+
+ $this->assertEquals(1, UUID::getVersion($uuid));
+ }
+
+ public function testUUIDV1WithMac () : void {
+ $uuid = UUID::UUIDv1("00-00-5E-00-53-00");
+
+ $macFromUUID = BitsVector::fromDecoratedHexString($uuid)
+ ->slice(80, 48)
+ ->toBytesArray();
+
+ $this->assertSame([0, 0, 0x5E, 0, 0x53, 0], $macFromUUID);
+ }
+
+ public function testUUIDv1FromValuesWithBadCount () : void {
+ $this->expectException(InvalidArgumentException::class);
+
+ $node = BitsVector::new(0); // too small, must be 48
+ UUID::UUIDv1FromValues(UUIDv1TimeStamp::now(), 0, 0, $node);
+ }
+
public function testUUIDv4 () : void {
$uuid = UUID::UUIDv4();
@@ -36,6 +72,130 @@
$this->assertNotEquals(UUID::UUIDv4(), UUID::UUIDv4());
}
+ public function testUUIDv6 () : void {
+ $uuid = UUID::UUIDv6();
+
+ $this->assertEquals(
+ 36, strlen($uuid),
+ "UUID size must be 36 characters."
+ );
+
+ $re = "/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/";
+ $this->assertMatchesRegularExpression($re, $uuid);
+
+ $this->assertEquals(6, UUID::getVersion($uuid));
+ }
+
+ public function testUUIDV6WithMac () : void {
+ $uuid = UUID::UUIDv6("00-00-5E-00-53-00");
+
+ $macFromUUID = BitsVector::fromDecoratedHexString($uuid)
+ ->slice(80, 48)
+ ->toBytesArray();
+
+ $this->assertSame([0, 0, 0x5E, 0, 0x53, 0], $macFromUUID);
+ }
+
+ public function testUUIDv6FromValuesWithBadCount () : void {
+ $this->expectException(InvalidArgumentException::class);
+
+ $node = BitsVector::new(0); // too small, must be 48
+ UUID::UUIDv6FromValues(UUIDv1TimeStamp::now(), 0, 0, $node);
+ }
+
+ public function testUUIDv8 () : void {
+ $this->assertEquals(
+ "320c3d4d-cc00-875b-8ec9-32d5f69181c0",
+ UUID::UUIDv8(0x320C3D4DCC00, 0x75B, 0xEC932D5F69181C0)
+ );
+ }
+
+ public function provideUUIDV8OverflowValues () : iterable {
+ yield [PHP_INT_MAX, 0x75B, 0xEC932D5F69181C0];
+ yield [0x320C3D4DCC00, PHP_INT_MAX, 0xEC932D5F69181C0];
+ yield [0x320C3D4DCC00, 0x75B, PHP_INT_MAX];
+ }
+
+ /**
+ * @dataProvider provideUUIDV8OverflowValues
+ */
+ public function testUUIDV8WithOverflowValues ($a, $b, $c) : void {
+ $this->expectException(InvalidArgumentException::class);
+ UUID::UUIDv8($a, $b, $c);
+ }
+
+
+ public function testUUIDv7FromValues () : void {
+ $this->assertEquals(
+ "017f21cf-d130-7cc3-98c4-dc0c0c07398f",
+ UUID::UUIDv7FromValues(0x017F21CFD130, 0xCC3, 0x18C4DC0C0C07398F)
+ );
+ }
+
+ public function testUUIDv7 () : void {
+ $uuid = UUID::UUIDv7();
+
+ $this->assertEquals(
+ 36, strlen($uuid),
+ "UUID size must be 36 characters."
+ );
+
+ $re = "/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/";
+ $this->assertMatchesRegularExpression($re, $uuid);
+ }
+
+ public function testUUIDv7FromBitsWithBadCount () : void {
+ $this->expectException(InvalidArgumentException::class);
+
+ UUID::UUIDv7FromBits(BitsVector::new(0), 0xCC3, 0x18C4DC0C0C07398F);
+ }
+
+ ///
+ /// Tests for convert between UUID methods
+ ///
+
+ public function testUUIDv1ToUUIDv6 () : void {
+ $this->assertEquals(
+ "1ec9414c-232a-6b00-b3c8-9e6bdeced846",
+ UUID::UUIDv1ToUUIDv6("c232ab00-9414-11ec-b3c8-9e6bdeced846")
+ );
+ }
+
+ public function testUUIDv6ToUUIDv1 () : void {
+ $this->assertEquals(
+ "c232ab00-9414-11ec-b3c8-9e6bdeced846",
+ UUID::UUIDv6ToUUIDv1("1ec9414c-232a-6b00-b3c8-9e6bdeced846")
+ );
+ }
+
+ ///
+ /// Tests for helper methods
+ ///
+
+ public function provideFormattedUUID () : iterable {
+ yield [
+ "320c3d4dcc00875b8ec932d5f69181c0",
+ "320c3d4d-cc00-875b-8ec9-32d5f69181c0",
+ ];
+
+ yield [
+ "320c3d4d-cc00-875b-8ec9-32d5f69181c0",
+ "320c3d4d-cc00-875b-8ec9-32d5f69181c0",
+ ];
+
+ yield [
+ "320C3D4D-CC00-875B-8EC9-32D5F69181C0",
+ "320c3d4d-cc00-875b-8ec9-32d5f69181c0",
+ ];
+ }
+
+ /**
+ * @dataProvider provideFormattedUUID
+ */
+ public function testReformat($uuidToReformat, $expected) {
+ $this->assertEquals($expected, UUID::reformat($uuidToReformat));
+ }
+
public function testIsUUID () : void {
$this->assertTrue(UUID::isUUID("e14d5045-4959-11e8-a2e6-0007cb03f249"));
$this->assertFalse(
@@ -43,7 +203,73 @@
"The method shouldn't allow arbitrary size strings."
);
$this->assertFalse(UUID::isUUID("d825a90a27e7f161a07161c3a37dce8e"));
+ }
+
+ private function provideUUIDsWithVersionAndVariant () : iterable {
+ // RFC 4122
+ yield ["c232ab00-9414-11ec-b3c8-9e6bdeced846", 1, 2];
+ yield ["f6244210-bbc3-3689-bb54-76528802d4d5", 3, 2];
+ yield ["23b50a2e-0543-4eaa-a53f-2a9dd02606e7", 4, 2];
+ yield ["2f8c2178-9c05-55ba-9b69-f4e076017270", 5, 2];
+
+ // draft-peabody-dispatch-new-uuid-format-03
+ yield ["1ec9414c-232a-6b00-b3c8-9e6bdeced846", 6, 2];
+ yield ["018003e1-0e46-7c62-9e4e-63cda74165ea", 7, 2];
+ yield ["320c3d4d-cc00-875b-8ec9-32d5f69181c0", 8, 2];
+
+ // Special values from both RFC
+ yield [UUID::NIL, 0, 0];
+ yield [UUID::MAX, 15, 3];
+ }
+
+ /**
+ * @dataProvider provideUUIDsWithVersionAndVariant
+ */
+ public function testGetVersion (string $uuid, int $version, int $variant) : void {
+ $this->assertEquals($version, UUID::getVersion($uuid));
+ }
+
+ /**
+ * @dataProvider provideUUIDsWithVersionAndVariant
+ */
+ public function testGetVariant (string $uuid, int $version, int $variant) : void {
+ $this->assertEquals($variant, UUID::getVariant($uuid));
+ }
+
+ ///
+ /// Monotonicity :: UUIDv6 UUIDv7
+ ///
+
+ private function assertMonotonicity (iterable $series) : void {
+ $bigrams = Vector::from($series)->bigrams();
+ foreach ($bigrams as $bigram) {
+ $this->assertGreaterThan($bigram[0], $bigram[1]);
+ }
+ }
+
+ public function testMonotonicityForUUIDv6 () {
+ $series = Vector::range(0, 99)->map(fn($_) => UUID::UUIDv6());
+ $this->assertMonotonicity($series);
+ }
+
+ public function testMonotonicityForSlowlyGeneratedUUIDv7 () {
+ $series = Vector::range(0, 99)->map(function ($_) {
+ usleep(1000);
+ return UUID::UUIDv7();
+ });
+ $this->assertMonotonicity($series);
+ }
+
+ public function testMonotonicityForBatchesOfUUIDv7WhenBatchQuantityIsSmallEnough () {
+ $series = UUID::batchOfUUIDv7(63);
+ $this->assertMonotonicity($series);
+ }
+
+ public function testMonotonicityForBatchesOfUUIDv7 () {
+ $series = UUID::batchOfUUIDv7(1000);
+ $this->assertCount(1000, $series);
+ $this->assertMonotonicity($series);
}
}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Tue, Nov 26, 09:42 (22 h, 6 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2264438
Default Alt Text
D2660.diff (34 KB)
Attached To
Mode
D2660: Implement UUIDv1 UUIDv6 UUIDv7 and UUIDv8
Attached
Detach File
Event Timeline
Log In to Comment