Skip to content

Commit 05dd547

Browse files
CopilotDaanV2
andcommitted
Fix V7 UUID to use Unix milliseconds per RFC9562 instead of FileTime
Co-authored-by: DaanV2 <2393905+DaanV2@users.noreply.github.com>
1 parent e1749fb commit 05dd547

4 files changed

Lines changed: 80 additions & 36 deletions

File tree

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,37 @@
11
namespace DaanV2.UUID;
22

33
public static partial class V7 {
4-
/// <summary>Extracts the UTC from the UUID</summary>
5-
/// <param name="uuid">The <see cref="UUID"/> to extract the UTC from</param>
6-
/// <returns>The UTC value</returns>
4+
/// <summary>Extracts the Unix milliseconds timestamp from the UUID</summary>
5+
/// <param name="uuid">The <see cref="UUID"/> to extract the timestamp from</param>
6+
/// <returns>Unix timestamp in milliseconds</returns>
77
public static UInt64 ExtractUtc(UUID uuid) {
88
(UInt64 bits48, _, _) = Format.Extract(uuid);
99

1010
return bits48;
1111
}
1212

1313
/// <summary>Extracts the datetime from the UUID</summary>
14-
/// <param name="uuid">The <see cref="UUID"/> to extract the UTC from</param>
14+
/// <param name="uuid">The <see cref="UUID"/> to extract the timestamp from</param>
1515
/// <returns>The <see cref="DateTime"/></returns>
1616
public static DateTime Extract(UUID uuid) {
17-
UInt64 fileUTC = ExtractUtc(uuid);
17+
UInt64 unixMs = ExtractUtc(uuid);
1818

19-
return DateTime.FromFileTimeUtc((Int64)fileUTC);
19+
return UnixMillisecondsToDateTime(unixMs);
20+
}
21+
22+
/// <summary>Converts Unix milliseconds to DateTime</summary>
23+
/// <param name="unixMs">Milliseconds since Unix epoch (1970-01-01 00:00:00 UTC)</param>
24+
/// <returns>The corresponding DateTime in UTC</returns>
25+
private static DateTime UnixMillisecondsToDateTime(UInt64 unixMs) {
26+
// Unix epoch: January 1, 1970 00:00:00 UTC
27+
var unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
28+
29+
try {
30+
return unixEpoch.AddMilliseconds(unixMs);
31+
}
32+
catch (ArgumentOutOfRangeException) {
33+
// If the milliseconds value is too large, return MaxValue
34+
return DateTime.MaxValue;
35+
}
2036
}
2137
}

Library/Static Classes/V7/V7 - Generate.cs

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,15 +65,34 @@ public static UUID Generate(UInt64 utc, ReadOnlySpan<Byte> bytes10) {
6565

6666
/// <inheritdoc cref="Generate()"/>
6767
public static UUID Generate(DateTime timestamp, UInt16 randomA, UInt64 randomB) {
68-
UInt64 t = (UInt64)timestamp.ToFileTimeUtc();
68+
// RFC9562: V7 uses Unix epoch timestamp in milliseconds (48 bits)
69+
UInt64 unixMs = DateTimeToUnixMilliseconds(timestamp);
6970

70-
return Generate(t, randomA, randomB);
71+
return Generate(unixMs, randomA, randomB);
7172
}
7273

7374
/// <inheritdoc cref="Generate()"/>
74-
public static UUID Generate(UInt64 utc, UInt16 randomA, UInt64 randomB) {
75-
Vector128<Byte> u = Format.Create(V7.Version, V7.Variant, utc, randomA, randomB);
75+
/// <param name="unixMs">Unix timestamp in milliseconds (48-bit value)</param>
76+
/// <param name="randomA">12 bits of random data</param>
77+
/// <param name="randomB">62 bits of random data</param>
78+
public static UUID Generate(UInt64 unixMs, UInt16 randomA, UInt64 randomB) {
79+
Vector128<Byte> u = Format.Create(V7.Version, V7.Variant, unixMs, randomA, randomB);
7680

7781
return new UUID(u);
7882
}
83+
84+
/// <summary>Converts a DateTime to Unix milliseconds</summary>
85+
/// <param name="timestamp">The DateTime to convert</param>
86+
/// <returns>Milliseconds since Unix epoch (1970-01-01 00:00:00 UTC)</returns>
87+
private static UInt64 DateTimeToUnixMilliseconds(DateTime timestamp) {
88+
// Unix epoch: January 1, 1970 00:00:00 UTC
89+
var unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
90+
var milliseconds = (timestamp.ToUniversalTime() - unixEpoch).TotalMilliseconds;
91+
92+
// Ensure non-negative and within 48-bit range
93+
if (milliseconds < 0) milliseconds = 0;
94+
if (milliseconds > 0xFFFFFFFFFFFF) milliseconds = 0xFFFFFFFFFFFF;
95+
96+
return (UInt64)milliseconds;
97+
}
7998
}

Tests/Generation/V7Tests.cs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,21 @@ public sealed partial class V7Tests {
55
// From https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-04.html#name-example-of-a-uuidv7-value
66
[Fact(DisplayName = "Given a known example, will generate the expected UUID")]
77
public void TestVector() {
8-
var timestamp = DateTime.FromFileTimeUtc(1645557742000);
8+
// RFC9562 test vector: UUID 017f22e2-79b0-7cc3-98c4-dc0c0c07398f
9+
// First 48 bits are Unix milliseconds: 0x017f22e279b0 = 1645557742000
10+
// This is 2022-02-22 19:22:22 UTC
11+
var unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
12+
var timestamp = unixEpoch.AddMilliseconds(1645557742000);
13+
914
UInt16 randA = (UInt16)0xCC3;
1015
UInt64 randB = (UInt64)0x18C4DC0C0C07398F;
1116

1217
UUID u = V7.Generate(timestamp, randA, randB);
1318

1419
Assert.Equal("017f22e2-79b0-7cc3-98c4-dc0c0c07398f", u.ToString());
1520

16-
Int64 extractedUTC = (Int64)V7.ExtractUtc(u);
17-
Assert.Equal(timestamp.ToFileTimeUtc(), extractedUTC);
21+
UInt64 extractedMs = V7.ExtractUtc(u);
22+
Assert.Equal(1645557742000UL, extractedMs);
1823

1924
DateTime extracted = V7.Extract(u);
2025
Assert.Equal(timestamp, extracted);

Tests/RFC/RFC9562.cs

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -130,9 +130,8 @@ public void RFC9562_V6_Uniqueness() {
130130

131131
[Fact(DisplayName = "RFC9562 Section 5.7 - V7 contains Unix Epoch timestamp")]
132132
public void RFC9562_V7_ContainsTimestamp() {
133-
// Note: V7 only stores 48 bits for timestamp. When using FileTimeUtc, this limits the range.
134-
// Use a timestamp that fits within 48-bit FileTime range for this test
135-
var inputTime = DateTime.FromFileTimeUtc(1645557742000); // Known good value from RFC test vector
133+
// V7 uses Unix epoch milliseconds (RFC9562 Section 5.7)
134+
var inputTime = DateTime.UtcNow;
136135
var uuid = V7.Generate(inputTime);
137136

138137
Assert.Equal(DaanV2.UUID.Version.V7, uuid.Version);
@@ -141,34 +140,39 @@ public void RFC9562_V7_ContainsTimestamp() {
141140
// Extract timestamp and verify round-trip accuracy
142141
var extractedTime = V7.Extract(uuid);
143142

144-
// V7 timestamps should round-trip correctly
145-
Assert.Equal(inputTime, extractedTime);
143+
// V7 only stores milliseconds, so allow up to 1ms difference
144+
var timeDiff = Math.Abs((extractedTime - inputTime).TotalMilliseconds);
145+
Assert.True(timeDiff < 2,
146+
$"Time difference {timeDiff}ms is too large. Input: {inputTime}, Extracted: {extractedTime}");
146147
}
147148

148149
[Fact(DisplayName = "RFC9562 Section 5.7 - V7 UUIDs are sortable by time")]
149150
public void RFC9562_V7_Sortable() {
150-
var uuids = new List<UUID>();
151+
var uuids = new List<(UUID uuid, DateTime time)>();
151152

152-
for (int i = 0; i < 100; i++) {
153-
uuids.Add(V7.Generate());
154-
// Small delay to ensure time progression
155-
if (i % 10 == 0) {
156-
Thread.Sleep(1);
157-
}
153+
// Generate UUIDs with known time gaps
154+
for (int i = 0; i < 20; i++) {
155+
var time = DateTime.UtcNow;
156+
uuids.Add((V7.Generate(time), time));
157+
Thread.Sleep(5); // 5ms delay to ensure time progression
158158
}
159159

160-
// V7 UUIDs should be in ascending order when generated sequentially
161-
// Check that most UUIDs maintain order
162-
int inOrder = 0;
163-
for (int i = 0; i < uuids.Count - 1; i++) {
164-
if (string.Compare(uuids[i].ToString(), uuids[i + 1].ToString(), StringComparison.Ordinal) <= 0) {
165-
inOrder++;
166-
}
167-
}
160+
// Verify that UUIDs generated at different times maintain time order
161+
// Group by millisecond and verify cross-group ordering
162+
var groups = uuids.GroupBy(x => x.time.Ticks / TimeSpan.TicksPerMillisecond).ToList();
168163

169-
// At least 90% should be in order
170-
Assert.True(inOrder >= (uuids.Count - 1) * 0.9,
171-
$"Expected at least 90% in order, got {inOrder}/{uuids.Count - 1}");
164+
// With 5ms delays, we should have multiple distinct timestamps
165+
Assert.True(groups.Count >= 10, $"Expected at least 10 distinct timestamps, got {groups.Count}");
166+
167+
// Compare first UUID from each group - they should be in time order
168+
for (int i = 0; i < groups.Count - 1; i++) {
169+
var earlier = groups[i].First().uuid;
170+
var later = groups[i + 1].First().uuid;
171+
172+
// UUIDs from earlier time should compare less than UUIDs from later time
173+
Assert.True(string.Compare(earlier.ToString(), later.ToString(), StringComparison.Ordinal) < 0,
174+
$"UUID from earlier time should be less than UUID from later time");
175+
}
172176
}
173177

174178
[Fact(DisplayName = "RFC9562 Section 5.7 - V7 uniqueness")]

0 commit comments

Comments
 (0)