Page MenuHomeDevCentral

No OneTemporary

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

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)

Event Timeline