Skip to content

Commit 1a8e22e

Browse files
committed
UUIDv7 Method 3 implementation
1 parent de3931f commit 1a8e22e

File tree

1 file changed

+154
-10
lines changed

1 file changed

+154
-10
lines changed

std/uuid.d

Lines changed: 154 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ module std.uuid;
121121
}
122122

123123
import core.time : dur;
124+
import std.bitmanip : bigEndianToNative, nativeToBigEndian;
124125
import std.datetime.systime : SysTime;
125126
import std.datetime : Clock, DateTime, UTC;
126127
import std.range.primitives;
@@ -323,13 +324,16 @@ public struct UUID
323324
* random = UUID V7 has 74 bits of random data, which rounds to 10 ubyte's.
324325
* If no random data is given, random data is generated.
325326
*/
326-
@safe pure this(SysTime timestamp, ubyte[10] random = generateV7RandomData())
327+
@safe pure this(SysTime timestamp, ubyte[10] random = generateV7RandomData!10)
327328
{
328-
import std.bitmanip : nativeToBigEndian;
329+
ulong epoch = (timestamp - SysTime.fromUnixTime(0)).total!"msecs";
330+
this(epoch, random);
331+
}
329332

330-
ubyte[8] epoch = (timestamp - SysTime.fromUnixTime(0))
331-
.total!"msecs"
332-
.nativeToBigEndian;
333+
/// ditto
334+
@safe pure this(ulong epoch_msecs, ubyte[10] random = generateV7RandomData!10)
335+
{
336+
ubyte[8] epoch = epoch_msecs.nativeToBigEndian;
333337

334338
this.data[0 .. 6] = epoch[2 .. 8];
335339
this.data[6 .. $] = random;
@@ -557,7 +561,7 @@ public struct UUID
557561

558562
/**
559563
* If the UUID is of version 7 it has a timestamp that this function
560-
* returns, otherwise and UUIDParsingException is thrown.
564+
* returns, otherwise an UUIDParsingException is thrown.
561565
*/
562566
SysTime v7Timestamp() const {
563567
if (this.uuidVersion != Version.timestampRandom)
@@ -574,6 +578,25 @@ public struct UUID
574578
return SysTime(DateTime(1970, 1, 1), UTC()) + dur!"msecs"(milli);
575579
}
576580

581+
/**
582+
* If the UUID is of version 7 it has a timestamp that this function
583+
* returns as described in RFC 9562 (Method 3), otherwise an
584+
* UUIDParsingException is thrown.
585+
*/
586+
SysTime v7Timestamp_method3() const {
587+
auto ret = v7Timestamp();
588+
589+
const ubyte[2] rand_a = [
590+
data[6] & 0x0f, // masks version bits
591+
data[7]
592+
];
593+
594+
const float hnsecs = rand_a.bigEndianToNative!ushort / MonotonicUUIDsFactory.subMsecsPart;
595+
ret += dur!"hnsecs"(cast(ulong) hnsecs);
596+
597+
return ret;
598+
}
599+
577600
/**
578601
* RFC 4122 defines different internal data layouts for UUIDs.
579602
* Returns the format used by this UUID.
@@ -1378,6 +1401,128 @@ if (isInputRange!RNG && isIntegral!(ElementType!RNG))
13781401
assert(u1.uuidVersion == UUID.Version.randomNumberBased);
13791402
}
13801403

1404+
///
1405+
class MonotonicUUIDsFactory
1406+
{
1407+
import core.sync.mutex : Mutex;
1408+
import std.datetime.stopwatch : StopWatch;
1409+
1410+
private shared Mutex mtx;
1411+
private StopWatch startTimePoint;
1412+
1413+
// Passthrough for old compilers
1414+
version (unittest)
1415+
ref __start() shared => cast() startTimePoint;
1416+
1417+
///
1418+
this(in SysTime startTime = SysTime.fromUnixTime(0)) shared
1419+
{
1420+
mtx = new shared Mutex();
1421+
1422+
(cast() startTimePoint).start();
1423+
(cast() startTimePoint).setTimeElapsed = Clock.currTime - startTime;
1424+
}
1425+
1426+
private auto peek() shared
1427+
{
1428+
mtx.lock();
1429+
scope(exit) mtx.unlock();
1430+
1431+
return (cast() startTimePoint).peek;
1432+
}
1433+
1434+
// hnsecs is 1/10_000 of millisecond
1435+
// rand_a size is 12 bits (4096 values)
1436+
private enum float subMsecsPart = 1.0f / 10_000 * 4096;
1437+
1438+
/**
1439+
* Returns a monotonic timestamp + random based UUIDv7
1440+
* as described in RFC 9562 (Method 3).
1441+
*/
1442+
//FIXME: for some reason this method call causes SIGSEGV, but works as template
1443+
UUID createUUIDv7_method3()(ubyte[8] rnd = generateV7RandomData!8) shared
1444+
{
1445+
const curr = peek.split!("msecs", "hnsecs");
1446+
const qhnsecs = cast(ushort) (curr.hnsecs * subMsecsPart);
1447+
1448+
ubyte[10] rand;
1449+
1450+
// Whole rand_a is 16 bit, but usable only 12 MSB.
1451+
// additional 4 less significant bits consumed
1452+
// by a version value
1453+
rand[0 .. 2] = qhnsecs.nativeToBigEndian;
1454+
rand[2 .. $] = rnd;
1455+
1456+
return UUID(curr.msecs, rand);
1457+
}
1458+
}
1459+
1460+
///
1461+
@system unittest
1462+
{
1463+
import std.conv : to;
1464+
import std.datetime;
1465+
1466+
scope f = new shared MonotonicUUIDsFactory;
1467+
1468+
// trick to give reproducible testing
1469+
Duration setElapsedOffset(Duration dura){
1470+
if (f.__start.running)
1471+
f.__start.stop();
1472+
1473+
const st = SysTime(DateTime(2025, 9, 12, 21, 38, 45), UTC());
1474+
Duration ret = st - SysTime.fromUnixTime(0) + dura;
1475+
f.__start.setTimeElapsed = ret;
1476+
return ret;
1477+
}
1478+
1479+
Duration d = dur!"msecs"(123);
1480+
setElapsedOffset(d);
1481+
1482+
const uuidv7_milli = f.createUUIDv7_method3().v7Timestamp;
1483+
1484+
{
1485+
const st = f.createUUIDv7_method3().v7Timestamp_method3;
1486+
assert(cast(DateTime) st == DateTime(2025, 9, 12, 21, 38, 45), st.to!string);
1487+
1488+
const sp = st.fracSecs.split!("msecs", "usecs");
1489+
assert(sp.msecs == 123, sp.to!string);
1490+
assert(sp.usecs == 0, sp.to!string);
1491+
}
1492+
1493+
// 0.3 usecs, but Method 3 precision is only 0.25 of usec,
1494+
// thus, expected value is 2
1495+
d += dur!"hnsecs"(3);
1496+
setElapsedOffset(d);
1497+
1498+
const uuidv7_milli_2 = f.createUUIDv7_method3().v7Timestamp;
1499+
assert(uuidv7_milli == uuidv7_milli_2);
1500+
1501+
{
1502+
const st = f.createUUIDv7_method3().v7Timestamp_method3;
1503+
assert(cast(DateTime) st == DateTime(2025, 9, 12, 21, 38, 45), st.to!string);
1504+
1505+
const sp = st.fracSecs.split!("msecs", "usecs", "hnsecs");
1506+
assert(sp.msecs == 123, sp.to!string);
1507+
assert(sp.usecs == 0, sp.to!string);
1508+
assert(sp.hnsecs == 2, sp.to!string);
1509+
}
1510+
}
1511+
1512+
///
1513+
@system unittest
1514+
{
1515+
scope f = new shared MonotonicUUIDsFactory;
1516+
1517+
UUID[100_000] uuids = void;
1518+
1519+
foreach (ref u; uuids)
1520+
u = f.createUUIDv7_method3;
1521+
1522+
foreach (i; 1 .. uuids.length)
1523+
assert(uuids[i-1].v7Timestamp_method3 < uuids[i].v7Timestamp_method3);
1524+
}
1525+
13811526
/**
13821527
* This function returns a timestamp + random based UUID aka. uuid v7.
13831528
*/
@@ -1794,12 +1939,12 @@ enum uuidRegex = "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}"~
17941939
]);
17951940
}
17961941

1797-
private ubyte[10] generateV7RandomData() {
1942+
private ubyte[Size] generateV7RandomData(ubyte Size)() {
17981943
import std.random : Random, uniform, unpredictableSeed;
17991944

18001945
auto rnd = Random(unpredictableSeed!(ubyte)());
18011946

1802-
ubyte[10] bytes;
1947+
ubyte[Size] bytes;
18031948
foreach (idx; 0 .. bytes.length)
18041949
{
18051950
bytes[idx] = uniform!(ubyte)(rnd);
@@ -1901,6 +2046,5 @@ public class UUIDParsingException : Exception
19012046
{
19022047
import std.datetime : DateTime, SysTime;
19032048
UUID u = UUID("0198c2b2-c5a8-7a0f-a1db-86aac7906c7b");
1904-
auto d = DateTime(2025,8,19);
1905-
assert((cast(DateTime) u.v7Timestamp()).year == d.year);
2049+
assert(u.v7Timestamp.toISOExtString == "2025-08-19T14:19:12.68Z", u.v7Timestamp.toISOExtString);
19062050
}

0 commit comments

Comments
 (0)