Skip to content

Commit a694be4

Browse files
authored
Add polyfill for Guid.CreateVersion7 and Guid.CreateVersion7(DateTimeOffset) (#149)
1 parent 7e64962 commit a694be4

File tree

5 files changed

+141
-2
lines changed

5 files changed

+141
-2
lines changed
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
partial class PolyfillExtensions
2+
{
3+
extension(System.Guid)
4+
{
5+
public static System.Guid CreateVersion7(System.DateTimeOffset timestamp)
6+
{
7+
// UUIDv7 structure (RFC 9562, Section 5.7):
8+
// 0 1 2 3
9+
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
10+
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
11+
// | unix_ts_ms |
12+
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
13+
// | unix_ts_ms | ver | rand_a |
14+
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
15+
// |var| rand_b |
16+
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
17+
// | rand_b |
18+
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
19+
20+
long unixMs = timestamp.ToUnixTimeMilliseconds();
21+
22+
if (unchecked((ulong)unixMs) > 0xFFFF_FFFF_FFFFUL)
23+
{
24+
throw new System.ArgumentOutOfRangeException(nameof(timestamp));
25+
}
26+
27+
byte[] rand = new byte[10];
28+
using (var rng = System.Security.Cryptography.RandomNumberGenerator.Create())
29+
{
30+
rng.GetBytes(rand);
31+
}
32+
33+
// Set version 7 in high nibble of UUID byte 6 (rand[0])
34+
rand[0] = (byte)((rand[0] & 0x0F) | 0x70);
35+
36+
// Set variant 0b10 in high 2 bits of UUID byte 8 (rand[2])
37+
rand[2] = (byte)((rand[2] & 0x3F) | 0x80);
38+
39+
// Build the Guid from the UUID v7 big-endian byte layout:
40+
// UUID bytes 0-3 → Data1 (upper 32 bits of 48-bit timestamp, big-endian)
41+
// UUID bytes 4-5 → Data2 (lower 16 bits of timestamp, big-endian)
42+
// UUID bytes 6-7 → Data3 (version nibble + rand_a, big-endian)
43+
// UUID bytes 8-15 → Data4 (variant bits + rand_b, stored as-is)
44+
return new System.Guid(
45+
unchecked((int)(unixMs >> 16)),
46+
unchecked((short)(unixMs & 0xFFFF)),
47+
(short)((rand[0] << 8) | rand[1]),
48+
rand[2], rand[3], rand[4], rand[5], rand[6], rand[7], rand[8], rand[9]);
49+
}
50+
}
51+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// when M:System.Guid.CreateVersion7(System.DateTimeOffset)
2+
partial class PolyfillExtensions
3+
{
4+
extension(System.Guid)
5+
{
6+
public static System.Guid CreateVersion7()
7+
{
8+
return System.Guid.CreateVersion7(System.DateTimeOffset.UtcNow);
9+
}
10+
}
11+
}

Meziantou.Polyfill.Tests/SystemTests.cs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2042,4 +2042,79 @@ public void Delegate_EnumerateInvocationList_Null()
20422042
Assert.Equal(0, count);
20432043
}
20442044

2045+
[Fact]
2046+
public void Guid_CreateVersion7_ReturnsVersion7Guid()
2047+
{
2048+
var guid = Guid.CreateVersion7();
2049+
var str = guid.ToString("D");
2050+
2051+
// Version should be 7 (character at position 14 in "D" format: xxxxxxxx-xxxx-Vxxx-...)
2052+
Assert.Equal('7', str[14]);
2053+
2054+
// Variant should be 0b10xx (8, 9, a, or b) at position 19
2055+
Assert.Contains(str[19], new[] { '8', '9', 'a', 'b' });
2056+
}
2057+
2058+
[Fact]
2059+
public void Guid_CreateVersion7_IsSortable()
2060+
{
2061+
// UUID v7 GUIDs with different (1ms apart) timestamps should sort by timestamp
2062+
var timestamp1 = new DateTimeOffset(2024, 1, 1, 0, 0, 0, 0, TimeSpan.Zero);
2063+
var timestamp2 = new DateTimeOffset(2024, 1, 1, 0, 0, 0, 1, TimeSpan.Zero);
2064+
2065+
var guid1 = Guid.CreateVersion7(timestamp1);
2066+
var guid2 = Guid.CreateVersion7(timestamp2);
2067+
2068+
Assert.True(string.Compare(guid1.ToString("D"), guid2.ToString("D"), StringComparison.Ordinal) < 0);
2069+
}
2070+
2071+
[Fact]
2072+
public void Guid_CreateVersion7_WithTimestamp_ReturnsVersion7Guid()
2073+
{
2074+
var timestamp = new DateTimeOffset(2024, 1, 1, 0, 0, 0, TimeSpan.Zero);
2075+
var guid = Guid.CreateVersion7(timestamp);
2076+
var str = guid.ToString("D");
2077+
2078+
// Version should be 7
2079+
Assert.Equal('7', str[14]);
2080+
2081+
// Variant should be 0b10xx (8, 9, a, or b)
2082+
Assert.Contains(str[19], new[] { '8', '9', 'a', 'b' });
2083+
}
2084+
2085+
[Fact]
2086+
public void Guid_CreateVersion7_WithTimestamp_EmbedsTimestamp()
2087+
{
2088+
var timestamp = new DateTimeOffset(2024, 6, 15, 12, 30, 45, 123, TimeSpan.Zero);
2089+
var guid1 = Guid.CreateVersion7(timestamp);
2090+
var guid2 = Guid.CreateVersion7(timestamp);
2091+
2092+
// Both GUIDs created with the same timestamp should have identical timestamp portions
2093+
// (first 12 hex chars in "N" format = 48-bit timestamp)
2094+
var str1 = guid1.ToString("N");
2095+
var str2 = guid2.ToString("N");
2096+
Assert.True(str1.AsSpan(0, 12).SequenceEqual(str2.AsSpan(0, 12)));
2097+
}
2098+
2099+
[Fact]
2100+
public void Guid_CreateVersion7_WithTimestamp_TimestampOrderPreserved()
2101+
{
2102+
var timestamp1 = new DateTimeOffset(2024, 1, 1, 0, 0, 0, TimeSpan.Zero);
2103+
var timestamp2 = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero);
2104+
2105+
var guid1 = Guid.CreateVersion7(timestamp1);
2106+
var guid2 = Guid.CreateVersion7(timestamp2);
2107+
2108+
// GUIDs with earlier timestamps should sort before GUIDs with later timestamps
2109+
Assert.True(string.Compare(guid1.ToString("D"), guid2.ToString("D"), StringComparison.Ordinal) < 0);
2110+
}
2111+
2112+
[Fact]
2113+
public void Guid_CreateVersion7_WithTimestamp_OutOfRange_ThrowsArgumentOutOfRangeException()
2114+
{
2115+
// Timestamp before Unix epoch
2116+
var beforeEpoch = new DateTimeOffset(1969, 12, 31, 23, 59, 59, TimeSpan.Zero);
2117+
Assert.Throws<ArgumentOutOfRangeException>(() => Guid.CreateVersion7(beforeEpoch));
2118+
}
2119+
20452120
}

Meziantou.Polyfill/Meziantou.Polyfill.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Meziantou.NET.Sdk">
22
<PropertyGroup>
33
<TargetFrameworks>netstandard2.0</TargetFrameworks>
4-
<Version>1.0.102</Version>
4+
<Version>1.0.103</Version>
55
<TransformOnBuild>true</TransformOnBuild>
66
<RoslynVersion>4.3.1</RoslynVersion>
77

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ The filtering logic works as follows:
124124
- `System.ValueTuple<T1, T2, T3, T4, T5, T6, T7, TRest> where TRest : struct`
125125
- `System.ITupleInternal`
126126

127-
### Methods (523)
127+
### Methods (525)
128128

129129
- `System.ArgumentException.ThrowIfNullOrEmpty(System.String? argument, [System.String? paramName = null])`
130130
- `System.ArgumentException.ThrowIfNullOrWhiteSpace(System.String? argument, [System.String? paramName = null])`
@@ -211,6 +211,7 @@ The filtering logic works as follows:
211211
- `System.Enum.TryParse<TEnum>(System.ReadOnlySpan<System.Char> value, out TEnum result) where TEnum : struct`
212212
- `System.Enum.TryParse<TEnum>(System.String? value, System.Boolean ignoreCase, out TEnum result) where TEnum : struct`
213213
- `System.Enum.TryParse<TEnum>(System.String? value, out TEnum result) where TEnum : struct`
214+
- `System.Guid.CreateVersion7(System.DateTimeOffset timestamp)`
214215
- `System.IO.File.AppendAllBytes(System.String path, System.Byte[] bytes)`
215216
- `System.IO.File.AppendAllBytes(System.String path, System.ReadOnlySpan<System.Byte> bytes)`
216217
- `System.IO.File.AppendAllBytesAsync(System.String path, System.Byte[] bytes, [System.Threading.CancellationToken cancellationToken = default])`
@@ -641,6 +642,7 @@ The filtering logic works as follows:
641642
- `System.Xml.Linq.XElement.SaveAsync(System.IO.Stream stream, System.Xml.Linq.SaveOptions options, System.Threading.CancellationToken cancellationToken)`
642643
- `System.Xml.Linq.XElement.SaveAsync(System.IO.TextWriter textWriter, System.Xml.Linq.SaveOptions options, System.Threading.CancellationToken cancellationToken)`
643644
- `System.Xml.Linq.XElement.SaveAsync(System.Xml.XmlWriter writer, System.Threading.CancellationToken cancellationToken)`
645+
- `System.Guid.CreateVersion7()`
644646
- `System.Random.GetItems<T>(T[] choices, System.Int32 length)`
645647
- `System.Random.Shuffle<T>(T[] values)`
646648
- `System.Security.Cryptography.RandomNumberGenerator.GetInt32(System.Int32 toExclusive)`

0 commit comments

Comments
 (0)