Skip to content

Commit 4dadb7c

Browse files
denizzzkathewilsonator
authored andcommitted
UUIDv7 Method 3 implementation
1 parent c3f1f26 commit 4dadb7c

File tree

1 file changed

+174
-8
lines changed

1 file changed

+174
-8
lines changed

std/uuid.d

Lines changed: 174 additions & 8 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;
@@ -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

Comments
 (0)