Page Menu
Home
DevCentral
Search
Configure Global Search
Log In
Files
F3768473
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
56 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/Collections/BaseVector.php b/src/Collections/BaseVector.php
index cc4b9a6..7334cb4 100644
--- a/src/Collections/BaseVector.php
+++ b/src/Collections/BaseVector.php
@@ -1,283 +1,312 @@
<?php
declare(strict_types=1);
namespace Keruald\OmniTools\Collections;
use ArrayAccess;
use ArrayIterator;
use InvalidArgumentException;
use IteratorAggregate;
use Traversable;
use Keruald\OmniTools\Reflection\CallableElement;
use Keruald\OmniTools\Strings\Multibyte\OmniString;
abstract class BaseVector extends BaseCollection implements ArrayAccess, IteratorAggregate {
///
/// Properties
///
protected array $items;
///
/// Constructors
///
public function __construct (iterable $items = []) {
if (is_array($items)) {
$this->items = $items;
return;
}
foreach ($items as $item) {
$this->items[] = $item;
}
}
public static function from (iterable $items) : static {
return new static($items);
}
///
/// Interact with collection content at key level
///
public function get (int $key) : mixed {
if (!array_key_exists($key, $this->items)) {
throw new InvalidArgumentException("Key not found.");
}
return $this->items[$key];
}
public function getOr (int $key, mixed $defaultValue) : mixed {
return $this->items[$key] ?? $defaultValue;
}
public function set (int $key, mixed $value) : static {
$this->items[$key] = $value;
return $this;
}
public function unset (int $key) : static {
unset($this->items[$key]);
return $this;
}
public function contains (mixed $value) : bool {
return in_array($value, $this->items);
}
///
/// Interact with collection content at collection level
///
public function count () : int {
return count($this->items);
}
public function isEmpty () : bool {
return $this->count() === 0;
}
public function clear () : self {
$this->items = [];
return $this;
}
public function push (mixed $item) : self {
$this->items[] = $item;
return $this;
}
/**
* Append all elements of the specified iterable
* to the current vector.
*
* If a value already exists, the value is still added
* as a duplicate.
*
* @see update() when you need to only add unique values.
*/
public function append (iterable $iterable) : self {
foreach ($iterable as $value) {
$this->items[] = $value;
}
return $this;
}
/**
* Append all elements of the specified iterable
* to the current vector.
*
* If a value already exists, it is skipped.
*
* @see append() when you need to always add everything.
*/
public function update (iterable $iterable) : self {
foreach ($iterable as $value) {
if (!$this->contains($value)) {
$this->items[] = $value;
}
}
return $this;
}
/**
* Replaces a part of the vector by the specified iterable.
*
* @param int $offset Allow to replace a part inside the vector by an iterable with keys starting at 0, by adding the specified offset.
* @param int $len The maximum amount of elements to read. If 0, the read isn't bounded.
*/
public function replace(iterable $iterable, int $offset = 0, int $len = 0) : self {
$itemsCount = 0;
foreach ($iterable as $key => $value) {
$this->items[$key + $offset] = $value;
$itemsCount++;
if ($len > 0 && $itemsCount >= $len) {
break;
}
}
return $this;
}
/**
* Gets a copy of the internal vector.
*
* Scalar values (int, strings) are cloned.
* Objects are references to a specific objet, not a clone.
*
* @return array
*/
public function toArray () : array {
return $this->items;
}
///
/// HOF :: generic
///
public function map (callable $callable) : self {
return new static(array_map($callable, $this->items));
}
public function filter (callable $callable) : self {
$argc = (new CallableElement($callable))->countArguments();
if ($argc === 0) {
throw new InvalidArgumentException(
"Callback should have at least one argument"
);
}
$mode = (int)($argc > 1);
return new static(array_filter($this->items, $callable, $mode));
}
public function mapKeys (callable $callable) : self {
$mappedVector = [];
foreach ($this->items as $key => $value) {
$mappedVector[$callable($key)] = $value;
}
return new static($mappedVector);
}
public function flatMap (callable $callable) : self {
$argc = (new CallableElement($callable))->countArguments();
$newMap = new static;
foreach ($this->items as $key => $value) {
$toAdd = match($argc) {
0 => throw new InvalidArgumentException(self::CB_ZERO_ARG),
1 => $callable($value),
default => $callable($key, $value),
};
$newMap->append($toAdd);
}
return $newMap;
}
public function filterKeys (callable $callable) : self {
return new static(
array_filter($this->items, $callable, ARRAY_FILTER_USE_KEY)
);
}
public function chunk (int $length): Vector {
return new Vector(array_chunk($this->items, $length));
}
public function slice (int $offset, int $length) : self {
$slice = array_slice($this->items, $offset, $length);
return new static($slice);
}
public function implode(string $delimiter) : OmniString {
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.
///
private static function ensureOffsetIsInteger (mixed $offset) {
if (is_int($offset)) {
return;
}
throw new InvalidArgumentException(
"Offset of a vector must be an integer."
);
}
public function offsetExists (mixed $offset) : bool {
self::ensureOffsetIsInteger($offset);
return array_key_exists($offset, $this->items);
}
public function offsetGet (mixed $offset) : mixed {
self::ensureOffsetIsInteger($offset);
return $this->get($offset);
}
public function offsetSet (mixed $offset, mixed $value) : void {
if ($offset === null) {
$this->push($value);
return;
}
self::ensureOffsetIsInteger($offset);
$this->set($offset, $value);
}
public function offsetUnset (mixed $offset) : void {
self::ensureOffsetIsInteger($offset);
$this->unset($offset);
}
///
/// IteratorAggregate
///
public function getIterator () : Traversable {
return new ArrayIterator($this->items);
}
}
diff --git a/src/Collections/Vector.php b/src/Collections/Vector.php
index 28a72f1..2e8c561 100644
--- a/src/Collections/Vector.php
+++ b/src/Collections/Vector.php
@@ -1,59 +1,63 @@
<?php
declare(strict_types=1);
namespace Keruald\OmniTools\Collections;
use Keruald\OmniTools\Strings\Multibyte\OmniString;
/**
* A generic vector implementation to accept any kind of value.
*
* Vector offers specialized methods to convert from and to int/string.
*
* This class is intended to be used in every case a more specialized
* vector implementation doesn't exist or isn't needed, ie every time
* an array is needed, to contains ordered values, without string keys.
*/
class Vector extends BaseVector {
///
/// Specialized constructors
///
/**
* Constructs a new instance of a vector by exploding a string
* according a specified delimiter.
*
* @param string $delimiter The substring to find for explosion
* @param string $string The string to explode
* @param int $limit If specified, the maximum count of vector elements
* @return static
*/
public static function explode (string $delimiter, string $string,
int $limit = PHP_INT_MAX) : self {
// There is some discussion to know if this method belongs
// to Vector or OmniString.
//
// The advantage to keep it here is we can have constructs like:
// Vector::explode(",", "1,1,2,3,5,8,13")
// ->toIntegers()
// >map(function($n) { return $n * $n; })
// ->toArray();
//
// In this chaining, it is clear we manipulate Vector methods.
return (new OmniString($string))
->explode($delimiter, $limit);
}
+ public static function range (int $start, int $end, int $step = 1) : self {
+ return new Vector(range($start, $end, $step));
+ }
+
///
/// HOF :: specialized
///
public function toIntegers () : self {
array_walk($this->items, ArrayUtilities::toIntegerCallback());
return $this;
}
}
diff --git a/src/DateTime/UUIDv1TimeStamp.php b/src/DateTime/UUIDv1TimeStamp.php
new file mode 100644
index 0000000..70a8897
--- /dev/null
+++ b/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/src/DateTime/UUIDv7TimeStamp.php b/src/DateTime/UUIDv7TimeStamp.php
new file mode 100644
index 0000000..0d68a74
--- /dev/null
+++ b/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/src/Identifiers/Random.php b/src/Identifiers/Random.php
index 2898c60..12cce83 100644
--- a/src/Identifiers/Random.php
+++ b/src/Identifiers/Random.php
@@ -1,138 +1,167 @@
<?php
declare(strict_types=1);
namespace Keruald\OmniTools\Identifiers;
use Closure;
use Exception;
use InvalidArgumentException;
use Keruald\OmniTools\Strings\Multibyte\StringUtilities;
class Random {
/**
* @return string 32 random hexadecimal characters
*/
public static function generateHexHash () : string {
return UUID::UUIDv4WithoutHyphens();
}
/**
* @param string $format A for letters, 1 for digits, e.g. AAA111
*
* @return string a random string based on the format e.g. ZCK530
*/
public static function generateString (string $format) : string {
$randomString = "";
$len = strlen($format);
for ($i = 0 ; $i < $len ; $i++) {
$randomString .= self::generateCharacter($format[$i]);
}
return $randomString;
}
/**
* @param string $format A for letters, 1 for digits, e.g. A
*
* @return string a random string based on the format e.g. Z
*/
public static function generateCharacter (string $format) : string {
return self::getPicker(self::normalizeFormat($format))();
}
/**
* @throws Exception if an appropriate source of randomness cannot be found.
*/
public static function generateIdentifier (int $bytes_count) : string {
$bytes = random_bytes($bytes_count);
return StringUtilities::encodeInBase64($bytes);
}
///
/// Helper methods for pickers
///
public static function normalizeFormat (string $format) : string {
$normalizers = self::getNormalizers();
foreach ($normalizers as $normalizedFormat => $conditionClosure) {
if ($conditionClosure($format)) {
return (string)$normalizedFormat;
}
}
return $format;
}
private static function getNormalizers () : array {
/**
* <normalized format> => <method which returns true if format matches>
*/
return [
'A' => function ($format) : bool {
return ctype_upper($format);
},
'a' => function ($format) : bool {
return ctype_lower($format);
},
'1' => function ($format) : bool {
return is_numeric($format);
},
];
}
private static function getPickers () : array {
return [
'A' => function () : string {
return Random::pickLetter();
},
'a' => function () : string {
return strtolower(Random::pickLetter());
},
'1' => function () : string {
return (string)Random::pickDigit();
},
];
}
/**
* @throws Exception if an appropriate source of randomness cannot be found.
*/
public static function pickLetter () : string {
$asciiCode = 65 + self::pickDigit(26);
return chr($asciiCode);
}
/**
* @throws Exception if an appropriate source of randomness cannot be found.
*/
public static function pickDigit (int $base = 10) : int {
return random_int(0, $base - 1);
}
private static function getPicker (string $format) : Closure {
$pickers = self::getPickers();
if (isset($pickers[$format])) {
return $pickers[$format];
}
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/src/Identifiers/UUID.php b/src/Identifiers/UUID.php
index d91591b..cb156e1 100644
--- a/src/Identifiers/UUID.php
+++ b/src/Identifiers/UUID.php
@@ -1,52 +1,357 @@
<?php
declare(strict_types=1);
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.
*/
public static function UUIDv4 () : string {
// Code by Andrew Moore
// See http://php.net/manual/en/function.uniqid.php#94959
// https://www.ietf.org/rfc/rfc4122.txt
return sprintf(
'%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
// 32 bits for "time_low"
random_int(0, 0xffff), random_int(0, 0xffff),
// 16 bits for "time_mid"
random_int(0, 0xffff),
// 16 bits for "time_hi_and_version",
// four most significant bits holds version number 4
random_int(0, 0x0fff) | 0x4000,
// 16 bits, 8 bits for "clk_seq_hi_res",
// 8 bits for "clk_seq_low",
// two most significant bits holds zero and one for variant DCE1.1
random_int(0, 0x3fff) | 0x8000,
// 48 bits for "node"
random_int(0, 0xffff), random_int(0, 0xffff), random_int(0, 0xffff)
);
}
public static function UUIDv4WithoutHyphens () : string {
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/tests/Collections/VectorTest.php b/tests/Collections/VectorTest.php
index 2720ace..4874717 100644
--- a/tests/Collections/VectorTest.php
+++ b/tests/Collections/VectorTest.php
@@ -1,299 +1,376 @@
<?php
declare(strict_types=1);
namespace Keruald\OmniTools\Tests\Collections;
use Keruald\OmniTools\Collections\Vector;
use PHPUnit\Framework\TestCase;
use InvalidArgumentException;
use IteratorAggregate;
use Traversable;
/**
* @covers \Keruald\OmniTools\Collections\Vector
* @covers \Keruald\OmniTools\Collections\BaseVector
*/
class VectorTest extends TestCase {
private Vector $vector;
protected function setUp () : void {
$this->vector = new Vector([1, 2, 3, 4, 5]);
}
public function testConstructorWithIterable () : void {
$iterable = new class implements IteratorAggregate {
public function getIterator () : Traversable {
yield 42;
yield 100;
}
};
$vector = new Vector($iterable);
$this->assertEquals([42, 100], $vector->toArray());
}
public function testFrom () : void {
$this->assertEquals([42, 100], Vector::from([42, 100])->toArray());
}
public function testGet () : void {
$vector = new Vector(["a", "b", "c"]);
$this->assertEquals("b", $vector->get(1));
}
public function testGetOverflow () : void {
$this->expectException(InvalidArgumentException::class);
$this->vector->get(800);
}
public function testGetOr () : void {
$vector = new Vector(["a", "b", "c"]);
$this->assertEquals("X", $vector->getOr(800, "X"));
}
public function testSet () : void {
$vector = new Vector(["a", "b", "c"]);
$vector->set(1, "x"); // should replace "b"
$this->assertEquals(["a", "x", "c"], $vector->toArray());
}
public function testContains () : void {
$this->assertTrue($this->vector->contains(2));
$this->assertFalse($this->vector->contains(666));
}
public function testCount () : void {
$this->assertEquals(5, $this->vector->count());
$this->assertEquals(0, (new Vector)->count());
}
public function testClear () : void {
$this->vector->clear();
$this->assertEquals(0, $this->vector->count());
}
public function testIsEmpty () : void {
$this->vector->clear();
$this->assertTrue($this->vector->isEmpty());
}
public function testPush () : void {
$this->vector->push(6);
$this->assertEquals([1, 2, 3, 4, 5, 6], $this->vector->toArray());
}
public function testAppend () : void {
$this->vector->append([6, 7, 8]);
$this->assertEquals([1, 2, 3, 4, 5, 6, 7 ,8], $this->vector->toArray());
}
public function testUpdate () : void {
$this->vector->update([5, 5, 5, 6, 7, 8]); // 5 already exists
$this->assertEquals([1, 2, 3, 4, 5, 6, 7 ,8], $this->vector->toArray());
}
public function testMap () : void {
$actual = $this->vector
->map(function ($x) { return $x * $x; })
->toArray();
$this->assertEquals([1, 4, 9, 16, 25], $actual);
}
public function testMapKeys () : void {
$vector = new Vector(["foo", "bar", "quux", "xizzy"]);
$filter = function ($key) {
return 0; // Let's collapse our array
};
$actual = $vector->mapKeys($filter)->toArray();
$this->assertEquals(["xizzy"], $actual);
}
public function testFlatMap () : void {
$expected = [
// Squares and cubes
1, 1,
4, 8,
9, 27,
16, 64,
25, 125
];
$callback = function ($n) {
yield $n * $n;
yield $n * $n * $n;
};
$actual = $this->vector->flatMap($callback)->toArray();
$this->assertEquals($expected, $actual);
}
public function testFlatMapWithKeyValueCallback() : void {
$vector = new Vector(["foo", "bar", "quux", "xizzy"]);
$callback = function (int $key, string $value) {
yield "$key::$value";
yield "$value ($key)";
};
$expected = [
"0::foo",
"foo (0)",
"1::bar",
"bar (1)",
"2::quux",
"quux (2)",
"3::xizzy",
"xizzy (3)",
];
$actual = $vector->flatMap($callback)->toArray();
$this->assertEquals($expected, $actual);
}
public function testFlatMapWithCallbackWithoutArgument() : void {
$this->expectException(InvalidArgumentException::class);
$callback = function () {};
$this->vector->flatMap($callback);
}
public function testFilter () : void {
$vector = new Vector(["foo", "bar", "quux", "xizzy"]);
$filter = function ($item) {
return strlen($item) === 3; // Let's keep 3-letters words
};
$actual = $vector->filter($filter)->toArray();
$this->assertEquals(["foo", "bar"], $actual);
}
public function testFilterWithBadCallback () : void {
$this->expectException(InvalidArgumentException::class);
$badFilter = function () {};
$this->vector->filter($badFilter);
}
public function testFilterKeys () : void {
$filter = function ($key) {
return $key % 2 === 0; // Let's keep even indices
};
$actual = $this->vector
->filterKeys($filter)
->toArray();
$this->assertEquals([0, 2, 4], array_keys($actual));
}
public function testChunk () : void {
$vector = new Vector([1, 2, 3, 4, 5, 6]);
$this->assertEquals(
[[1, 2], [3, 4], [5, 6]],
$vector->chunk(2)->toArray()
);
}
public function testSlice () : void {
$actual = $this->vector->slice(2, 3);
$this->assertEquals([3, 4, 5], $actual->toArray());
}
public function testImplode() : void {
$actual = (new Vector(["a", "b", "c"]))
->implode(".")
->__toString();
$this->assertEquals("a.b.c", $actual);
}
public function testImplodeWithoutDelimiter() : void {
$actual = (new Vector(["a", "b", "c"]))
->implode("")
->__toString();
$this->assertEquals("abc", $actual);
}
public function testExplode() : void {
$actual = Vector::explode(".", "a.b.c");
$this->assertEquals(["a", "b", "c"], $actual->toArray());
}
public function testExplodeWithoutDelimiter() : void {
$actual = Vector::explode("", "a.b.c");
$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
///
public function testArrayAccessFailsWithStringKey () : void {
$this->expectException(InvalidArgumentException::class);
$this->vector["foo"];
}
public function testOffsetExists () : void {
$this->assertTrue(isset($this->vector[0]));
$this->assertFalse(isset($this->vector[8]));
}
public function testOffsetSetWithoutOffset () : void {
$this->vector[] = 6;
$this->assertEquals(6, $this->vector[5]);
}
public function testOffsetSet () : void {
$this->vector[0] = 9;
$this->assertEquals(9, $this->vector[0]);
}
public function testOffsetUnset () : void {
unset($this->vector[2]);
$expected = [
0 => 1,
1 => 2,
// vector[2] has been unset
3 => 4,
4 => 5,
];
$this->assertEquals($expected, $this->vector->toArray());
}
///
/// IteratorAggregate
///
public function testGetIterator () : void {
$this->assertEquals([1, 2, 3, 4, 5], iterator_to_array($this->vector));
}
}
diff --git a/tests/DateTime/UUIDv1TimeStampTest.php b/tests/DateTime/UUIDv1TimeStampTest.php
new file mode 100644
index 0000000..2830577
--- /dev/null
+++ b/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/tests/DateTime/UUIDv7TimeStampTest.php b/tests/DateTime/UUIDv7TimeStampTest.php
new file mode 100644
index 0000000..cd19c89
--- /dev/null
+++ b/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/tests/Identifiers/UUIDTest.php b/tests/Identifiers/UUIDTest.php
index 5dd2175..e5404c9 100644
--- a/tests/Identifiers/UUIDTest.php
+++ b/tests/Identifiers/UUIDTest.php
@@ -1,49 +1,275 @@
<?php
declare(strict_types=1);
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();
$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 testUUIDv4WithoutHyphens () : void {
$uuid = UUID::UUIDv4WithoutHyphens();
$this->assertEquals(
32, strlen($uuid),
"UUID size must be 36 characters, and there are 4 hyphens, so here 32 characters are expected."
);
$re = "/[0-9a-f]/";
$this->assertMatchesRegularExpression($re, $uuid);
}
public function testUUIDv4AreUnique () : void {
$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(
UUID::isUUID("e14d5045-4959-11e8-a2e6-0007cb03f249c"),
"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/x-diff
Expires
Mon, Nov 25, 08:10 (1 d, 12 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2259814
Default Alt Text
(56 KB)
Attached To
Mode
rKOT Keruald OmniTools
Attached
Detach File
Event Timeline
Log In to Comment