Skip to content

Commit 4d9280a

Browse files
committed
Add StringUuidV7::generate method with UUID v7 generation and test cases
- Implemented `StringUuidV7::generate` to produce valid, lowercase UUID v7 values as per RFC 4122 standard. - Integrated `GenerateInterface` for reusable value generation capabilities. - Enhanced unit tests to verify UUID v7 validity, uniqueness, lexicographic time-ordering, and format compliance.
1 parent 1d9474f commit 4d9280a

File tree

2 files changed

+80
-1
lines changed

2 files changed

+80
-1
lines changed

src/String/StringUuidV7.php

100644100755
Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,14 @@
44

55
namespace PhpTypedValues\String;
66

7+
use const STR_PAD_LEFT;
8+
9+
use PhpTypedValues\Code\Contract\GenerateInterface;
710
use PhpTypedValues\Code\Exception\StringTypeException;
811
use PhpTypedValues\Code\String\StrType;
12+
use Random\RandomException;
913

14+
use function ord;
1015
use function preg_match;
1116
use function sprintf;
1217
use function strtolower;
@@ -16,7 +21,7 @@
1621
*
1722
* @psalm-immutable
1823
*/
19-
readonly class StringUuidV7 extends StrType
24+
readonly class StringUuidV7 extends StrType implements GenerateInterface
2025
{
2126
/** @var non-empty-string */
2227
protected string $value;
@@ -56,4 +61,53 @@ public function value(): string
5661
{
5762
return $this->value;
5863
}
64+
65+
/**
66+
* @throws RandomException
67+
* @throws StringTypeException
68+
*/
69+
public static function generate(): static
70+
{
71+
// Get current Unix time in milliseconds (48 bits)
72+
// Use a float literal for the multiplier to satisfy Psalm strict operands (float*float)
73+
$timeMs = (int) floor(microtime(true) * 1000.0);
74+
75+
// Split timestamp into 48 bits: high 32 bits and low 16 bits
76+
$timeHigh = ($timeMs & 0xFFFFFFFF0000) >> 16; // upper 32 bits of 48-bit timestamp
77+
$timeLow = $timeMs & 0xFFFF; // lower 16 bits
78+
79+
// Generate 10 random bytes for the remaining fields
80+
$random = random_bytes(10);
81+
82+
// Build the UUID fields (all integers) according to UUID v7 layout
83+
// time_high (32 bits)
84+
$timeHighHex = str_pad(dechex($timeHigh), 8, '0', STR_PAD_LEFT);
85+
86+
// time_mid (16 bits)
87+
$timeMidHex = str_pad(dechex($timeLow), 4, '0', STR_PAD_LEFT);
88+
89+
// time_hi_and_version (16 bits): high 4 bits = version 7 (0b0111)
90+
$timeHiAndVersion = (ord($random[0]) & 0x0F) | 0x70; // clear high 4 bits, set to 0111
91+
$timeHiAndVersionHex = sprintf('%02x%02x', $timeHiAndVersion, ord($random[1]));
92+
93+
// clock_seq_hi_and_reserved (8 bits): set variant to 0b10xx
94+
$clockSeqHi = (ord($random[2]) & 0x3F) | 0x80; // clear top 2 bits, set to 10
95+
$clockSeqLow = ord($random[3]);
96+
$clockSeqHex = sprintf('%02x%02x', $clockSeqHi, $clockSeqLow);
97+
98+
// node (48 bits) = remaining 6 random bytes
99+
$nodeHex = bin2hex(substr($random, 4, 6));
100+
101+
// Assemble canonical UUID string: 8-4-4-4-12
102+
$uuid = sprintf(
103+
'%s-%s-%s-%s-%s',
104+
$timeHighHex,
105+
$timeMidHex,
106+
$timeHiAndVersionHex,
107+
$clockSeqHex,
108+
$nodeHex
109+
);
110+
111+
return new static($uuid);
112+
}
59113
}

tests/Unit/String/StringUuidV7Test.php

100644100755
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,28 @@
4545
expect(fn() => StringUuidV7::fromString($badChar))
4646
->toThrow(StringTypeException::class, 'Expected UUID v7 (xxxxxxxx-xxxx-7xxx-[89ab]xxx-xxxxxxxxxxxx), got "' . $badChar . '"');
4747
});
48+
49+
it('generate produces a valid lowercase UUID v7 and different values across calls', function (): void {
50+
$a = StringUuidV7::generate();
51+
$b = StringUuidV7::generate();
52+
53+
$regex = '/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/';
54+
55+
expect($a->toString())->toMatch($regex)
56+
->and($a->value())->toBe($a->toString())
57+
->and($a->toString())->toBe(strtolower($a->toString()))
58+
->and($b->toString())->toMatch($regex)
59+
->and($b->toString())->toBe(strtolower($b->toString()))
60+
// Two generated UUIDs should almost certainly differ
61+
->and($a->toString())->not->toBe($b->toString());
62+
});
63+
64+
it('generate produces time-ordered UUID v7 (lexicographic order increases over time)', function (): void {
65+
$first = StringUuidV7::generate();
66+
// Sleep a little to ensure the next millisecond is different on most systems
67+
usleep(2000); // 2 ms
68+
$second = StringUuidV7::generate();
69+
70+
// For UUID v7, the canonical string representation is time-ordered; later values should compare greater
71+
expect(strcmp($first->toString(), $second->toString()))->toBeLessThan(0);
72+
});

0 commit comments

Comments
 (0)