Skip to content

Commit d8ee376

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

File tree

1 file changed

+130
-10
lines changed

1 file changed

+130
-10
lines changed

std/uuid.d

Lines changed: 130 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,104 @@ 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;
1408+
import std.datetime.stopwatch;
1409+
1410+
private shared Mutex mtx;
1411+
private __gshared StopWatch epochTimePoint;
1412+
1413+
///
1414+
this(in SysTime startTime = SysTime.fromUnixTime(0)) shared
1415+
{
1416+
mtx = new shared Mutex();
1417+
1418+
epochTimePoint.start();
1419+
epochTimePoint.setTimeElapsed = Clock.currTime - startTime;
1420+
}
1421+
1422+
// hnsecs is 1/10_000 of millisecond
1423+
// rand_a size is 12 bits (4096 values)
1424+
private enum float subMsecsPart = 1.0f / 10_000 * 4096;
1425+
1426+
/**
1427+
* Returns a monotonic timestamp + random based UUIDv7
1428+
* as described in RFC 9562 (Method 3).
1429+
*/
1430+
UUID createUUIDv7_method3(ubyte[8] rnd = generateV7RandomData!8) shared
1431+
{
1432+
mtx.lock();
1433+
scope(exit) mtx.unlock();
1434+
1435+
const dur = epochTimePoint.peek;
1436+
const curr = dur.split!("msecs", "hnsecs");
1437+
const qhnsecs = cast(ushort) (curr.hnsecs * subMsecsPart);
1438+
1439+
ubyte[10] rand;
1440+
1441+
// Whole rand_a is 16 bit, but usable only 12 MSB.
1442+
// additional 4 less significant bits consumed
1443+
// by a version value
1444+
rand[0 .. 2] = qhnsecs.nativeToBigEndian;
1445+
rand[2 .. $] = rnd;
1446+
1447+
return UUID(curr.msecs, rand);
1448+
}
1449+
}
1450+
1451+
unittest
1452+
{
1453+
import std.conv : to;
1454+
import std.datetime;
1455+
1456+
auto f = new shared MonotonicUUIDsFactory;
1457+
1458+
// trick to give reproducible testing
1459+
Duration setElapsedOffset(Duration dura){
1460+
if (f.epochTimePoint.running)
1461+
f.epochTimePoint.stop();
1462+
1463+
const st = SysTime(DateTime(2025, 9, 12, 21, 38, 45), UTC());
1464+
Duration ret = st - SysTime.fromUnixTime(0) + dura;
1465+
f.epochTimePoint.setTimeElapsed = ret;
1466+
return ret;
1467+
}
1468+
1469+
Duration d = dur!"msecs"(123);
1470+
setElapsedOffset(d);
1471+
1472+
const uuidv7_milli = f.createUUIDv7_method3().v7Timestamp;
1473+
1474+
{
1475+
const st = f.createUUIDv7_method3().v7Timestamp_method3;
1476+
assert(cast(DateTime) st == DateTime(2025, 9, 12, 21, 38, 45), st.to!string);
1477+
1478+
const sp = st.fracSecs.split!("msecs", "usecs");
1479+
assert(sp.msecs == 123, sp.to!string);
1480+
assert(sp.usecs == 0, sp.to!string);
1481+
}
1482+
1483+
// 0.3 usecs, but Method 3 precision is only 0.25 of usec,
1484+
// thus, expected value is 2
1485+
d += dur!"hnsecs"(3);
1486+
setElapsedOffset(d);
1487+
1488+
const uuidv7_milli_2 = f.createUUIDv7_method3().v7Timestamp;
1489+
assert(uuidv7_milli == uuidv7_milli_2);
1490+
1491+
{
1492+
const st = f.createUUIDv7_method3().v7Timestamp_method3;
1493+
assert(cast(DateTime) st == DateTime(2025, 9, 12, 21, 38, 45), st.to!string);
1494+
1495+
const sp = st.fracSecs.split!("msecs", "usecs", "hnsecs");
1496+
assert(sp.msecs == 123, sp.to!string);
1497+
assert(sp.usecs == 0, sp.to!string);
1498+
assert(sp.hnsecs == 2, sp.to!string);
1499+
}
1500+
}
1501+
13811502
/**
13821503
* This function returns a timestamp + random based UUID aka. uuid v7.
13831504
*/
@@ -1794,12 +1915,12 @@ enum uuidRegex = "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}"~
17941915
]);
17951916
}
17961917

1797-
private ubyte[10] generateV7RandomData() {
1918+
private ubyte[Size] generateV7RandomData(ubyte Size)() {
17981919
import std.random : Random, uniform, unpredictableSeed;
17991920

18001921
auto rnd = Random(unpredictableSeed!(ubyte)());
18011922

1802-
ubyte[10] bytes;
1923+
ubyte[Size] bytes;
18031924
foreach (idx; 0 .. bytes.length)
18041925
{
18051926
bytes[idx] = uniform!(ubyte)(rnd);
@@ -1901,6 +2022,5 @@ public class UUIDParsingException : Exception
19012022
{
19022023
import std.datetime : DateTime, SysTime;
19032024
UUID u = UUID("0198c2b2-c5a8-7a0f-a1db-86aac7906c7b");
1904-
auto d = DateTime(2025,8,19);
1905-
assert((cast(DateTime) u.v7Timestamp()).year == d.year);
2025+
assert(u.v7Timestamp.toISOExtString == "2025-08-19T14:19:12.68Z", u.v7Timestamp.toISOExtString);
19062026
}

0 commit comments

Comments
 (0)