@@ -121,6 +121,7 @@ module std.uuid;
121121}
122122
123123import core.time : dur;
124+ import std.bitmanip : bigEndianToNative, nativeToBigEndian;
124125import std.datetime.systime : SysTime;
125126import std.datetime : Clock , DateTime , UTC ;
126127import std.range.primitives ;
@@ -326,13 +327,16 @@ public struct UUID
326327 * random = UUID V7 has 74 bits of random data, which rounds to 10 ubyte's.
327328 * If no random data is given, random data is generated.
328329 */
329- @safe pure this (SysTime timestamp, ubyte [10 ] random = generateV7RandomData() )
330+ @safe pure this (SysTime timestamp, ubyte [10 ] random = generateRandomData ! 10 )
330331 {
331- import std.bitmanip : nativeToBigEndian;
332+ ulong epoch = (timestamp - SysTime.fromUnixTime(0 )).total! " msecs" ;
333+ this (epoch, random);
334+ }
332335
333- ubyte [8 ] epoch = (timestamp - SysTime.fromUnixTime(0 ))
334- .total! " msecs"
335- .nativeToBigEndian;
336+ // / ditto
337+ @safe pure this (ulong epoch_msecs, ubyte [10 ] random = generateRandomData! 10 )
338+ {
339+ ubyte [8 ] epoch = epoch_msecs.nativeToBigEndian;
336340
337341 this .data[0 .. 6 ] = epoch[2 .. 8 ];
338342 this .data[6 .. $] = random;
@@ -560,7 +564,7 @@ public struct UUID
560564
561565 /**
562566 * If the UUID is of version 7 it has a timestamp that this function
563- * returns, otherwise and UUIDParsingException is thrown.
567+ * returns, otherwise an UUIDParsingException is thrown.
564568 */
565569 SysTime v7Timestamp () const {
566570 if (this .uuidVersion != Version.timestampRandom)
@@ -580,6 +584,25 @@ public struct UUID
580584 return SysTime (DateTime (1970 , 1 , 1 ), UTC ()) + dur! " msecs" (milli);
581585 }
582586
587+ /**
588+ * If the UUID is of version 7 it has a timestamp that this function
589+ * returns as described in RFC 9562 (Method 3), otherwise an
590+ * UUIDParsingException is thrown.
591+ */
592+ SysTime v7Timestamp_method3 () const {
593+ auto ret = v7Timestamp();
594+
595+ const ubyte [2 ] rand_a = [
596+ data[6 ] & 0x0f , // masks version bits
597+ data[7 ]
598+ ];
599+
600+ const float hnsecs = rand_a.bigEndianToNative! ushort / MonotonicUUIDsFactory.subMsecsPart;
601+ ret += dur! " hnsecs" (cast (ulong ) hnsecs);
602+
603+ return ret;
604+ }
605+
583606 /**
584607 * RFC 4122 defines different internal data layouts for UUIDs.
585608 * Returns the format used by this UUID.
@@ -1384,6 +1407,149 @@ if (isInputRange!RNG && isIntegral!(ElementType!RNG))
13841407 assert (u1.uuidVersion == UUID .Version.randomNumberBased);
13851408}
13861409
1410+ // /
1411+ class MonotonicUUIDsFactory
1412+ {
1413+ import core.sync.mutex : Mutex ;
1414+ import core.time : Duration;
1415+ import std.datetime.stopwatch : StopWatch;
1416+
1417+ private shared Mutex mtx;
1418+ private StopWatch startTimePoint;
1419+
1420+ // /
1421+ this (in SysTime startTime = SysTime.fromUnixTime(0 )) shared
1422+ {
1423+ this (Clock .currTime - startTime);
1424+ }
1425+
1426+ // /
1427+ this (in Duration timeElapsed, bool autostartDisabledForTesting = false ) shared
1428+ {
1429+ mtx = new shared Mutex ();
1430+
1431+ (cast () startTimePoint).setTimeElapsed = timeElapsed;
1432+
1433+ if (! autostartDisabledForTesting)
1434+ (cast () startTimePoint).start();
1435+ }
1436+
1437+ private auto peek () shared
1438+ {
1439+ mtx.lock();
1440+ scope (exit) mtx.unlock();
1441+
1442+ return (cast () startTimePoint).peek;
1443+ }
1444+
1445+ // hnsecs is 1/10_000 of millisecond
1446+ // rand_a size is 12 bits (4096 values)
1447+ private enum float subMsecsPart = 1.0f / 10_000 * 4096 ;
1448+
1449+ /**
1450+ * Returns a monotonic timestamp + random based UUIDv7
1451+ * as described in RFC 9562 (Method 3).
1452+ */
1453+ UUID createUUIDv7_method3 (ubyte [8 ] externalRandom = generateRandomData! 8 ) shared
1454+ {
1455+ const curr = peek.split! (" msecs" , " hnsecs" );
1456+ const qhnsecs = cast (ushort ) (curr.hnsecs * subMsecsPart);
1457+
1458+ ubyte [10 ] rand;
1459+
1460+ // Whole rand_a is 16 bit, but usable only 12 MSB.
1461+ // additional 4 less significant bits consumed
1462+ // by a version value
1463+ rand[0 .. 2 ] = qhnsecs.nativeToBigEndian;
1464+ rand[2 .. $] = externalRandom;
1465+
1466+ return UUID (curr.msecs, rand);
1467+ }
1468+ }
1469+
1470+ // / Generate monotone UUIDs
1471+ @system unittest
1472+ {
1473+ auto f = new shared MonotonicUUIDsFactory;
1474+
1475+ UUID [10 ] monotonic;
1476+
1477+ foreach (ref u; monotonic)
1478+ u = f.createUUIDv7_method3;
1479+ }
1480+
1481+ @system unittest
1482+ {
1483+ import std.conv : to;
1484+ import std.datetime ;
1485+
1486+ const currTime = SysTime(DateTime (2025 , 9 , 12 , 21 , 38 , 45 ), UTC ());
1487+ Duration d = currTime - SysTime.fromUnixTime(0 ) + dur! " msecs" (123 );
1488+
1489+ auto f = new shared MonotonicUUIDsFactory(d, true );
1490+
1491+ const u1 = f.createUUIDv7_method3();
1492+ assert (u1.uuidVersion == UUID .Version.timestampRandom);
1493+
1494+ // sub-millisecond part zeroed
1495+ assert ((u1.data[6 ] & 0b0000_1111) == 0 );
1496+ assert (u1.data[7 ] == 0 );
1497+
1498+ const uuidv7_milli_1 = u1.v7Timestamp;
1499+
1500+ {
1501+ const st = u1.v7Timestamp_method3;
1502+ assert (cast (DateTime ) st == cast (DateTime ) currTime, st.to! string );
1503+
1504+ const sp = st.fracSecs.split! (" msecs" , " usecs" , " hnsecs" );
1505+ assert (sp.msecs == 123 , sp.to! string );
1506+ assert (sp.usecs == 0 , sp.to! string );
1507+ }
1508+
1509+ // 0.3 usecs, but Method 3 precision is only 0.25 of usec,
1510+ // thus, expected value is 2
1511+ d += dur! " hnsecs" (3 );
1512+ f = new shared MonotonicUUIDsFactory(d, true );
1513+
1514+ const u2 = f.createUUIDv7_method3();
1515+ const uuidv7_milli_2 = u2.v7Timestamp;
1516+ assert (uuidv7_milli_1 == uuidv7_milli_2);
1517+
1518+ {
1519+ const st = u2.v7Timestamp_method3;
1520+ assert (cast (DateTime ) st == cast (DateTime ) currTime, st.to! string );
1521+
1522+ const sp = st.fracSecs.split! (" msecs" , " usecs" , " hnsecs" );
1523+ assert (sp.msecs == 123 , sp.to! string );
1524+ assert (sp.usecs == 0 , sp.to! string );
1525+ assert (sp.hnsecs == 2 , sp.to! string );
1526+ }
1527+ }
1528+
1529+ @system unittest
1530+ {
1531+ import core.thread.osthread : Thread ;
1532+ import std.datetime ;
1533+
1534+ scope f = new shared MonotonicUUIDsFactory;
1535+
1536+ UUID [1000 ] uuids;
1537+
1538+ foreach (ref u; uuids)
1539+ {
1540+ // UUIDv7 Method 3 monotonicity is only guaranteed if UUIDs are
1541+ // generated slower than 2.5 microseconds
1542+ Thread .sleep(dur! (" hnsecs" )(25 ));
1543+ u = f.createUUIDv7_method3;
1544+ }
1545+
1546+ foreach (i; 1 .. uuids.length)
1547+ {
1548+ assert (uuids[i- 1 ].v7Timestamp_method3 < uuids[i].v7Timestamp_method3);
1549+ assert (uuids[i- 1 ].data[8 .. $] != uuids[i].data[8 .. $], " random parts are equal" );
1550+ }
1551+ }
1552+
13871553/**
13881554 * This function returns a timestamp + random based UUID aka. uuid v7.
13891555 */
@@ -1800,12 +1966,12 @@ enum uuidRegex = "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}"~
18001966 ]);
18011967}
18021968
1803- private ubyte [10 ] generateV7RandomData () {
1969+ private ubyte [Size] generateRandomData ( ubyte Size) () {
18041970 import std.random : Random , uniform, unpredictableSeed;
18051971
18061972 auto rnd = Random (unpredictableSeed);
18071973
1808- ubyte [10 ] bytes;
1974+ ubyte [Size ] bytes;
18091975 foreach (idx; 0 .. bytes.length)
18101976 {
18111977 bytes[idx] = uniform! (ubyte )(rnd);
0 commit comments