Page MenuHomeDevCentral

D2660.diff
No OneTemporary

D2660.diff

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

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)

Event Timeline