Skip to content

Instantly share code, notes, and snippets.

@sn4k3
Created August 6, 2023 19:42
Show Gist options
  • Save sn4k3/7e2b9282533b17376b1c4a0084143dfa to your computer and use it in GitHub Desktop.
Save sn4k3/7e2b9282533b17376b1c4a0084143dfa to your computer and use it in GitHub Desktop.
A TUID is like a UUID (it conforms to UUID v4) but instead of being fully random (except for 6 bits for the version) it is prefixed with the time since epoch in microseconds.
using System.Buffers;
using System.Buffers.Text;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
namespace Types;
/// <summary>
/// A TUID is like a UUID (it conforms to UUID v4) but instead of being fully random (except for 6 bits for the version) it is prefixed with the time since epoch in microseconds.
/// <remarks>https://github.com/tanglebones/pg_tuid</remarks>
/// </summary>
[StructLayout(LayoutKind.Sequential)]
[Serializable]
public struct Tuid : IComparable, IComparable<Tuid>, IEquatable<Tuid>
{
#region Constants
public const int PartTimestampBytes = 6;
public const int PartRandomBytes = 10;
public const int TotalBufferLength = PartTimestampBytes + PartRandomBytes;
public const int StringRepresentationLength = TotalBufferLength * 2 + 4;
/// <summary>
/// 00000000-0000-0000-0000-000000000000
/// </summary>
public static readonly Tuid Empty;
#endregion
#region Members
private static object _lock = new object();
private static long _lastMillisecondsSinceUnixEpoch;
// 00000000-0000-0000-0000-000000000000
private readonly uint _t1; // Do not rename (binary serialization)
private readonly ushort _t2; // Do not rename (binary serialization)
private readonly ushort _r1; // Do not rename (binary serialization)
private readonly ushort _r2; // Do not rename (binary serialization)
private readonly ushort _r3; // Do not rename (binary serialization)
private readonly uint _r4; // Do not rename (binary serialization)
#endregion
#region Properties
public uint T1 => _t1;
public ushort T2 => _t2;
public ushort R1 => _r1;
public ushort R2 => _r2;
public ushort R3 => _r3;
public uint R4 => _r4;
/// <summary>
/// Gets the total of milliseconds since unix epoch of this tuid
/// </summary>
public long MillisecondsSinceUnixEpoch => ((long)_t1 << 16) | _t2;
public DateTimeOffset DateTimeOffset => DateTimeOffset.FromUnixTimeMilliseconds(MillisecondsSinceUnixEpoch);
public string CompactString => ToCompact();
#endregion
#region Constructors
/// <summary>
/// Creates an empty tuid (00000000-0000-0000-0000-000000000000)
/// </summary>
public Tuid()
{ }
/// <summary>
/// Creates an tuid from a byte array
/// </summary>
/// <param name="bytes">The byte array</param>
/// <exception cref="ArgumentException"></exception>
public Tuid(byte[] bytes) : this(bytes.AsSpan())
{ }
public Tuid(ReadOnlySpan<byte> bytes)
{
if (bytes.Length != TotalBufferLength)
{
throw new ArgumentException($"Invalid array size, expecting {TotalBufferLength}, got {bytes.Length}", nameof(bytes));
}
_t1 = ((uint)bytes[0] << 24) | ((uint)bytes[1] << 16) | ((uint)bytes[2] << 8) | bytes[3];
_t2 = (ushort)((bytes[4] << 8) | bytes[5]);
_r1 = (ushort)((bytes[6] << 8) | bytes[7]);
_r2 = (ushort)((bytes[8] << 8) | bytes[9]);
_r3 = (ushort)((bytes[10] << 8) | bytes[11]);
_r4 = ((uint)bytes[12] << 24) | ((uint)bytes[13] << 16) | ((uint)bytes[14] << 8) | bytes[15];
}
/// <summary>
/// Creates an tuid from multiple bytes<br/>
/// {t1234}-{t56}-{r12}-{r34}-{r510}
/// </summary>
/// <param name="t1"></param>
/// <param name="t2"></param>
/// <param name="t3"></param>
/// <param name="t4"></param>
/// <param name="t5"></param>
/// <param name="t6"></param>
/// <param name="r1"></param>
/// <param name="r2"></param>
/// <param name="r3"></param>
/// <param name="r4"></param>
/// <param name="r5"></param>
/// <param name="r6"></param>
/// <param name="r7"></param>
/// <param name="r8"></param>
/// <param name="r9"></param>
/// <param name="r10"></param>
public Tuid(byte t1, byte t2, byte t3, byte t4,
byte t5, byte t6,
byte r1, byte r2,
byte r3, byte r4,
byte r5, byte r6,
byte r7, byte r8, byte r9, byte r10) : this(new []{t1, t2, t3, t4, t5, t6, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10})
{ }
/// <summary>
/// Creates an tuid from a group of numbers<br/>
/// {a}-{b}-{c}-{d}-{e}{f}
/// </summary>
/// <param name="t1"></param>
/// <param name="t2"></param>
/// <param name="r1"></param>
/// <param name="r2"></param>
/// <param name="r3"></param>
/// <param name="r4"></param>
public Tuid(uint t1, ushort t2, ushort r1, ushort r2, ushort r3, uint r4)
{
_t1 = t1;
_t2 = t2;
_r1 = r1;
_r2 = r2;
_r3 = r3;
_r4 = r4;
}
/// <summary>
/// Creates an tuid from a known time and random numbers
/// </summary>
/// <param name="time"></param>
/// <param name="r1"></param>
/// <param name="r2"></param>
/// <param name="r3"></param>
public Tuid(ulong time, uint r1, ushort r2, uint r3)
{
_t1 = (uint)(time >> 16);
_t2 = (ushort)time;
_r1 = (ushort)(r1 >> 8);
_r2 = (ushort)r1;
_r3 = r2;
_r4 = r3;
}
/// <summary>
/// Creates an tuid based on a known time but with generated random bytes
/// </summary>
/// <param name="millisecondsSinceUnixEpoch"></param>
/// <param name="useStrongCryptoRandom">True to use safer random to generate strong bytes, otherwise false</param>
/// <exception cref="ArgumentOutOfRangeException"></exception>
public Tuid(long millisecondsSinceUnixEpoch, bool useStrongCryptoRandom = true) : this(millisecondsSinceUnixEpoch, GenerateRandomBuffer(useStrongCryptoRandom))
{}
/// <summary>
/// Creates an tuid based on a known time and random bytes
/// </summary>
/// <param name="millisecondsSinceUnixEpoch"></param>
/// <param name="randomBytes">Known random bytes</param>
/// <exception cref="ArgumentOutOfRangeException"></exception>
/// <exception cref="ArgumentException"></exception>
public Tuid(long millisecondsSinceUnixEpoch, byte[] randomBytes)
{
if (randomBytes.Length < PartRandomBytes) throw new ArgumentException($"Random bytes must have a size of {PartRandomBytes} or greater.", nameof(randomBytes));
// xxxxxxxx-xxxx-0000-0000-000000000000
_t1 = (uint)(millisecondsSinceUnixEpoch >> 16);
_t2 = (ushort)millisecondsSinceUnixEpoch;
// 00000000-0000-xxxx-xxxx-xxxxxxxxxxxx
_r1 = (ushort)((randomBytes[0] << 8) | randomBytes[1]);
_r2 = (ushort)((randomBytes[2] << 8) | randomBytes[3]);
_r3 = (ushort)((randomBytes[4] << 8) | randomBytes[5]);
_r4 = ((uint)randomBytes[6] << 24) | ((uint)randomBytes[7] << 16) | ((uint)randomBytes[8] << 8) | randomBytes[9];
}
/// <summary>
/// Creates a random tuid
/// </summary>
/// <param name="useStrongCryptoRandom">True to use safer random to generate strong bytes, otherwise false</param>
private Tuid(bool useStrongCryptoRandom) : this(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), useStrongCryptoRandom)
{ }
/// <summary>
/// Creates an tuid based on a known time but with generated random bytes
/// </summary>
/// <param name="dateTime">Known date time</param>
/// <param name="useStrongCryptoRandom">True to use safer random to generate strong bytes, otherwise false</param>
public Tuid(DateTime dateTime, bool useStrongCryptoRandom = true) : this((long)dateTime.ToUniversalTime().Subtract(DateTime.UnixEpoch).TotalMilliseconds, useStrongCryptoRandom)
{ }
/// <summary>
/// Creates an tuid based on a known time and random bytes
/// </summary>
/// <param name="dateTime">Known date time</param>
/// <param name="randomBytes">Known random bytes</param>
public Tuid(DateTime dateTime, byte[] randomBytes) : this((long)dateTime.ToUniversalTime().Subtract(DateTime.UnixEpoch).TotalMilliseconds, randomBytes)
{ }
/// <summary>
/// Creates an tuid based on a known time but with generated random bytes
/// </summary>
/// <param name="dateTime">Known date time</param>
/// <param name="useStrongCryptoRandom">True to use safer random to generate strong bytes, otherwise false</param>
public Tuid(DateTimeOffset dateTime, bool useStrongCryptoRandom = true) : this(dateTime.ToUnixTimeMilliseconds(), useStrongCryptoRandom)
{ }
/// <summary>
/// Creates an tuid based on a known time and random bytes
/// </summary>
/// <param name="dateTime">Known date time</param>
/// <param name="randomBytes">Known random bytes</param>
public Tuid(DateTimeOffset dateTime, byte[] randomBytes) : this(dateTime.ToUnixTimeMilliseconds(), randomBytes)
{ }
/// <summary>
/// Restore a tuid from a known string.
/// </summary>
/// <param name="tuidString">The full tuid string representation or in compact string form</param>
/// <exception cref="ArgumentException"></exception>
public Tuid(string tuidString)
{
// check for compact version first
if (tuidString.Length is >= 22 and <= 24)
{
var charArray = tuidString.PadRight(24, '=').ToCharArray();
for (int i = 0; i < charArray.Length; i++)
{
if (charArray[i] == '_') charArray[i] = '/';
else if (charArray[i] == '-') charArray[i] = '+';
}
var charByteSpan = Encoding.UTF8.GetBytes(charArray);
var bytes = new byte[TotalBufferLength];
if (Base64.DecodeFromUtf8(charByteSpan, bytes, out _, out _) !=
OperationStatus.Done)
{
throw new ArgumentException("The compact tuid format is not valid.", nameof(tuidString));
}
_t1 = ((uint)bytes[0] << 24) | ((uint)bytes[1] << 16) | ((uint)bytes[2] << 8) | bytes[3];
_t2 = (ushort)((bytes[4] << 8) | bytes[5]);
_r1 = (ushort)((bytes[6] << 8) | bytes[7]);
_r2 = (ushort)((bytes[8] << 8) | bytes[9]);
_r3 = (ushort)((bytes[10] << 8) | bytes[11]);
_r4 = ((uint)bytes[12] << 24) | ((uint)bytes[13] << 16) | ((uint)bytes[14] << 8) | bytes[15];
return;
}
// Check for {tuid} and (tuid)
if (tuidString.Length == StringRepresentationLength + 2 && tuidString[0] is '{' or '(' && tuidString[^1] is '}' or ')')
{
tuidString = tuidString[1..^1];
}
if (tuidString.Length == StringRepresentationLength && tuidString[8] == '-' && tuidString[13] == '-' && tuidString[18] == '-' && tuidString[23] == '-')
{
// TTTTTTTT-TTTT-RRRR-RRRR-RRRRRRRRRRRR
// 00000000-0000-0000-0000-000000000000
_t1 = uint.Parse(tuidString[0..8], NumberStyles.HexNumber);
_t2 = ushort.Parse(tuidString[9..13], NumberStyles.HexNumber);
// -
_r1 = ushort.Parse(tuidString[14..18], NumberStyles.HexNumber);
// -
_r2 = ushort.Parse(tuidString[19..23], NumberStyles.HexNumber);
// -
_r3 = ushort.Parse(tuidString[24..28], NumberStyles.HexNumber);
_r4 = uint.Parse(tuidString[28..36], NumberStyles.HexNumber);
}
else
{
throw new ArgumentException("The tuid format is not valid / known.", nameof(tuidString));
}
}
#endregion
#region Formatters
/// <inheritdoc />
public override string ToString()
{
// 00000000-0000-0000-0000-000000000000
return $"{_t1:x8}-{_t2:x4}-{_r1:x4}-{_r2:x4}-{_r3:x4}{_r4:x8}";
}
#endregion
#region Equality and Compare
public bool Equals(Tuid other)
{
return _t1 == other._t1 && _t2 == other._t2 && _r1 == other._r1 && _r2 == other._r2 && _r3 == other._r3 && _r4 == other._r4;
}
public override bool Equals(object? obj)
{
return obj is Tuid other && Equals(other);
}
public override int GetHashCode()
{
return HashCode.Combine(_t1, _t2, _r1, _r2, _r3, _r4);
}
public int CompareTo(Tuid other)
{
var t1Comparison = _t1.CompareTo(other._t1);
if (t1Comparison != 0) return t1Comparison;
var t2Comparison = _t2.CompareTo(other._t2);
if (t2Comparison != 0) return t2Comparison;
var r1Comparison = _r1.CompareTo(other._r1);
if (r1Comparison != 0) return r1Comparison;
var r2Comparison = _r2.CompareTo(other._r2);
if (r2Comparison != 0) return r2Comparison;
var r3Comparison = _r3.CompareTo(other._r3);
if (r3Comparison != 0) return r3Comparison;
return _r4.CompareTo(other._r4);
}
public int CompareTo(object? obj)
{
if (obj is Tuid other) return CompareTo(other);
return -1;
}
private sealed class TuidEqualityComparer : IEqualityComparer<Tuid>
{
public bool Equals(Tuid x, Tuid y)
{
return x._t1 == y._t1 && x._t2 == y._t2 && x._r1 == y._r1 && x._r2 == y._r2 && x._r3 == y._r3 && x._r4 == y._r4;
}
public int GetHashCode(Tuid obj)
{
return HashCode.Combine(obj._t1, obj._t2, obj._r1, obj._r2, obj._r3, obj._r4);
}
}
public static IEqualityComparer<Tuid> TuidComparer { get; } = new TuidEqualityComparer();
private sealed class TuidRelationalComparer : IComparer<Tuid>
{
public int Compare(Tuid x, Tuid y)
{
var t1Comparison = x._t1.CompareTo(y._t1);
if (t1Comparison != 0) return t1Comparison;
var t2Comparison = x._t2.CompareTo(y._t2);
if (t2Comparison != 0) return t2Comparison;
var r1Comparison = x._r1.CompareTo(y._r1);
if (r1Comparison != 0) return r1Comparison;
var r2Comparison = x._r2.CompareTo(y._r2);
if (r2Comparison != 0) return r2Comparison;
var r3Comparison = x._r3.CompareTo(y._r3);
if (r3Comparison != 0) return r3Comparison;
return x._r4.CompareTo(y._r4);
}
}
#endregion
#region Utilities
/// <summary>
/// Checks if this tuid is empty (00000000-0000-0000-0000-000000000000)
/// </summary>
/// <returns>True if empty (00000000-0000-0000-0000-000000000000), otherwise false</returns>
public bool IsEmpty()
{
return Equals(Empty);
}
/// <summary>Creates an array copy of the buffer data</summary>
/// <returns>An array that contains the elements from the input sequence.</returns>
public byte[] ToArray()
{
return new byte[]
{
(byte)(_t1 >> 24),
(byte)(_t1 >> 16),
(byte)(_t1 >> 8),
(byte)_t1,
(byte)(_t2 >> 8),
(byte)_t2,
(byte)(_r1 >> 8),
(byte)_r1,
(byte)(_r2 >> 8),
(byte)_r2,
(byte)(_r3 >> 8),
(byte)_r3,
(byte)(_r4 >> 24),
(byte)(_r4 >> 16),
(byte)(_r4 >> 8),
(byte)_r4,
};
}
/// <summary>
/// Returns a compact base64 encoded form (takes 22+ bytes)
/// </summary>
/// <returns></returns>
private string ToCompact()
{
var charArray = new char[24];
var byteArray = ToArray();
Convert.ToBase64CharArray(byteArray, 0, byteArray.Length, charArray, 0);
var length = charArray.Length;
for (int i = 0; i < charArray.Length; i++)
{
if (charArray[i] == '/') charArray[i] = '_';
else if (charArray[i] == '+') charArray[i] = '-';
else if (charArray[i] is '=' or '\0')
{
length = i;
break;
}
}
return new string(charArray[..length]);
}
/// <summary>
/// Generates a new <see cref="Tuid"/> using current time and random bytes<br/>
/// This method also ensure that time in this session never repeats
/// </summary>
/// <param name="useStrongCryptoRandom">True to use safer random to generate strong bytes, otherwise false</param>
/// <returns>New <see cref="Tuid"/></returns>
public static Tuid Generate(bool useStrongCryptoRandom = true)
{
long ms = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
lock (_lock)
{
if (ms <= _lastMillisecondsSinceUnixEpoch)
{
_lastMillisecondsSinceUnixEpoch++;
}
else
{
_lastMillisecondsSinceUnixEpoch = ms;
}
}
return new Tuid(_lastMillisecondsSinceUnixEpoch, useStrongCryptoRandom);
}
/// <summary>
/// Gets an buffer[10] with random bytes
/// </summary>
/// <param name="useStrongCryptoRandom">True to use safer random to generate strong bytes, otherwise false</param>
/// <returns></returns>
public static byte[] GenerateRandomBuffer(bool useStrongCryptoRandom = true)
{
var randomBuffer = new byte[PartRandomBytes];
if (useStrongCryptoRandom)
{
RandomNumberGenerator.Fill(randomBuffer);
}
else
{
Random.Shared.NextBytes(randomBuffer);
}
return randomBuffer;
}
#endregion
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment