fixed seqid test, added timestamp extraction
.NET Test / .NET tests (push) Successful in 1m55s

This commit is contained in:
2026-07-10 22:06:37 +04:00
parent 566c813e8d
commit 8e961352d2
4 changed files with 188 additions and 7 deletions
+41 -1
View File
@@ -1,10 +1,11 @@
using System.Runtime.InteropServices;
using System.Security.Cryptography;
namespace Just.Core;
public static class GuidV8
{
private const long TicksPrecision = TimeSpan.TicksPerMillisecond / 10;
private const long TicksPrecision = TimeSpan.TicksPerMillisecond / 10; // 100-microsecond units
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[ExcludeFromCodeCoverage]
@@ -15,6 +16,12 @@ public static class GuidV8
var epoch = dateTime.Subtract(DateTime.UnixEpoch);
var timestamp = epoch.Ticks / TicksPrecision;
// Negative timestamps can't be encoded correctly due to unsigned bit operations
if (timestamp < 0)
{
throw new ArgumentException("Timestamp must be on or after UnixEpoch (1970-01-01 UTC).", nameof(dateTime));
}
uint tsHigh = (uint)((timestamp >> 16) & 0xFFFFFFFF);
ushort tsLow = (ushort)(timestamp & 0x0000FFFF);
@@ -40,4 +47,37 @@ public static class GuidV8
version,
bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], bytes[8], bytes[9]);
}
/// <summary>
/// Extracts the timestamp from a UUID v8 generated by <see cref="NewGuid(DateTime, RngEntropy)"/>.
/// </summary>
/// <param name="guid">The UUID v8 to extract the timestamp from.</param>
/// <returns>The <see cref="DateTime"/> (UTC) encoded in the GUID's timestamp fields.</returns>
/// <exception cref="ArgumentException">
/// Thrown if <paramref name="guid"/> is not a UUID v8.
/// </exception>
/// <remarks>
/// The returned timestamp has 100-microsecond precision (the resolution used by
/// <see cref="NewGuid(DateTime, RngEntropy)"/>). Sub-100µs components of the original
/// <see cref="DateTime"/> are not recoverable.
/// </remarks>
[Pure]
public static DateTime ExtractTimestamp(Guid guid)
{
Span<byte> bytes = stackalloc byte[16];
guid.TryWriteBytes(bytes);
// Version is the high nibble of byte 7 (parameter 'c' high byte, little-endian byte 7)
var version = bytes[7] >> 4;
if (version != 8)
throw new ArgumentException($"The provided GUID is not a UUID v8 (version={version}).", nameof(guid));
// tsHigh is stored as Int32 at bytes 0-3
uint tsHigh = MemoryMarshal.Read<uint>(bytes);
// tsLow is stored as Int16 at bytes 4-5
ushort tsLow = MemoryMarshal.Read<ushort>(bytes[4..]);
long timestamp = ((long)tsHigh << 16) | tsLow;
return DateTime.UnixEpoch.AddTicks(timestamp * TicksPrecision);
}
}
+3 -3
View File
@@ -48,7 +48,7 @@ public sealed class SeqId(DateTime epoch)
/// <param name="entropy">Entropy quality (default: Strong)</param>
/// <returns>64-bit sequential ID with random component</returns>
/// <exception cref="IndexOutOfRangeException">
/// Thrown if more than 255 IDs generated in 1ms
/// Thrown if more than 256 IDs generated in 1ms
/// </exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static long NextId(RngEntropy entropy = RngEntropy.Strong) => Default.Next(entropy);
@@ -70,7 +70,7 @@ public sealed class SeqId(DateTime epoch)
/// <param name="entropy">Entropy quality (default: Strong)</param>
/// <returns>64-bit sequential ID with random component</returns>
/// <exception cref="IndexOutOfRangeException">
/// Thrown if more than 255 IDs generated in 1ms
/// Thrown if more than 256 IDs generated in 1ms
/// </exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public long Next(RngEntropy entropy = RngEntropy.Strong) => Next(_defaultTimeFactory(), entropy);
@@ -85,7 +85,7 @@ public sealed class SeqId(DateTime epoch)
/// Thrown if <paramref name="dateTime"/> is earlier than last used timestamp
/// </exception>
/// <exception cref="IndexOutOfRangeException">
/// Thrown if more than 255 IDs generated in 1ms
/// Thrown if more than 256 IDs generated in 1ms
/// </exception>
public long Next(DateTime dateTime, RngEntropy entropy = RngEntropy.Strong)
{