From 37cbb461a7ec8f60d846dfd1009fd49d8f642d84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joonatan=20Uusv=C3=A4li?= Date: Fri, 3 Jul 2026 14:24:11 +0300 Subject: [PATCH 1/5] Experiment: `using` instead of try-finally, cache-aware state. Simple new Ulid generation: 38ns -> 34ns. --- .../ByteAether.Benchmarks.csproj | 1 + src/ByteAether.Ulid.Benchmarks/Program.cs | 9 +- src/ByteAether.Ulid.sln | 6 + src/ByteAether.Ulid/Ulid.Guid.cs | 2 + src/ByteAether.Ulid2/ByteAether.Ulid2.csproj | 63 +++ src/ByteAether.Ulid2/Compatibility/Index.cs | 176 +++++++ .../Compatibility/IsExternalInit.cs | 28 ++ .../Compatibility/MemoryMarshal.cs | 30 ++ .../Compatibility/NullableAttributes.cs | 208 ++++++++ .../RandomNumberGeneratorExtensions.cs | 29 ++ src/ByteAether.Ulid2/Compatibility/Range.cs | 141 ++++++ .../CryptographicallySecureRandomProvider.cs | 22 + src/ByteAether.Ulid2/IRandomProvider.cs | 18 + src/ByteAether.Ulid2/LowLatencyLock.cs | 123 +++++ src/ByteAether.Ulid2/PACKAGE.md | 205 ++++++++ src/ByteAether.Ulid2/PseudoRandomProvider.cs | 33 ++ src/ByteAether.Ulid2/Ulid.Boundaries.cs | 68 +++ src/ByteAether.Ulid2/Ulid.Comparable.cs | 125 +++++ src/ByteAether.Ulid2/Ulid.Equatable.cs | 125 +++++ src/ByteAether.Ulid2/Ulid.Guid.cs | 134 +++++ src/ByteAether.Ulid2/Ulid.IsValid.cs | 128 +++++ .../Ulid.New.GenerationOptions.cs | 142 ++++++ src/ByteAether.Ulid2/Ulid.New.cs | 350 +++++++++++++ src/ByteAether.Ulid2/Ulid.Obsolete.cs | 102 ++++ src/ByteAether.Ulid2/Ulid.String.cs | 458 ++++++++++++++++++ src/ByteAether.Ulid2/Ulid.cs | 162 +++++++ src/ByteAether.Ulid2/UlidJsonConverter.cs | 69 +++ src/ByteAether.Ulid2/UlidTypeConverter.cs | 48 ++ 28 files changed, 3002 insertions(+), 3 deletions(-) create mode 100644 src/ByteAether.Ulid2/ByteAether.Ulid2.csproj create mode 100644 src/ByteAether.Ulid2/Compatibility/Index.cs create mode 100644 src/ByteAether.Ulid2/Compatibility/IsExternalInit.cs create mode 100644 src/ByteAether.Ulid2/Compatibility/MemoryMarshal.cs create mode 100644 src/ByteAether.Ulid2/Compatibility/NullableAttributes.cs create mode 100644 src/ByteAether.Ulid2/Compatibility/RandomNumberGeneratorExtensions.cs create mode 100644 src/ByteAether.Ulid2/Compatibility/Range.cs create mode 100644 src/ByteAether.Ulid2/CryptographicallySecureRandomProvider.cs create mode 100644 src/ByteAether.Ulid2/IRandomProvider.cs create mode 100644 src/ByteAether.Ulid2/LowLatencyLock.cs create mode 100644 src/ByteAether.Ulid2/PACKAGE.md create mode 100644 src/ByteAether.Ulid2/PseudoRandomProvider.cs create mode 100644 src/ByteAether.Ulid2/Ulid.Boundaries.cs create mode 100644 src/ByteAether.Ulid2/Ulid.Comparable.cs create mode 100644 src/ByteAether.Ulid2/Ulid.Equatable.cs create mode 100644 src/ByteAether.Ulid2/Ulid.Guid.cs create mode 100644 src/ByteAether.Ulid2/Ulid.IsValid.cs create mode 100644 src/ByteAether.Ulid2/Ulid.New.GenerationOptions.cs create mode 100644 src/ByteAether.Ulid2/Ulid.New.cs create mode 100644 src/ByteAether.Ulid2/Ulid.Obsolete.cs create mode 100644 src/ByteAether.Ulid2/Ulid.String.cs create mode 100644 src/ByteAether.Ulid2/Ulid.cs create mode 100644 src/ByteAether.Ulid2/UlidJsonConverter.cs create mode 100644 src/ByteAether.Ulid2/UlidTypeConverter.cs diff --git a/src/ByteAether.Ulid.Benchmarks/ByteAether.Benchmarks.csproj b/src/ByteAether.Ulid.Benchmarks/ByteAether.Benchmarks.csproj index 6f08bcf..ba6a5b7 100644 --- a/src/ByteAether.Ulid.Benchmarks/ByteAether.Benchmarks.csproj +++ b/src/ByteAether.Ulid.Benchmarks/ByteAether.Benchmarks.csproj @@ -15,6 +15,7 @@ + diff --git a/src/ByteAether.Ulid.Benchmarks/Program.cs b/src/ByteAether.Ulid.Benchmarks/Program.cs index feb3ec1..d5cc64a 100644 --- a/src/ByteAether.Ulid.Benchmarks/Program.cs +++ b/src/ByteAether.Ulid.Benchmarks/Program.cs @@ -13,8 +13,8 @@ .HideColumns(Column.Job, Column.StdDev, Column.Median) ; -BenchmarkRunner.Run( - typeof(Program).Assembly, +BenchmarkRunner.Run( + //typeof(Program).Assembly, benchmarkConfig ); @@ -52,6 +52,9 @@ public class Generate public ByteAether.Ulid.Ulid ByteAetherUlid() => ByteAether.Ulid.Ulid.New(); [Benchmark] + public ByteAether.Ulid2.Ulid ByteAetherUlid2() => ByteAether.Ulid2.Ulid.New(); + + /*[Benchmark] public ByteAether.Ulid.Ulid ByteAetherUlidR1Bp() => ByteAether.Ulid.Ulid.New(_byteAetherUlidOptionsR1Bp); [Benchmark] @@ -67,7 +70,7 @@ public class Generate public NetUlid.Ulid NetUlid() => global::NetUlid.Ulid.Generate(); [Benchmark] - public NUlid.Ulid NUlid() => global::NUlid.Ulid.NewUlid(_nUlidRandomProvider); + public NUlid.Ulid NUlid() => global::NUlid.Ulid.NewUlid(_nUlidRandomProvider);*/ } [MemoryDiagnoser] diff --git a/src/ByteAether.Ulid.sln b/src/ByteAether.Ulid.sln index 7fb400e..a898e6b 100644 --- a/src/ByteAether.Ulid.sln +++ b/src/ByteAether.Ulid.sln @@ -18,6 +18,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ByteAether.Ulid.Tests.AotConsole", "ByteAether.Ulid.Tests.AotConsole\ByteAether.Ulid.Tests.AotConsole.csproj", "{A16BAD2D-8F7F-49E5-880A-349E31F6E70E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ByteAether.Ulid2", "ByteAether.Ulid2\ByteAether.Ulid2.csproj", "{8B5C57B8-11A3-4D0B-A444-9BAC50A339B6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -40,6 +42,10 @@ Global {A16BAD2D-8F7F-49E5-880A-349E31F6E70E}.Debug|Any CPU.Build.0 = Debug|Any CPU {A16BAD2D-8F7F-49E5-880A-349E31F6E70E}.Release|Any CPU.ActiveCfg = Release|Any CPU {A16BAD2D-8F7F-49E5-880A-349E31F6E70E}.Release|Any CPU.Build.0 = Release|Any CPU + {8B5C57B8-11A3-4D0B-A444-9BAC50A339B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8B5C57B8-11A3-4D0B-A444-9BAC50A339B6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8B5C57B8-11A3-4D0B-A444-9BAC50A339B6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8B5C57B8-11A3-4D0B-A444-9BAC50A339B6}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/ByteAether.Ulid/Ulid.Guid.cs b/src/ByteAether.Ulid/Ulid.Guid.cs index 028ecec..c51e3a1 100644 --- a/src/ByteAether.Ulid/Ulid.Guid.cs +++ b/src/ByteAether.Ulid/Ulid.Guid.cs @@ -3,8 +3,10 @@ using System.Runtime.InteropServices; #if NETCOREAPP using System.Runtime.Intrinsics; +#if !NET7_0_OR_GREATER using System.Runtime.Intrinsics.X86; #endif +#endif namespace ByteAether.Ulid; diff --git a/src/ByteAether.Ulid2/ByteAether.Ulid2.csproj b/src/ByteAether.Ulid2/ByteAether.Ulid2.csproj new file mode 100644 index 0000000..dc56f79 --- /dev/null +++ b/src/ByteAether.Ulid2/ByteAether.Ulid2.csproj @@ -0,0 +1,63 @@ + + + + net10.0;net9.0;net8.0;net7.0;net6.0;net5.0;netstandard2.1;netstandard2.0 + latest + library + + enable + enable + true + true + + true + true + true + + true + true + snupkg + + ByteAether.Ulid - High-Performance ULID for .NET + A fast, spec-compliant ULID (Universally Unique Lexicographically Sortable Identifier) implementation for modern .NET. Optimized for database primary keys, Native AOT, zero-allocation, GUID interoperability, and high-performance serialization. + Joonatan Uusväli + ByteAether + © $(Authors), $(Company). All rights reserved. + + https://github.com/ByteAether/Ulid + git + + ByteAether.Ulid2 + ulid;id;uuid;guid;id-generator;unique-id;aot-ready;efcore;dotnet;csharp + $(RepositoryUrl)/tree/v$(PackageVersion) + See $(RepositoryUrl)/releases/tag/v$(PackageVersion) + PACKAGE.md + MIT + logo.png + + + + + + + + + + + + + + True + \ + + + True + \ + + + True + \ + + + + diff --git a/src/ByteAether.Ulid2/Compatibility/Index.cs b/src/ByteAether.Ulid2/Compatibility/Index.cs new file mode 100644 index 0000000..8626651 --- /dev/null +++ b/src/ByteAether.Ulid2/Compatibility/Index.cs @@ -0,0 +1,176 @@ +#if NETSTANDARD2_0 + +// ReSharper disable All +#pragma warning disable +// https://github.com/dotnet/runtime/blob/v9.0.0/src/libraries/System.Private.CoreLib/src/System/Index.cs + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace System +{ + /// Represent a type can be used to index a collection either from the start or the end. + /// + /// Index is used by the C# compiler to support the new index syntax + /// + /// int[] someArray = new int[5] { 1, 2, 3, 4, 5 } ; + /// int lastElement = someArray[^1]; // lastElement = 5 + /// + /// +#if SYSTEM_PRIVATE_CORELIB || MICROSOFT_BCL_MEMORY + public +#else + internal +#endif + readonly struct Index : IEquatable + { + private readonly int _value; + + /// Construct an Index using a value and indicating if the index is from the start or from the end. + /// The index value. it has to be zero or positive number. + /// Indicating if the index is from the start or from the end. + /// + /// If the Index constructed from the end, index value 1 means pointing at the last element and index value 0 means pointing at beyond last element. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Index(int value, bool fromEnd = false) + { + if (value < 0) + { + ThrowValueArgumentOutOfRange_NeedNonNegNumException(); + } + + if (fromEnd) + _value = ~value; + else + _value = value; + } + + // The following private constructors mainly created for perf reason to avoid the checks + private Index(int value) + { + _value = value; + } + + /// Create an Index pointing at first element. + public static Index Start => new Index(0); + + /// Create an Index pointing at beyond last element. + public static Index End => new Index(~0); + + /// Create an Index from the start at the position indicated by the value. + /// The index value from the start. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Index FromStart(int value) + { + if (value < 0) + { + ThrowValueArgumentOutOfRange_NeedNonNegNumException(); + } + + return new Index(value); + } + + /// Create an Index from the end at the position indicated by the value. + /// The index value from the end. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Index FromEnd(int value) + { + if (value < 0) + { + ThrowValueArgumentOutOfRange_NeedNonNegNumException(); + } + + return new Index(~value); + } + + /// Returns the index value. + public int Value + { + get + { + if (_value < 0) + return ~_value; + else + return _value; + } + } + + /// Indicates whether the index is from the start or the end. + public bool IsFromEnd => _value < 0; + + /// Calculate the offset from the start using the giving collection length. + /// The length of the collection that the Index will be used with. length has to be a positive value + /// + /// For performance reason, we don't validate the input length parameter and the returned offset value against negative values. + /// we don't validate either the returned offset is greater than the input length. + /// It is expected Index will be used with collections which always have non negative length/count. If the returned offset is negative and + /// then used to index a collection will get out of range exception which will be same affect as the validation. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int GetOffset(int length) + { + int offset = _value; + if (IsFromEnd) + { + // offset = length - (~value) + // offset = length + (~(~value) + 1) + // offset = length + value + 1 + + offset += length + 1; + } + return offset; + } + + /// Indicates whether the current Index object is equal to another object of the same type. + /// An object to compare with this object + public override bool Equals([NotNullWhen(true)] object? value) => value is Index && _value == ((Index)value)._value; + + /// Indicates whether the current Index object is equal to another Index object. + /// An object to compare with this object + public bool Equals(Index other) => _value == other._value; + + /// Returns the hash code for this instance. + public override int GetHashCode() => _value; + + /// Converts integer number to an Index. + public static implicit operator Index(int value) => FromStart(value); + + /// Converts the value of the current Index object to its equivalent string representation. + public override string ToString() + { + if (IsFromEnd) + return ToStringFromEnd(); + + return ((uint)Value).ToString(); + } + + private static void ThrowValueArgumentOutOfRange_NeedNonNegNumException() + { +#if SYSTEM_PRIVATE_CORELIB + throw new ArgumentOutOfRangeException("value", SR.ArgumentOutOfRange_NeedNonNegNum); +#else + throw new ArgumentOutOfRangeException("value", "value must be non-negative"); +#endif + } + + private string ToStringFromEnd() + { +#if (!NETSTANDARD2_0 && !NETFRAMEWORK) + Span span = stackalloc char[11]; // 1 for ^ and 10 for longest possible uint value + bool formatted = ((uint)Value).TryFormat(span.Slice(1), out int charsWritten); + Debug.Assert(formatted); + span[0] = '^'; + return new string(span.Slice(0, charsWritten + 1)); +#else + return '^' + Value.ToString(); +#endif + } + } +} + +#endif \ No newline at end of file diff --git a/src/ByteAether.Ulid2/Compatibility/IsExternalInit.cs b/src/ByteAether.Ulid2/Compatibility/IsExternalInit.cs new file mode 100644 index 0000000..c90ddb0 --- /dev/null +++ b/src/ByteAether.Ulid2/Compatibility/IsExternalInit.cs @@ -0,0 +1,28 @@ +#if !NET5_0_OR_GREATER + +// ReSharper disable All +#pragma warning disable +// https://github.com/dotnet/runtime/blob/v9.0.0/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/IsExternalInit.cs + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel; + +namespace System.Runtime.CompilerServices +{ + /// + /// Reserved to be used by the compiler for tracking metadata. + /// This class should not be used by developers in source code. + /// + [EditorBrowsable(EditorBrowsableState.Never)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + static class IsExternalInit + { + } +} +#endif \ No newline at end of file diff --git a/src/ByteAether.Ulid2/Compatibility/MemoryMarshal.cs b/src/ByteAether.Ulid2/Compatibility/MemoryMarshal.cs new file mode 100644 index 0000000..ffe520d --- /dev/null +++ b/src/ByteAether.Ulid2/Compatibility/MemoryMarshal.cs @@ -0,0 +1,30 @@ +#if NETSTANDARD +using System.Runtime.CompilerServices; + +// ReSharper disable All +#pragma warning disable + +// https://github.com/dotnet/runtime/blob/8d796d8e60a5236cbd5f113ead1d3831064cdba1/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/MemoryMarshal.cs#L226 + +namespace ByteAether.Ulid2.Compatibility; + +public static class MemoryMarshal +{ + /// + /// Creates a new span over a portion of a regular managed object. This can be useful + /// if part of a managed object represents a "fixed array." This is dangerous because the + /// is not checked. + /// + /// A reference to data. + /// The number of elements the memory contains. + /// A span representing the specified reference and length. + /// + /// This method should be used with caution. It is dangerous because the length argument is not checked. + /// Even though the ref is annotated as scoped, it will be stored into the returned span, and the lifetime + /// of the returned span will not be validated for safety, even by span-aware languages. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static Span CreateSpan(scoped ref T reference, int length) => + new Span(Unsafe.AsPointer(ref reference), length); +} +#endif \ No newline at end of file diff --git a/src/ByteAether.Ulid2/Compatibility/NullableAttributes.cs b/src/ByteAether.Ulid2/Compatibility/NullableAttributes.cs new file mode 100644 index 0000000..8d14d62 --- /dev/null +++ b/src/ByteAether.Ulid2/Compatibility/NullableAttributes.cs @@ -0,0 +1,208 @@ +#if NETSTANDARD2_0 + +// ReSharper disable All +#pragma warning disable +// https://github.com/dotnet/runtime/blob/v9.0.0/src/libraries/System.Private.CoreLib/src/System/Diagnostics/CodeAnalysis/NullableAttributes.cs + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Diagnostics.CodeAnalysis +{ +#if !NETSTANDARD2_1 + /// Specifies that null is allowed as an input even if the corresponding type disallows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + sealed class AllowNullAttribute : Attribute + { } + + /// Specifies that null is disallowed as an input even if the corresponding type allows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + sealed class DisallowNullAttribute : Attribute + { } + + /// Specifies that an output may be null even if the corresponding type disallows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + sealed class MaybeNullAttribute : Attribute + { } + + /// Specifies that an output will not be null even if the corresponding type allows it. Specifies that an input argument was not null when the call returns. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + sealed class NotNullAttribute : Attribute + { } + + /// Specifies that when a method returns , the parameter may be null even if the corresponding type disallows it. + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + sealed class MaybeNullWhenAttribute : Attribute + { + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter may be null. + /// + public MaybeNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + /// Gets the return value condition. + public bool ReturnValue { get; } + } + + /// Specifies that when a method returns , the parameter will not be null even if the corresponding type allows it. + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + sealed class NotNullWhenAttribute : Attribute + { + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + /// Gets the return value condition. + public bool ReturnValue { get; } + } + + /// Specifies that the output will be non-null if the named parameter is non-null. + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + sealed class NotNullIfNotNullAttribute : Attribute + { + /// Initializes the attribute with the associated parameter name. + /// + /// The associated parameter name. The output will be non-null if the argument to the parameter specified is non-null. + /// + public NotNullIfNotNullAttribute(string parameterName) => ParameterName = parameterName; + + /// Gets the associated parameter name. + public string ParameterName { get; } + } + + /// Applied to a method that will never return under any circumstance. + [AttributeUsage(AttributeTargets.Method, Inherited = false)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + sealed class DoesNotReturnAttribute : Attribute + { } + + /// Specifies that the method will not return if the associated Boolean parameter is passed the specified value. + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + sealed class DoesNotReturnIfAttribute : Attribute + { + /// Initializes the attribute with the specified parameter value. + /// + /// The condition parameter value. Code after the method will be considered unreachable by diagnostics if the argument to + /// the associated parameter matches this value. + /// + public DoesNotReturnIfAttribute(bool parameterValue) => ParameterValue = parameterValue; + + /// Gets the condition parameter value. + public bool ParameterValue { get; } + } +#endif + + /// Specifies that the method or property will ensure that the listed field and property members have not-null values. + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + sealed class MemberNotNullAttribute : Attribute + { + /// Initializes the attribute with a field or property member. + /// + /// The field or property member that is promised to be not-null. + /// + public MemberNotNullAttribute(string member) => Members = [member]; + + /// Initializes the attribute with the list of field and property members. + /// + /// The list of field and property members that are promised to be not-null. + /// + public MemberNotNullAttribute(params string[] members) => Members = members; + + /// Gets field or property member names. + public string[] Members { get; } + } + + /// Specifies that the method or property will ensure that the listed field and property members have not-null values when returning with the specified return value condition. + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + sealed class MemberNotNullWhenAttribute : Attribute + { + /// Initializes the attribute with the specified return value condition and a field or property member. + /// + /// The return value condition. If the method returns this value, the associated field or property member will not be null. + /// + /// + /// The field or property member that is promised to be not-null. + /// + public MemberNotNullWhenAttribute(bool returnValue, string member) + { + ReturnValue = returnValue; + Members = [member]; + } + + /// Initializes the attribute with the specified return value condition and list of field and property members. + /// + /// The return value condition. If the method returns this value, the associated field and property members will not be null. + /// + /// + /// The list of field and property members that are promised to be not-null. + /// + public MemberNotNullWhenAttribute(bool returnValue, params string[] members) + { + ReturnValue = returnValue; + Members = members; + } + + /// Gets the return value condition. + public bool ReturnValue { get; } + + /// Gets field or property member names. + public string[] Members { get; } + } +} +#endif \ No newline at end of file diff --git a/src/ByteAether.Ulid2/Compatibility/RandomNumberGeneratorExtensions.cs b/src/ByteAether.Ulid2/Compatibility/RandomNumberGeneratorExtensions.cs new file mode 100644 index 0000000..81a3445 --- /dev/null +++ b/src/ByteAether.Ulid2/Compatibility/RandomNumberGeneratorExtensions.cs @@ -0,0 +1,29 @@ +#if NETSTANDARD2_0 +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Security.Cryptography; + +// ReSharper disable once CheckNamespace +namespace ByteAether.Ulid2; + +internal static class RandomNumberGeneratorExtensions +{ + // In NetStandard 2.0, RandomNumberGenerator.GetBytes() does not support Span overloads. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void GetBytes(this RandomNumberGenerator rng, Span buffer) + { + var rndInc = ArrayPool.Shared.Rent(buffer.Length); + rng.GetBytes(rndInc, 0, buffer.Length); + new ReadOnlySpan(rndInc, 0, buffer.Length).CopyTo(buffer); + } + + // In NetStandard 2.0, Random.NextBytes() does not support Span overloads. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void NextBytes(this Random rng, Span buffer) + { + var rndInc = ArrayPool.Shared.Rent(buffer.Length); + rng.NextBytes(rndInc); + new ReadOnlySpan(rndInc, 0, buffer.Length).CopyTo(buffer); + } +} +#endif \ No newline at end of file diff --git a/src/ByteAether.Ulid2/Compatibility/Range.cs b/src/ByteAether.Ulid2/Compatibility/Range.cs new file mode 100644 index 0000000..c99e735 --- /dev/null +++ b/src/ByteAether.Ulid2/Compatibility/Range.cs @@ -0,0 +1,141 @@ +#if NETSTANDARD2_0 + +// ReSharper disable All +#pragma warning disable +// https://github.com/dotnet/runtime/blob/v9.0.0/src/libraries/System.Private.CoreLib/src/System/Range.cs + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace System +{ + /// Represent a range has start and end indexes. + /// + /// Range is used by the C# compiler to support the range syntax. + /// + /// int[] someArray = new int[5] { 1, 2, 3, 4, 5 }; + /// int[] subArray1 = someArray[0..2]; // { 1, 2 } + /// int[] subArray2 = someArray[1..^0]; // { 2, 3, 4, 5 } + /// + /// +#if SYSTEM_PRIVATE_CORELIB || MICROSOFT_BCL_MEMORY + public +#else + internal +#endif + readonly struct Range : IEquatable + { + /// Represent the inclusive start index of the Range. + public Index Start { get; } + + /// Represent the exclusive end index of the Range. + public Index End { get; } + + /// Construct a Range object using the start and end indexes. + /// Represent the inclusive start index of the range. + /// Represent the exclusive end index of the range. + public Range(Index start, Index end) + { + Start = start; + End = end; + } + + /// Indicates whether the current Range object is equal to another object of the same type. + /// An object to compare with this object + public override bool Equals([NotNullWhen(true)] object? value) => + value is Range r && + r.Start.Equals(Start) && + r.End.Equals(End); + + /// Indicates whether the current Range object is equal to another Range object. + /// An object to compare with this object + public bool Equals(Range other) => other.Start.Equals(Start) && other.End.Equals(End); + + /// Returns the hash code for this instance. + public override int GetHashCode() + { +#if (!NETSTANDARD2_0 && !NETFRAMEWORK) + return HashCode.Combine(Start.GetHashCode(), End.GetHashCode()); +#else + //https://github.com/dotnet/runtime/blob/v9.0.0/src/libraries/System.Private.CoreLib/src/System/Numerics/Hashing/HashHelpers.cs + var h1 = Start.GetHashCode(); + var h2 = End.GetHashCode(); + // RyuJIT optimizes this to use the ROL instruction + // Related GitHub pull request: https://github.com/dotnet/coreclr/pull/1830 + uint rol5 = ((uint)h1 << 5) | ((uint)h1 >> 27); + return ((int)rol5 + h1) ^ h2; +#endif + } + + /// Converts the value of the current Range object to its equivalent string representation. + public override string ToString() + { +#if (!NETSTANDARD2_0 && !NETFRAMEWORK) + Span span = stackalloc char[2 + (2 * 11)]; // 2 for "..", then for each index 1 for '^' and 10 for longest possible uint + int pos = 0; + + if (Start.IsFromEnd) + { + span[0] = '^'; + pos = 1; + } + bool formatted = ((uint)Start.Value).TryFormat(span.Slice(pos), out int charsWritten); + Debug.Assert(formatted); + pos += charsWritten; + + span[pos++] = '.'; + span[pos++] = '.'; + + if (End.IsFromEnd) + { + span[pos++] = '^'; + } + formatted = ((uint)End.Value).TryFormat(span.Slice(pos), out charsWritten); + Debug.Assert(formatted); + pos += charsWritten; + + return new string(span.Slice(0, pos)); +#else + return Start.ToString() + ".." + End.ToString(); +#endif + } + + /// Create a Range object starting from start index to the end of the collection. + public static Range StartAt(Index start) => new Range(start, Index.End); + + /// Create a Range object starting from first element in the collection to the end Index. + public static Range EndAt(Index end) => new Range(Index.Start, end); + + /// Create a Range object starting from first element to the end. + public static Range All => new Range(Index.Start, Index.End); + + /// Calculate the start offset and length of range object using a collection length. + /// The length of the collection that the range will be used with. length has to be a positive value. + /// + /// For performance reason, we don't validate the input length parameter against negative values. + /// It is expected Range will be used with collections which always have non negative length/count. + /// We validate the range is inside the length scope though. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public (int Offset, int Length) GetOffsetAndLength(int length) + { + int start = Start.GetOffset(length); + int end = End.GetOffset(length); + + if ((uint)end > (uint)length || (uint)start > (uint)end) + { + ThrowArgumentOutOfRangeException(); + } + + return (start, end - start); + } + + private static void ThrowArgumentOutOfRangeException() + { + throw new ArgumentOutOfRangeException("length"); + } + } +} + +#endif \ No newline at end of file diff --git a/src/ByteAether.Ulid2/CryptographicallySecureRandomProvider.cs b/src/ByteAether.Ulid2/CryptographicallySecureRandomProvider.cs new file mode 100644 index 0000000..908d692 --- /dev/null +++ b/src/ByteAether.Ulid2/CryptographicallySecureRandomProvider.cs @@ -0,0 +1,22 @@ +using System.Runtime.CompilerServices; +using System.Security.Cryptography; + +namespace ByteAether.Ulid2; + +/// +/// Provides cryptographically secure random number generation functionality.
+/// Implements the interface to generate random bytes +/// securely using a system-provided implementation of the . +///
+public readonly struct CryptographicallySecureRandomProvider : IRandomProvider +{ + private static readonly RandomNumberGenerator _rng = RandomNumberGenerator.Create(); + + /// +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public void GetBytes(Span buffer) => _rng.GetBytes(buffer); +} \ No newline at end of file diff --git a/src/ByteAether.Ulid2/IRandomProvider.cs b/src/ByteAether.Ulid2/IRandomProvider.cs new file mode 100644 index 0000000..584ba2e --- /dev/null +++ b/src/ByteAether.Ulid2/IRandomProvider.cs @@ -0,0 +1,18 @@ +namespace ByteAether.Ulid2; + +/// +/// Implementations of this interface dictate the method by which randomness +/// is produced. +/// +public interface IRandomProvider +{ + /// + /// Fills the specified buffer with random byte values.
+ /// The implementation determines the source of randomness. + ///
+ /// + /// The span of bytes to be filled with random data. The length of + /// the buffer determines how many bytes are generated. + /// + public void GetBytes(Span buffer); +} \ No newline at end of file diff --git a/src/ByteAether.Ulid2/LowLatencyLock.cs b/src/ByteAether.Ulid2/LowLatencyLock.cs new file mode 100644 index 0000000..21da602 --- /dev/null +++ b/src/ByteAether.Ulid2/LowLatencyLock.cs @@ -0,0 +1,123 @@ +using System.Runtime.CompilerServices; +#if !NET7_0_OR_GREATER +using System.Runtime.InteropServices; +#endif + +namespace ByteAether.Ulid2; + +internal struct LowLatencyLock +{ + internal int LockState; + + internal readonly ref struct Scope + { +#if NET7_0_OR_GREATER + private readonly ref int _lockState; + + [SkipLocalsInit] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + internal Scope(ref LowLatencyLock @lock) + { + _lockState = ref @lock.LockState; + } + + [SkipLocalsInit] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public void Dispose() + { + Volatile.Write(ref _lockState, 0); + } +#else + // This is fully supported in .NET Standard 2.0 / .NET 5.0. + private readonly Span _lockState; + +#if NET5_0_OR_GREATER + [SkipLocalsInit] +#endif +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + internal Scope(ref LowLatencyLock @lock) + { + _lockState = +#if NETCOREAPP + MemoryMarshal.CreateSpan(ref @lock.LockState, 1); +#else + Compatibility.MemoryMarshal.CreateSpan(ref @lock.LockState, 1); +#endif + ; + } + +#if NET5_0_OR_GREATER + [SkipLocalsInit] +#endif +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public void Dispose() + { + Volatile.Write(ref MemoryMarshal.GetReference(_lockState), 0); + } +#endif + + } +} + +internal static class LowLatencyLockExtensions +{ + private const int _spinWaitAttemptCount = 8; + private const int _spinWaitIterationCount = 8; + +#if NET5_0_OR_GREATER + [SkipLocalsInit] +#endif +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public static LowLatencyLock.Scope Enter(ref this LowLatencyLock @lock) + { + if (Interlocked.CompareExchange(ref @lock.LockState, 1, 0) != 0) + { + ContendedEnter(ref @lock); + } + + return new(ref @lock); + } + +#if NET5_0_OR_GREATER + [SkipLocalsInit] +#endif +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.NoInlining)] +#endif + private static void ContendedEnter(ref LowLatencyLock @lock) + { + // First we do Thread.SpinWait() for limited amount of times + var remainingAttempts = _spinWaitAttemptCount; + do + { + Thread.SpinWait(_spinWaitIterationCount); + + // Test-and-Test-and-Set (TTAS) + // Initial read-only check keeps the cache line in the "Shared" state + if (@lock.LockState == 0 && Interlocked.CompareExchange(ref @lock.LockState, 1, 0) == 0) + { + return; + } + } while (--remainingAttempts > 0); + + // Later we do Thread.Yield() + do + { + Thread.Yield(); + } while (@lock.LockState != 0 || Interlocked.CompareExchange(ref @lock.LockState, 1, 0) != 0); + } +} \ No newline at end of file diff --git a/src/ByteAether.Ulid2/PACKAGE.md b/src/ByteAether.Ulid2/PACKAGE.md new file mode 100644 index 0000000..0070c8e --- /dev/null +++ b/src/ByteAether.Ulid2/PACKAGE.md @@ -0,0 +1,205 @@ +# ULID +*from ByteAether* + +[![License](https://img.shields.io/github/license/ByteAether/Ulid?logo=github&label=License)](https://github.com/ByteAether/Ulid/blob/main/LICENSE) +[![NuGet Version](https://img.shields.io/nuget/v/ByteAether.Ulid?logo=nuget&label=Version)](https://www.nuget.org/packages/ByteAether.Ulid/) +[![NuGet Downloads](https://img.shields.io/nuget/dt/ByteAether.Ulid?logo=nuget&label=Downloads)](https://www.nuget.org/packages/ByteAether.Ulid/) +[![GitHub Build Status](https://img.shields.io/github/actions/workflow/status/ByteAether/Ulid/build-and-test.yml?logo=github&label=Build%20%26%20Test)](https://github.com/ByteAether/Ulid/actions/workflows/build-and-test.yml) +[![GitHub Security](https://img.shields.io/github/actions/workflow/status/ByteAether/Ulid/codeql.yml?logo=github&label=Security%20Validation)](https://github.com/ByteAether/Ulid/actions/workflows/codeql.yml) + +A high-performance, fully compliant .NET implementation of ULIDs (Universally Unique Lexicographically Sortable Identifiers), adhering to the [official ULID specification](https://github.com/ulid/spec). + +For more detailed documentation, visit our [GitHub repository](https://github.com/ByteAether/Ulid). + +## Features + +![.NET AOT Ready](https://img.shields.io/badge/.NET-AOT_Ready-blue) +![.NET 10.0](https://img.shields.io/badge/.NET-10.0-brightgreen) +![.NET 9.0](https://img.shields.io/badge/.NET-9.0-brightgreen) +![.NET 8.0](https://img.shields.io/badge/.NET-8.0-brightgreen) +![.NET 7.0](https://img.shields.io/badge/.NET-7.0-green) +![.NET 6.0](https://img.shields.io/badge/.NET-6.0-green) +![.NET 5.0](https://img.shields.io/badge/.NET-5.0-yellow) +![.NET Standard 2.1](https://img.shields.io/badge/.NET-Standard_2.1-yellow) +![.NET Standard 2.0](https://img.shields.io/badge/.NET-Standard_2.0-green) + +- **Universally Unique**: Ensures global uniqueness across systems. +- **Sortable**: Lexicographically ordered for time-based sorting. +- **Lock-Free Synchronization**: Monotonic generation utilizes a high-performance, **lock-free compare-and-exchange (CAS)** approach. +- **Specification-Compliant**: Fully adheres to the ULID specification. +- **Interoperable**: Includes conversion methods to and from GUIDs, [Crockford's Base32](https://www.crockford.com/base32.html) strings, and byte arrays. +- **Ahead-of-Time (AoT) Compilation Compatible**: Fully compatible with AoT compilation for improved startup performance and smaller binary sizes. +- **Error-Free Generation**: Prevents `OverflowException` by incrementing the timestamp component when the random part overflows, ensuring continuous unique ULID generation. + +## Installation + +Install the latest stable package via NuGet: +```sh +dotnet add package ByteAether.Ulid +``` +To install a specific [preview version](https://www.nuget.org/packages/ByteAether.Ulid/absoluteLatest), use the `--version` option: +```sh +dotnet add package ByteAether.Ulid --version +``` + +## Usage + +Here is a basic example of how to use the ULID implementation: +```csharp +using System; +using ByteAether.Ulid; + +// Create a new ULID +var ulid = Ulid.New(); + +// Convert to byte array and back +byte[] byteArray = ulid.ToByteArray(); +var ulidFromByteArray = Ulid.New(byteArray); + +// Convert to GUID and back +Guid guid = ulid.ToGuid(); +var ulidFromGuid = Ulid.New(guid); + +// Convert to string and back +string ulidString = ulid.ToString(); +var ulidFromString = Ulid.Parse(ulidString); + +Console.WriteLine($"ULID: {ulid}, GUID: {guid}, String: {ulidString}"); +``` + +## API + +The `Ulid` implementation provides the following properties and methods: + +### Creation + +- `Ulid.New(GenerationOptions? options = null)`\ + Generates a new ULID using default generation options. Accepts an optional `GenerationOptions` parameter to customize the generation behavior. +- `Ulid.New(DateTimeOffset dateTimeOffset, GenerationOptions? options = null)`\ + Generates a new ULID using the specified `DateTimeOffset` and default generation options. Accepts an optional `GenerationOptions` parameter to customize the generation behavior. +- `Ulid.New(long timestamp, GenerationOptions? options = null)`\ + Generates a new ULID using the specified Unix timestamp in milliseconds (`long`) and default generation options. Accepts an optional `GenerationOptions` parameter to customize the generation behavior. +- `Ulid.New(DateTimeOffset dateTimeOffset, ReadOnlySpan random)`\ + Generates a new ULID using the specified `DateTimeOffset` and a pre-existing random byte array. +- `Ulid.New(long timestamp, ReadOnlySpan random)`\ + Generates a new ULID using the specified Unix timestamp in milliseconds (`long`) and a pre-existing random byte array. +- `Ulid.New(ReadOnlySpan bytes)`\ + Creates a ULID from an existing byte array. +- `Ulid.New(Guid guid)`\ + Create from existing `Guid`. +- `Ulid.MinAt(DateTimeOffset datetime)`\ + Creates the minimum possible ULID value for the specified `DateTimeOffset`. +- `Ulid.MinAt(long timestamp)`\ + Creates the minimum possible ULID value for the specified Unix timestamp in milliseconds (`long`). +- `Ulid.MaxAt(DateTimeOffset datetime)`\ + Creates the maximum possible ULID value for the specified `DateTimeOffset`. +- `Ulid.MaxAt(long timestamp)`\ + Creates the maximum possible ULID value for the specified Unix timestamp in milliseconds (`long`). + +### Checking Validity + +- `Ulid.IsValid(string ulidString)`\ + Validates if the given string is a valid ULID. +- `Ulid.IsValid(ReadOnlySpan ulidString)`\ + Validates if the given span of characters is a valid ULID. +- `Ulid.IsValid(ReadOnlySpan ulidBytes)`\ + Validates if the given byte array represents a valid ULID. + +### Parsing + +- `Ulid.Parse(ReadOnlySpan chars, IFormatProvider? provider = null)`\ + Parses a ULID from a character span in canonical format. The `IFormatProvider` is ignored. +- `Ulid.TryParse(ReadOnlySpan s, IFormatProvider? provider, out Ulid result)`\ + Tries to parse a ULID from a character span in canonical format. Returns `true` if successful. +- `Ulid.Parse(string s, IFormatProvider? provider = null)`\ + Parses a ULID from a string in canonical format. The `IFormatProvider` is ignored. +- `Ulid.TryParse(string? s, IFormatProvider? provider, out Ulid result)`\ + Tries to parse a ULID from a string in canonical format. Returns `true` if successful. + +### Properties + +- `Ulid.MinValue`\ + Represents an empty ULID, equivalent to `default(Ulid)` and `Ulid.New(new byte[16])`. +- `Ulid.MaxValue`\ + Represents the maximum possible value for a ULID (all bytes set to `0xFF`). +- `Ulid.Empty`\ + Alias for `Ulid.MinValue`. +- `Ulid.DefaultGenerationOptions`\ + Default configuration for ULID generation when no options are provided by the `Ulid.New(...)` call. +- `.Time`\ + Gets the timestamp component of the ULID as a `DateTimeOffset`. +- `.TimeBytes`\ + Gets the time component of the ULID as a `ReadOnlySpan`. +- `.Random`\ + Gets the random component of the ULID as a `ReadOnlySpan`. + +### Conversions & Interoperability + +- `.AsByteSpan()`\ + Provides a `ReadOnlySpan` representing the contents of the ULID. +- `.ToByteArray()`\ + Converts the ULID to a byte array. +- `.ToGuid()`\ + Converts the ULID to a `Guid`. +- `.ToString(string? format = null, IFormatProvider? formatProvider = null)`\ + Converts the ULID to a canonical string representation. Format arguments are ignored. +- Provides implicit operators to and from `Guid` and `string`. + +### Comparison Operators & .NET Interfaces + +- Supports all comparison operators:\ + `==`, `!=`, `<`, `<=`, `>`, `>=`. +- Implements standard comparison and equality methods:\ + `CompareTo`, `Equals`, `GetHashCode`. +- Implements the following .NET standard interfaces:\ + `IMinMaxValue`, `IEquatable`, `IIEqualityComparer`, `IComparable`, `IComparable`, `IComparisonOperators`, `IFormattable`, `IParsable`, `ISpanFormattable`, `ISpanParsable`, `IUtf8SpanFormattable`, `IUtf8SpanParsable`. + +### GenerationOptions + +The `GenerationOptions` class provides detailed configuration for ULID generation, with the following key properties: + +- `Monotonicity`\ + Controls the behavior of ULID generation when multiple identifiers are created within the same millisecond. It determines whether ULIDs are strictly increasing or allow for random ordering within that millisecond. Available options include: `NonMonotonic`, `MonotonicIncrement` (default), `MonotonicRandom1Byte`, `MonotonicRandom2Byte`, `MonotonicRandom3Byte`, `MonotonicRandom4Byte`. + +- `InitialRandomSource`\ + An `IRandomProvider` for generating the random bytes of a ULID. The default `CryptographicallySecureRandomProvider` ensures robust, unpredictable ULIDs using a cryptographically secure generator. + +- `IncrementRandomSource`\ + An `IRandomProvider` that provides randomness for monotonic random increments. The default `PseudoRandomProvider` is a faster, non-cryptographically secure source optimized for this specific purpose. + +This library comes with two default `IRandomProvider` implementations: + +- **`CryptographicallySecureRandomProvider`**\ + Utilizes `System.Security.Cryptography.RandomNumberGenerator` for high-quality, cryptographically secure random data. +- **`PseudoRandomProvider`**\ + A faster, non-cryptographically secure option based on `System.Random`, ideal for performance-critical scenarios where cryptographic security is not required for random increments. + +Custom `IRandomProvider` implementations can also be created. + +## Integration with Other Libraries + +### ASP.NET Core + +Supports seamless integration as a route or query parameter with built-in `TypeConverter`. + +### System.Text.Json + +Includes a `JsonConverter` for easy serialization and deserialization. + +### Other Libraries + +Check out [README in GitHub repository](https://github.com/ByteAether/Ulid/blob/main/README.md) for examples to integrate with Entity Framework Core, Dapper, MessagePack, and Newtonsoft.Json. + +## Prior Art + +Much of this implementation is either based on or inspired by existing works. This library is standing on the shoulders of giants. + + * [NetUlid](https://github.com/ultimicro/netulid) + * [Ulid](https://github.com/Cysharp/Ulid) + * [NUlid](https://github.com/RobThree/NUlid) + * [Official ULID specification](https://github.com/ulid/spec) + * [Crockford's Base32](https://www.crockford.com/base32.html) + +## License + +This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. diff --git a/src/ByteAether.Ulid2/PseudoRandomProvider.cs b/src/ByteAether.Ulid2/PseudoRandomProvider.cs new file mode 100644 index 0000000..0ab114d --- /dev/null +++ b/src/ByteAether.Ulid2/PseudoRandomProvider.cs @@ -0,0 +1,33 @@ +using System.Runtime.CompilerServices; + +namespace ByteAether.Ulid2; + +/// +/// Provides a pseudo-random number generator implementation for the +/// interface using the shared instance +/// of . +/// +public readonly struct PseudoRandomProvider : IRandomProvider +{ +#if NET6_0_OR_GREATER + private static Random _rng + { +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + get => Random.Shared; + } +#else + private static readonly Random _rng = new(); +#endif + + /// +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public void GetBytes(Span buffer) => _rng.NextBytes(buffer); +} \ No newline at end of file diff --git a/src/ByteAether.Ulid2/Ulid.Boundaries.cs b/src/ByteAether.Ulid2/Ulid.Boundaries.cs new file mode 100644 index 0000000..c4b0773 --- /dev/null +++ b/src/ByteAether.Ulid2/Ulid.Boundaries.cs @@ -0,0 +1,68 @@ +#if NET7_0_OR_GREATER +using System.Numerics; +#endif + +namespace ByteAether.Ulid2; + +public readonly partial struct Ulid +#if NET7_0_OR_GREATER + : IMinMaxValue +#endif +{ + private static readonly byte[] _randomMin = Enumerable.Repeat((byte)0x00, _ulidSizeRandom).ToArray(); + private static readonly byte[] _randomMax = Enumerable.Repeat((byte)0xFF, _ulidSizeRandom).ToArray(); + + /// Gets the minimum value of the ULID type. + /// + /// The field is a ULID with all components set to zero. + /// It can be used as a default or placeholder value. + /// + public static Ulid MinValue { get; } = default; + + /// + /// Represents the maximum possible value for a ULID. + /// + /// + /// The field is a ULID where all byte components are set to their highest possible value (0xFF). + /// It can be used as a sentinel or boundary value in comparison operations or range validations. + /// + public static Ulid MaxValue { get; } = New(Enumerable.Repeat((byte)0xFF, _ulidSize).ToArray()); + + /// + /// Represents an empty ULID value. + /// + /// + /// The field is a ULID with all components set to zero. + /// It can be used as a default or placeholder value. + /// It is equivalent to , but is provided for clarity. + /// + public static Ulid Empty => MinValue; + + /// + /// Creates the minimum possible value for the specified timestamp. + /// + /// The timestamp used to create the minimum value. + /// The minimum value for the given timestamp. + public static Ulid MinAt(long timestamp) => New(timestamp, _randomMin); + + /// + /// Creates the minimum possible value for the specified timestamp. + /// + /// The timestamp used to create the minimum value. + /// The minimum value for the given timestamp. + public static Ulid MinAt(DateTimeOffset datetime) => New(datetime, _randomMin); + + /// + /// Creates the maximum possible value for the specified timestamp. + /// + /// The timestamp used to create the maximum value. + /// The maximum value for the given timestamp. + public static Ulid MaxAt(long timestamp) => New(timestamp, _randomMax); + + /// + /// Creates the maximum possible value for the specified timestamp. + /// + /// The timestamp used to create the maximum value. + /// The maximum value for the given timestamp. + public static Ulid MaxAt(DateTimeOffset datetime) => New(datetime, _randomMax); +} \ No newline at end of file diff --git a/src/ByteAether.Ulid2/Ulid.Comparable.cs b/src/ByteAether.Ulid2/Ulid.Comparable.cs new file mode 100644 index 0000000..fd31fac --- /dev/null +++ b/src/ByteAether.Ulid2/Ulid.Comparable.cs @@ -0,0 +1,125 @@ +using System.Buffers.Binary; +using System.Runtime.CompilerServices; +#if NET7_0_OR_GREATER +using System.Numerics; +#endif + +namespace ByteAether.Ulid2; +public readonly partial struct Ulid : IComparable, IComparable +#if NET7_0_OR_GREATER + , IComparisonOperators +#endif +{ + /// + /// Determines whether the value of the left ULID is less than the value of the right ULID. + /// + /// The first ULID to compare. + /// The second ULID to compare. + /// True if the value of the left ULID is less than the value of the right ULID; otherwise, false. +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public static bool operator <(Ulid left, Ulid right) + => left.CompareTo(right) < 0; + + /// + /// Determines whether the value of the left ULID is less than or equal to the value of the right ULID. + /// + /// The first ULID to compare. + /// The second ULID to compare. + /// True if the value of the left ULID is less than or equal to the value of the right ULID; otherwise, false. +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public static bool operator <=(Ulid left, Ulid right) + => left.CompareTo(right) <= 0; + + /// + /// Determines whether the value of the left ULID is greater than the value of the right ULID. + /// + /// The first ULID to compare. + /// The second ULID to compare. + /// True if the value of the left ULID is greater than the value of the right ULID; otherwise, false. +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public static bool operator >(Ulid left, Ulid right) + => left.CompareTo(right) > 0; + + /// + /// Determines whether the value of the left ULID is greater than or equal to the value of the right ULID. + /// + /// The first ULID to compare. + /// The second ULID to compare. + /// True if the value of the left ULID is greater than or equal to the value of the right ULID; otherwise, false. +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public static bool operator >=(Ulid left, Ulid right) + => left.CompareTo(right) >= 0; + + /// +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public int CompareTo(object? obj) + { + if (obj == null) + { + return 1; + } + + if (obj is not Ulid ulid) + { + throw new ArgumentException($"The value is not an instance of {nameof(Ulid)}.", nameof(obj)); + } + + return CompareTo(ulid); + } + + /// +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public int CompareTo(Ulid other) + => CompareToCore(this, other); + +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveOptimization)] +#endif + private static int CompareToCore(in Ulid left, in Ulid right) + { + ref var rA = ref Unsafe.As(ref Unsafe.AsRef(in left)); + ref var rB = ref Unsafe.As(ref Unsafe.AsRef(in right)); + + var a = BinaryPrimitives.ReverseEndianness(rA); + var b = BinaryPrimitives.ReverseEndianness(rB); + + if (a != b) + { + return a < b ? -1 : 1; + } + + a = BinaryPrimitives.ReverseEndianness(Unsafe.Add(ref rA, 1)); + b = BinaryPrimitives.ReverseEndianness(Unsafe.Add(ref rB, 1)); + + if (a != b) + { + return a < b ? -1 : 1; + } + + return 0; + } +} \ No newline at end of file diff --git a/src/ByteAether.Ulid2/Ulid.Equatable.cs b/src/ByteAether.Ulid2/Ulid.Equatable.cs new file mode 100644 index 0000000..523d96a --- /dev/null +++ b/src/ByteAether.Ulid2/Ulid.Equatable.cs @@ -0,0 +1,125 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +#if NET7_0_OR_GREATER +using System.Numerics; +using System.Runtime.Intrinsics; +#elif NETCOREAPP3_0_OR_GREATER +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +#endif + +namespace ByteAether.Ulid2; + +public readonly partial struct Ulid : IEquatable, IEqualityComparer +#if NET7_0_OR_GREATER + , IEqualityOperators // Keeping this here for clarity +#endif +{ + /// +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public int GetHashCode(Ulid ulid) => ulid.GetHashCode(); + + /// +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public bool Equals(Ulid x, Ulid y) => EqualsCore(x, y); + + /// +#if NET5_0_OR_GREATER + [SkipLocalsInit] +#endif +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public override int GetHashCode() + { + ref var rA = ref Unsafe.As(ref Unsafe.AsRef(in this)); + return rA ^ Unsafe.Add(ref rA, 1) ^ Unsafe.Add(ref rA, 2) ^ Unsafe.Add(ref rA, 3); + } + + /// +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public bool Equals(Ulid other) + => EqualsCore(this, other); + + /// +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public override bool Equals([NotNullWhen(true)] object? obj) + => obj is Ulid ulid && EqualsCore(this, ulid); + + /// + /// Determines whether two specified ULIDs have the same value. + /// + /// The first ULID to compare. + /// The second ULID to compare. + /// True if the value of the left ULID is equal to the value of the right ULID; otherwise, false. + +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public static bool operator ==(Ulid left, Ulid right) + => EqualsCore(left, right); + + /// + /// Determines whether two specified ULIDs have different values. + /// + /// The first ULID to compare. + /// The second ULID to compare. + /// True if the value of the left ULID is not equal to the value of the right ULID; otherwise, false. + public static bool operator !=(Ulid left, Ulid right) + => !EqualsCore(left, right); + +#if NET5_0_OR_GREATER + [SkipLocalsInit] +#endif +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveOptimization)] +#endif + private static bool EqualsCore(in Ulid left, in Ulid right) + { +#if NET7_0_OR_GREATER + if (Vector128.IsHardwareAccelerated) + { + var vA = Vector128.LoadUnsafe(ref Unsafe.As(ref Unsafe.AsRef(in left))); + var vB = Vector128.LoadUnsafe(ref Unsafe.As(ref Unsafe.AsRef(in right))); + return vA == vB; + } +#elif NETCOREAPP3_0_OR_GREATER + if (Sse2.IsSupported) + { + var vA = Unsafe.ReadUnaligned>(ref Unsafe.As(ref Unsafe.AsRef(in left))); + var vB = Unsafe.ReadUnaligned>(ref Unsafe.As(ref Unsafe.AsRef(in right))); + return Sse2.MoveMask(Sse2.CompareEqual(vA, vB)) == 0xFFFF; + } +#endif + + ref var rA = ref Unsafe.As(ref Unsafe.AsRef(in left)); + ref var rB = ref Unsafe.As(ref Unsafe.AsRef(in right)); + + // XOR-compare instead of 2x 64bit long compare with AND + // Branchless XOR-compare is faster (0.1787ns vs. 0.2463ns) + var xor0 = rA ^ rB; + var xor1 = Unsafe.Add(ref rA, 1) ^ Unsafe.Add(ref rB, 1); + + return (xor0 | xor1) == 0; + } +} \ No newline at end of file diff --git a/src/ByteAether.Ulid2/Ulid.Guid.cs b/src/ByteAether.Ulid2/Ulid.Guid.cs new file mode 100644 index 0000000..bbb8cbe --- /dev/null +++ b/src/ByteAether.Ulid2/Ulid.Guid.cs @@ -0,0 +1,134 @@ +using System.Buffers.Binary; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +#if NETCOREAPP +using System.Runtime.Intrinsics; +#if !NET7_0_OR_GREATER +using System.Runtime.Intrinsics.X86; +#endif +#endif + +namespace ByteAether.Ulid2; + +#if NET8_0_OR_GREATER +// We need to target netstandard2.1, so keep using ref for MemoryMarshal.Write +// CS9191: The 'ref' modifier for argument 2 corresponding to the 'in' parameter is equivalent to 'in'. Consider using 'in' instead. +#pragma warning disable CS9191 +#endif + +public readonly partial struct Ulid +{ + /// + /// Creates a new ULID using the specified GUID. + /// + /// The GUID to initialize the ULID with. +#if NET5_0_OR_GREATER + [SkipLocalsInit] +#endif +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public static Ulid New(Guid guid) + => BitConverter.IsLittleEndian + ? Shuffle(ref guid) + : Unsafe.As(ref guid); + + /// + /// Converts the ULID to a GUID. + /// + /// A GUID representing the ULID. +#if NET5_0_OR_GREATER + [SkipLocalsInit] +#endif +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public Guid ToGuid() + => BitConverter.IsLittleEndian + ? Shuffle(ref Unsafe.AsRef(in this)) + : Unsafe.As(ref Unsafe.AsRef(in this)); + + /// + /// Implicitly converts a ULID to a GUID. + /// + /// The ULID to convert. + /// A GUID representing the ULID. +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public static implicit operator Guid(Ulid ulid) => ulid.ToGuid(); + + /// + /// Implicitly converts a GUID to a ULID. + /// + /// The GUID to convert. + /// A ULID representing the GUID. +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public static implicit operator Ulid(Guid guid) => New(guid); + +#if NETCOREAPP + private static readonly Vector128 _shuffleMask + = Vector128.Create((byte)3, 2, 1, 0, 5, 4, 7, 6, 8, 9, 10, 11, 12, 13, 14, 15); +#endif + + // HACK: We assume the layout of a Guid is the following: + // Int32, Int16, Int16, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8 + // Source: https://github.com/dotnet/runtime/blob/5c4686f831d34c2c127e943d0f0d144793eeb0ad/src/libraries/System.Private.CoreLib/src/System/Guid.cs + // More info: https://stackoverflow.com/questions/10190817/guid-byte-order-in-net/10191075#10191075 +#if NET5_0_OR_GREATER + [SkipLocalsInit] +#endif +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + private static TOut Shuffle(ref TIn bytes) + { +#if NET7_0_OR_GREATER + if (Vector128.IsHardwareAccelerated) + { + var vector = Unsafe.As>(ref bytes); + vector = Vector128.Shuffle(vector, _shuffleMask); + return Unsafe.As, TOut>(ref vector); + } +#elif NETCOREAPP3_0_OR_GREATER + if (Ssse3.IsSupported) + { + var vector = Unsafe.As>(ref bytes); + vector = Ssse3.Shuffle(vector, _shuffleMask); + return Unsafe.As, TOut>(ref vector); + } +#endif + + // |A|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P| + // |D|C|B|A|... + // ...|F|E|H|G|... + // ...|I|J|K|L|M|N|O|P| + Span result = new byte[_ulidSize]; + + ref var ptr = ref Unsafe.As(ref bytes); + var lower = BinaryPrimitives.ReverseEndianness(ptr); + + ptr = ref Unsafe.Add(ref ptr, 1); + var upper = ((ptr & 0x00_FF_00_FF) << 8) | ((ptr & 0xFF_00_FF_00) >> 8); + + ref var upperBytes = ref Unsafe.As(ref Unsafe.Add(ref ptr, 1)); + + MemoryMarshal.Write(result, ref lower); + MemoryMarshal.Write(result[4..], ref upper); + MemoryMarshal.Write(result[8..], ref upperBytes); + + return Unsafe.As(ref result.GetPinnableReference()); + } +} \ No newline at end of file diff --git a/src/ByteAether.Ulid2/Ulid.IsValid.cs b/src/ByteAether.Ulid2/Ulid.IsValid.cs new file mode 100644 index 0000000..af67298 --- /dev/null +++ b/src/ByteAether.Ulid2/Ulid.IsValid.cs @@ -0,0 +1,128 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace ByteAether.Ulid2; + +public readonly partial struct Ulid +{ + /// + /// Validates if the given string is a valid ULID. + /// + /// The ULID string to validate. + /// + /// true if the string is a valid ULID, false otherwise. + /// +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public static bool IsValid(string ulidString) => IsValid(ulidString.AsSpan()); + + /// + /// Validates if the given span of characters is a valid ULID. + /// + /// The ULID character span to validate. + /// + /// true if the character span is a valid ULID, false otherwise. + /// +#if NET5_0_OR_GREATER + [SkipLocalsInit] +#endif +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveOptimization)] +#endif + public static unsafe bool IsValid(ReadOnlySpan ulidString) + { + if (ulidString.Length != UlidStringLength) // 26 + { + return false; + } + + fixed (char* src = &MemoryMarshal.GetReference(ulidString)) + { + // 1. Fast check for the first character (prevent 128-bit overflow) + uint c0 = src[0]; + if (c0 > 255 || _inverseBase32[c0] > 7) + { + return false; + } + + fixed (byte* table = _inverseBase32) + { + // Block A: Indices 1 to 6 + if (((src[1] | src[2] | src[3] | src[4] | src[5] | src[6]) & 0xFF00) != 0) + { + return false; + } + + if ( + table[src[1]] == 255 || table[src[2]] == 255 || table[src[3]] == 255 + || table[src[4]] == 255 || table[src[5]] == 255 || table[src[6]] == 255 + ) + { + return false; + } + + // Block B: Indices 7 to 12 + if (((src[7] | src[8] | src[9] | src[10] | src[11] | src[12]) & 0xFF00) != 0) + { + return false; + } + + if ( + table[src[7]] == 255 || table[src[8]] == 255 || table[src[9]] == 255 + || table[src[10]] == 255 || table[src[11]] == 255 || table[src[12]] == 255 + ) + { + return false; + } + + // Block C: Indices 13 to 18 + if (((src[13] | src[14] | src[15] | src[16] | src[17] | src[18]) & 0xFF00) != 0) + { + return false; + } + + if ( + table[src[13]] == 255 || table[src[14]] == 255 || table[src[15]] == 255 + || table[src[16]] == 255 || table[src[17]] == 255 || table[src[18]] == 255 + ) + { + return false; + } + + // Block D: Indices 19 to 25 + if (((src[19] | src[20] | src[21] | src[22] | src[23] | src[24] | src[25]) & 0xFF00) != 0) + { + return false; + } + + if ( + table[src[19]] == 255 || table[src[20]] == 255 || table[src[21]] == 255 + || table[src[22]] == 255 || table[src[23]] == 255 || table[src[24]] == 255 + || table[src[25]] == 255 + ) + { + return false; + } + } + } + + return true; + } + + /// + /// Validates if the given byte array represents a valid ULID. + /// + /// The byte array to validate. + /// + /// true if the byte array is a valid ULID, false otherwise. + /// +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public static bool IsValid(ReadOnlySpan ulidBytes) => ulidBytes.Length == _ulidSize; +} \ No newline at end of file diff --git a/src/ByteAether.Ulid2/Ulid.New.GenerationOptions.cs b/src/ByteAether.Ulid2/Ulid.New.GenerationOptions.cs new file mode 100644 index 0000000..5a5a7a4 --- /dev/null +++ b/src/ByteAether.Ulid2/Ulid.New.GenerationOptions.cs @@ -0,0 +1,142 @@ +namespace ByteAether.Ulid2; + +public readonly partial struct Ulid +{ + /// + /// Configuration options for ULID generation. + /// + public record GenerationOptions + { + /// + /// Monotonicity behavior for ULID generation. + /// + /// + /// The enum provides various options to configure + /// the generation of ULIDs with respect to their monotonic properties.
+ /// These options determine how the ULID sequence behaves in scenarios + /// where time does not progress or progresses non-linearly. + ///
+ public enum MonotonicityOptions + { + /// + /// ULIDs are generated in a completely non-monotonic manner. + /// + /// + /// When is used, ULIDs are created + /// without any monotonic guarantees. The random component of the ULID is + /// entirely random, and the sequence does not ensure order or incrementality. + /// + NonMonotonic = -1, + + /// + /// ULIDs are generated with a strictly monotonic increment in the random component. + /// + /// + /// When is used, the random portion of the ULID is + /// adjusted to ensure strict monotonic progression. This guarantees that the sequence of generated + /// ULIDs is always ordered and incremental, making it suitable for scenarios where strict ordering + /// is required without introducing additional randomness.
+ /// This is the default behavior when monotonicity settings are not explicitly defined. + ///
+ MonotonicIncrement = 0, + + /// + /// Generates ULIDs with monotonicity by adding a random 8-bit value to the existing random component. + /// + /// + /// When is used, a random value between 1 and 256 + /// is added to the existing random component. This addition may cause carries across all bytes of + /// the random component, ensuring the resulting ULID is greater than the previous one while + /// maintaining a degree of randomness in the increment. + /// + MonotonicRandom1Byte = 1, + + /// + /// Generates ULIDs with monotonicity by adding a random 16-bit value to the existing random component. + /// + /// + /// When is used, a random value between 1 and 65 536 + /// is added to the existing random component. This addition may cause carries across all bytes of + /// the random component, ensuring the resulting ULID is greater than the previous one while + /// providing a larger range of possible increments. + /// + MonotonicRandom2Byte = 2, + + /// + /// Generates ULIDs with monotonicity by adding a random 24-bit value to the existing random component. + /// + /// + /// When is used, a random value between 1 and 16 777 216 + /// is added to the existing random component. This addition may cause carries across all bytes of + /// the random component, ensuring the resulting ULID is greater than the previous one while + /// providing a significantly larger range of possible increments. + /// + MonotonicRandom3Byte = 3, + + /// + /// Generates ULIDs with monotonicity by adding a random 32-bit value to the existing random component. + /// + /// + /// When is used, a random value between 1 and 4 294 967 296 + /// is added to the existing random component. This addition may cause carries across all bytes of + /// the random component, ensuring the resulting ULID is greater than the previous one while + /// providing the maximum range of possible increments. + /// + MonotonicRandom4Byte = 4, + } + + /// + /// Monotonicity behavior for ULID generation. + /// + /// + /// This property determines how the timestamp and randomness components + /// will behave in regard to ordering and predictability:
+ /// - NonMonotonic: No monotonic guarantees, Random part will be fully randomized.
+ /// - MonotonicIncrement: Guarantees monotonic ordering by incrementing + /// the previous random by one if the same timestamp is generated consecutively.
+ /// - MonotonicRandom1Byte to MonotonicRandom4Byte: Ensures monotonicity by introducing a + /// randomized 1 to 4 bytes value as an increment to the previous Random part when timestamps are identical. + ///
+ /// + /// A value of the enum that specifies the monotonicity behavior. + /// Defaults to . + /// + public MonotonicityOptions Monotonicity + { + get; + init => field = Enum.IsDefined(typeof(MonotonicityOptions), value) + ? value + : throw new ArgumentOutOfRangeException(nameof(Monotonicity), value, "Invalid monotonicity option."); + } = MonotonicityOptions.MonotonicIncrement; + + /// + /// Initial random source used for ULID generation. + /// + /// + /// This property specifies the random number generator to be used for the initial + /// randomness during ULID creation. + /// + /// + /// An instance of a class that implements the interface + /// to provide the random number generation logic for the initial randomness component.
+ /// Defaults to . + ///
+ public IRandomProvider InitialRandomSource { get; init; } = new CryptographicallySecureRandomProvider(); + + /// + /// Random source used during monotonic ULID generation when timestamps + /// are identical and incremental randomness is required. + /// + /// + /// Specifies the random provider used to supply entropy for the Random component + /// during monotonic increments when consecutive ULIDs share the same timestamp.
+ /// It is used in maintaining monotonicity while ensuring random variation in ULID values. + ///
+ /// + /// An implementation of the interface that provides + /// randomness for monotonic increments.
+ /// Defaults to an instance of . + ///
+ public IRandomProvider IncrementRandomSource { get; init; } = new PseudoRandomProvider(); + }; +} \ No newline at end of file diff --git a/src/ByteAether.Ulid2/Ulid.New.cs b/src/ByteAether.Ulid2/Ulid.New.cs new file mode 100644 index 0000000..5e975e0 --- /dev/null +++ b/src/ByteAether.Ulid2/Ulid.New.cs @@ -0,0 +1,350 @@ +using System.Buffers.Binary; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace ByteAether.Ulid2; + +public readonly partial struct Ulid +{ + /// + /// The default used for generating new ULIDs. + /// + /// + /// Allows customization of the generation behavior for all new ULIDs.
+ /// It includes settings for monotonicity and the source of randomness for initial and + /// incremented scenarios during the generation of the ULID. Modifying this property + /// affects the global default behavior for ULID generation across the application. + ///
+ public static GenerationOptions DefaultGenerationOptions { get; set; } = new(); + + // Constant for Unix Epoch (1970-01-01 UTC) in Ticks + private const long _unixEpochTicks = 621355968000000000; + + /// + /// Initializes a new instance of the struct using the specified byte array. + /// + /// The byte array to initialize the with. + /// Given bytes as an instance. +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public static Ulid New(ReadOnlySpan bytes) + => MemoryMarshal.Read(bytes); + + /// + /// Creates a new with the current timestamp. + /// + /// + /// If null (default), the value of is used.
+ /// Otherwise, uses the specified to control the ULID generation behavior. + /// + /// A new instance. +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public static Ulid New(GenerationOptions? options = null) + // We can avoid Offset-related allocations by using DateTime over DateTimeOffset + // For public API, DateTimeOffset is an official recommendation + => New((DateTime.UtcNow.Ticks - _unixEpochTicks) / TimeSpan.TicksPerMillisecond, options); + + /// + /// Creates a new with the specified timestamp. + /// + /// The timestamp to use for the . + /// + /// If null (default), the value of is used.
+ /// Otherwise, uses the specified to control the ULID generation behavior. + /// + /// A new instance. +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public static Ulid New(DateTimeOffset dateTimeOffset, GenerationOptions? options = null) + => New(dateTimeOffset.ToUnixTimeMilliseconds(), options); + + /// + /// Creates a new with the specified timestamp. + /// + /// The timestamp to use for the . + /// + /// A span containing the random component of the .
+ /// Must be at least 10 bytes long to populate the random component of the Ulid. + /// + /// A new instance. +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public static Ulid New(DateTimeOffset dateTimeOffset, Span random) + => New(dateTimeOffset.ToUnixTimeMilliseconds(), random); + + /// + /// Creates a new with the specified timestamp in milliseconds. + /// + /// The timestamp in milliseconds to use for the . + /// + /// If null (default), the value of is used.
+ /// Otherwise, uses the specified to control the ULID generation behavior. + /// + /// A new instance. +#if NET5_0_OR_GREATER + [SkipLocalsInit] +#endif +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public static Ulid New(long timestamp, GenerationOptions? options = null) + { + Ulid ulid = default; + + ref var ulidRef = ref Unsafe.As(ref ulid); + + // Fill timestamp + BinaryPrimitives.WriteUInt64BigEndian( +#if NETCOREAPP + MemoryMarshal.CreateSpan(ref ulidRef, 8), +#else + Compatibility.MemoryMarshal.CreateSpan(ref ulidRef, 8), +#endif + (ulong)timestamp << 16 + ); + + FillRandom(ref ulidRef, timestamp, options ?? DefaultGenerationOptions); + + return ulid; + } + + /// + /// Creates a new instance of the struct using the specified timestamp and random byte sequence. + /// + /// + /// A 64-bit integer representing the timestamp in milliseconds since the Unix epoch (1970-01-01T00:00:00Z).
+ /// This value will be encoded into the first 6 bytes of the . + /// + /// + /// A span containing the random component of the .
+ /// It must be at least 10 bytes long, as the last 10 bytes of the are derived from this span. + /// + /// + /// A new instance composed of the given timestamp and random byte sequence. + /// +#if NET5_0_OR_GREATER + [SkipLocalsInit] +#endif +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public static Ulid New(long timestamp, Span random) + { + Ulid ulid = default; + + ref var ulidRef = ref Unsafe.As(ref ulid); + + // Fill timestamp + BinaryPrimitives.WriteUInt64BigEndian( +#if NETCOREAPP + MemoryMarshal.CreateSpan(ref ulidRef, 8), +#else + Compatibility.MemoryMarshal.CreateSpan(ref ulidRef, 8), +#endif + (ulong)timestamp << 16 + ); + + Unsafe.CopyBlockUnaligned( + ref Unsafe.Add(ref ulidRef, _ulidSizeTime), + ref random.GetPinnableReference(), + _ulidSizeRandom + ); + + return ulid; + } + + // Separates Lock and LastUlid into different cache lines to prevent "false sharing" + // x64 has 64-byte cache lines; ARM64 (e.g., Apple Silicon) has 128-byte cache lines + [StructLayout(LayoutKind.Explicit)] + private class State + { + // Cache Line 1 + [FieldOffset(16)] public LowLatencyLock Lock; + + // Cache Line 2(ARM64)/3(x64) +#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value + [FieldOffset(16+128)] public ulong LastUlidPart0; + [FieldOffset(16+128+8)] public ulong LastUlidPart1; +#pragma warning restore CS0649 // Field is never assigned to, and will always have its default value + } + + private static readonly State _state = new(); + +#if NET5_0_OR_GREATER + [SkipLocalsInit] +#endif +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveOptimization)] +#endif + private static void FillRandom(ref byte ulidBytesRef, long timestamp, GenerationOptions options) + { + // Calculate offset to a random part + ref var ulidBytesRandomRef = ref Unsafe.Add(ref ulidBytesRef, _ulidSizeTime); + var monotonicity = options.Monotonicity; + + if (monotonicity == GenerationOptions.MonotonicityOptions.NonMonotonic) + { + options.InitialRandomSource.GetBytes( +#if NETCOREAPP + MemoryMarshal.CreateSpan(ref ulidBytesRandomRef, _ulidSizeRandom) +#else + Compatibility.MemoryMarshal.CreateSpan(ref ulidBytesRandomRef, _ulidSizeRandom) +#endif + ); + return; + } + + ref var lastUlidRef = ref Unsafe.As(ref _state.LastUlidPart0); + + using(_state.Lock.Enter()) + { + // Read the last timestamp (from bytes 0-7 of "last ULID") + // Shift it to get 48 bits. + var lastTime = BitConverter.IsLittleEndian + ? BinaryPrimitives.ReverseEndianness(_state.LastUlidPart0) + : _state.LastUlidPart0; + lastTime >>= 16; + + // If the timestamp is bigger than the last one, generate a new ULID + if (timestamp > (long)lastTime) + { + // We work on "generated ULID", then copy it into "last ULID" + + // Generate a new random to the generated ULID + options.InitialRandomSource.GetBytes( +#if NETCOREAPP + MemoryMarshal.CreateSpan(ref ulidBytesRandomRef, _ulidSizeRandom) +#else + Compatibility.MemoryMarshal.CreateSpan(ref ulidBytesRandomRef, _ulidSizeRandom) +#endif + ); + + // Copy full generated ULID back to last ULID + Unsafe.CopyBlock(ref lastUlidRef, ref ulidBytesRef, _ulidSize); + } + else // Otherwise, increment the last ULID + { + // We work on "last ULID", then copy it into "generated ULID" + + if (monotonicity == GenerationOptions.MonotonicityOptions.MonotonicIncrement) + { + IncrementByOne(ref lastUlidRef); + } + else + { + // We can use the random bytes of incomplete ULID for the random increment span + var tempSpan = +#if NETCOREAPP + MemoryMarshal.CreateSpan(ref ulidBytesRandomRef, (int)monotonicity); +#else + Compatibility.MemoryMarshal.CreateSpan(ref ulidBytesRandomRef, (int)monotonicity); +#endif + options.IncrementRandomSource.GetBytes(tempSpan); + IncrementByByteSpan(ref lastUlidRef, tempSpan); + } + + // Copy full last ULID back to generated ULID + Unsafe.CopyBlock(ref ulidBytesRef, ref lastUlidRef, _ulidSize); + } + } + } + +#if NET5_0_OR_GREATER + [SkipLocalsInit] +#endif +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + private static void IncrementByOne(ref byte buffer) + { + const int lastIdx = _ulidSize - 1; + + ushort carry = 1; + ref var currentRef = ref Unsafe.Add(ref buffer, lastIdx); + + for (var i = lastIdx; i >= 0; i--) + { + var val = (ushort)(currentRef + carry); + currentRef = (byte)val; // Implicit & 0xFF + carry = (ushort)(val >> 8); + + if (carry == 0) + { + return; + } + + currentRef = ref Unsafe.Subtract(ref currentRef, 1); + } + + throw new OverflowException("Addition resulted in a value larger than the target span's capacity."); + } + +#if NET5_0_OR_GREATER + [SkipLocalsInit] +#endif +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + private static void IncrementByByteSpan(ref byte targetRef, ReadOnlySpan source) + { + ushort carry = 1; + ushort sum; + var lengthDifference = _ulidSize - source.Length; + + if (source.Length != 0) + { + for (var i = _ulidSize - 1; i >= lengthDifference; --i) + { + var sourceIdx = i - lengthDifference; + + ref var targetByteRef = ref Unsafe.Add(ref targetRef, i); + var byteFromSource = source[sourceIdx]; + + sum = (ushort)(targetByteRef + byteFromSource + carry); + targetByteRef = (byte)sum; // Implicit & 0xFF + carry = (byte)(sum >> 8); + } + + if (carry == 0) + { + return; + } + } + + for (var i = lengthDifference - 1; i >= 0; --i) + { + ref var targetByteRef = ref Unsafe.Add(ref targetRef, i); + sum = (ushort)(targetByteRef + carry); + targetByteRef = (byte)sum; // Implicit & 0xFF + carry = (ushort)(sum >> 8); + + if (carry == 0) + { + return; + } + } + + throw new OverflowException("Addition resulted in a value larger than the target span's capacity."); + } +} \ No newline at end of file diff --git a/src/ByteAether.Ulid2/Ulid.Obsolete.cs b/src/ByteAether.Ulid2/Ulid.Obsolete.cs new file mode 100644 index 0000000..2c7e2be --- /dev/null +++ b/src/ByteAether.Ulid2/Ulid.Obsolete.cs @@ -0,0 +1,102 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace ByteAether.Ulid2; + +public readonly partial struct Ulid +{ + /// + /// Represents the maximum possible value for a ULID. + /// + /// + /// The field is a ULID where all byte components are set to their highest possible value (0xFF). + /// It can be used as a sentinel or boundary value in comparison operations or range validations. + /// + [Obsolete("Use MaxValue instead.")] + public static Ulid Max => MaxValue; + + /// + /// Whether s should be generated in a monotonic manner by default.
+ /// Initial value is set to true.
+ /// This setting applies globally without any scoping. + ///
+ /// + /// When set to true (default), s generated without explicitly specifying monotonicity + /// will ensure that they are monotonically increasing.
+ /// When set to false, s generated without explicitly specifying monotonicity will be + /// generated with random value. + ///
+ [Obsolete("Use DefaultGenerationOptions instead.")] + public static bool DefaultIsMonotonic + { + get => ObsoleteHelper.GetByGenerationOptions(DefaultGenerationOptions); + set => DefaultGenerationOptions = ObsoleteHelper.GetByBoolean(value); + } + + /// + /// Creates a new with the current timestamp. + /// + /// + /// If true, ensures the is monotonically increasing.
+ /// If false, generates a random part in . + /// + /// A new instance. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [Obsolete("Use method with DefaultGenerationOptions argument instead.")] + public static Ulid New(bool isMonotonic) + => New(ObsoleteHelper.GetByBoolean(isMonotonic)); + + /// + /// Creates a new with the specified timestamp. + /// + /// The timestamp to use for the . + /// + /// If true, ensures the is monotonically increasing.
+ /// If false, generates a random part in . + /// + /// A new instance. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [Obsolete("Use method with DefaultGenerationOptions argument instead.")] + public static Ulid New(DateTimeOffset dateTimeOffset, bool isMonotonic) + => New(dateTimeOffset, ObsoleteHelper.GetByBoolean(isMonotonic)); + + /// + /// Creates a new with the specified timestamp in milliseconds. + /// + /// The timestamp in milliseconds to use for the . + /// + /// If true, ensures the is monotonically increasing.
+ /// If false, generates a random part in . + /// + /// A new instance. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [Obsolete("Use method with DefaultGenerationOptions argument instead.")] + public static Ulid New(long timestamp, bool isMonotonic) + => New(timestamp, ObsoleteHelper.GetByBoolean(isMonotonic)); +} + +internal static class ObsoleteHelper +{ + private static readonly Ulid.GenerationOptions _monotonicDefaultOptions = new(); + + private static readonly Ulid.GenerationOptions _nonmonotonicDefaultOptions = new() + { + InitialRandomSource = new CryptographicallySecureRandomProvider(), + IncrementRandomSource = new PseudoRandomProvider(), // This has no effect + Monotonicity = Ulid.GenerationOptions.MonotonicityOptions.NonMonotonic + }; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [return: NotNullIfNotNull(nameof(isMonotonic))] + public static Ulid.GenerationOptions? GetByBoolean(bool? isMonotonic) + => isMonotonic switch + { + true => _monotonicDefaultOptions, + false => _nonmonotonicDefaultOptions, + _ => null + }; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool GetByGenerationOptions(Ulid.GenerationOptions options) + => options.Monotonicity != Ulid.GenerationOptions.MonotonicityOptions.NonMonotonic; +} \ No newline at end of file diff --git a/src/ByteAether.Ulid2/Ulid.String.cs b/src/ByteAether.Ulid2/Ulid.String.cs new file mode 100644 index 0000000..df09faa --- /dev/null +++ b/src/ByteAether.Ulid2/Ulid.String.cs @@ -0,0 +1,458 @@ +using System.Buffers.Binary; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; + +namespace ByteAether.Ulid2; + +[DebuggerDisplay("{ToString(),nq}")] +public readonly partial struct Ulid + : IFormattable +#if NET6_0_OR_GREATER + , ISpanFormattable +#if NET7_0_OR_GREATER + , IParsable // Keeping this here for clarity + , ISpanParsable +#if NET8_0_OR_GREATER + , IUtf8SpanFormattable + , IUtf8SpanParsable +#endif +#endif +#endif +{ + /// + /// The length of a ULID when encoded as a string in its canonical format. + /// + /// + /// A ULID string consists of 26 characters, encoded using Crockford's Base32 encoding. + /// + public const byte UlidStringLength = 26; + + private static readonly char[] _base32Chars = "0123456789ABCDEFGHJKMNPQRSTVWXYZ".ToCharArray(); + private static readonly byte[] _base32Bytes = Encoding.UTF8.GetBytes(_base32Chars); + private static readonly byte[] _inverseBase32 = + [ + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // controls + 255, // space + 255, // ! + 255, // " + 255, // # + 255, // $ + 255, // % + 255, // & + 255, // ' + 255, // ( + 255, // ) + 255, // * + 255, // + + 255, // , + 255, // - + 255, // . + 255, // / + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, // 0-9 + 255, 255, 255, 255, 255, 255, 255, // :-@ + 10, 11, 12, 13, 14, 15, 16, 17, // A-H + 1, // I + 18, 19, // J-K + 1, // L + 20, 21, // M-N + 0, // O + 22, 23, 24, 25, 26, // P-T + 255, // U + 27, 28, 29, 30, 31, // V-Z + 255, 255, 255, 255, 255, 255, // [-` + 10, 11, 12, 13, 14, 15, 16, 17, // a-h + 1, // i + 18, 19, // j-k + 1, // l + 20, 21, // m-n + 0, // o + 22, 23, 24, 25, 26, // p-t + 255, // u + 27, 28, 29, 30, 31, // v-z + // Pad with value 255 so the array size is 256 + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255 + ]; + + /// +#if NET5_0_OR_GREATER + [SkipLocalsInit] +#endif +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public string ToString(string? format, IFormatProvider? formatProvider) => ToString(); + + /// + /// Returns a string representation of the current instance of in its canonical Crockford's Base32 format.' + /// + /// Crockford's Base32 representation of the ULID +#if NET5_0_OR_GREATER + [SkipLocalsInit] +#endif +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public override string ToString() + { +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP + return string.Create(UlidStringLength, this, static (span, ulid) => ulid.Fill(span, _base32Chars)); +#else + Span span = stackalloc char[UlidStringLength]; + Fill(span, _base32Chars); + return span.ToString(); +#endif + } + + /// + /// Parses a ULID from the provided read-only span of characters. + /// + /// The span of characters containing Crockford's Base32 representation of the ULID. + /// Ignored. The ULID is always formatted in its canonical Crockford's Base32 format. + /// A parsed instance of . + /// Thrown if the input span does not meet the ULID format requirements. +#if NET5_0_OR_GREATER + [SkipLocalsInit] +#endif +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public static Ulid Parse(ReadOnlySpan chars, IFormatProvider? provider = null) + => ParseCore(chars); + + /// + /// Parses a ULID from a read-only span of bytes and returns the corresponding ULID value. + /// + /// The read-only span of bytes containing the ULID string representation in Crockford's Base32 format. + /// Ignored. The ULID is always formatted in its canonical Crockford's Base32 format. + /// The ULID parsed from the specified byte span. + /// Thrown if the input byte span does not contain a valid ULID string representation. +#if NET5_0_OR_GREATER + [SkipLocalsInit] +#endif +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public static Ulid Parse(ReadOnlySpan bytes, IFormatProvider? provider = null) + => ParseCore(bytes); + +#if NET5_0_OR_GREATER + [SkipLocalsInit] +#endif +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveOptimization)] +#endif + private static unsafe Ulid ParseCore(ReadOnlySpan input) + where T : unmanaged + { + // Every T element is read as a byte. Other bits are ignored. + // We create 2 blocks of big-endian ulong values then reverse the endianness + // Creating a big-endian ulong and then reversing it is faster than creating directly a little-endian ulong + + if (input.Length != UlidStringLength) + { + throw new FormatException("The input sequence is not a valid ULID string representation."); + } + + var stepSize = sizeof(T); // We read the span as bytes and iterate by the size of an element + Ulid result = default; + + fixed (T* pSrc = &MemoryMarshal.GetReference(input)) + { + var pBytes = (byte*)pSrc; + ref var tableRef = ref _inverseBase32[0]; + ref var ulidRef = ref Unsafe.As(ref result); + + ulong t00 = Unsafe.Add(ref tableRef, pBytes[00 * stepSize]); + ulong t01 = Unsafe.Add(ref tableRef, pBytes[01 * stepSize]); + ulong t02 = Unsafe.Add(ref tableRef, pBytes[02 * stepSize]); + ulong t03 = Unsafe.Add(ref tableRef, pBytes[03 * stepSize]); + ulong t04 = Unsafe.Add(ref tableRef, pBytes[04 * stepSize]); + ulong t05 = Unsafe.Add(ref tableRef, pBytes[05 * stepSize]); + ulong t06 = Unsafe.Add(ref tableRef, pBytes[06 * stepSize]); + ulong t07 = Unsafe.Add(ref tableRef, pBytes[07 * stepSize]); + ulong t08 = Unsafe.Add(ref tableRef, pBytes[08 * stepSize]); + ulong t09 = Unsafe.Add(ref tableRef, pBytes[09 * stepSize]); + ulong r00 = Unsafe.Add(ref tableRef, pBytes[10 * stepSize]); + ulong r01 = Unsafe.Add(ref tableRef, pBytes[11 * stepSize]); + ulong r02 = Unsafe.Add(ref tableRef, pBytes[12 * stepSize]); + ulong r03 = Unsafe.Add(ref tableRef, pBytes[13 * stepSize]); + + var block1 = + (t00 << 61) + | (t01 << 56) + | (t02 << 51) + | (t03 << 46) + | (t04 << 41) + | (t05 << 36) + | (t06 << 31) + | (t07 << 26) + | (t08 << 21) + | (t09 << 16) + | (r00 << 11) + | (r01 << 6) + | (r02 << 1) + | (r03 >> 4); + + Unsafe.WriteUnaligned(ref Unsafe.Add(ref ulidRef, 0), BinaryPrimitives.ReverseEndianness(block1)); + + // Second block - ulong 64 bits + ulong r04 = Unsafe.Add(ref tableRef, pBytes[14 * stepSize]); + ulong r05 = Unsafe.Add(ref tableRef, pBytes[15 * stepSize]); + ulong r06 = Unsafe.Add(ref tableRef, pBytes[16 * stepSize]); + ulong r07 = Unsafe.Add(ref tableRef, pBytes[17 * stepSize]); + ulong r08 = Unsafe.Add(ref tableRef, pBytes[18 * stepSize]); + ulong r09 = Unsafe.Add(ref tableRef, pBytes[19 * stepSize]); + ulong r10 = Unsafe.Add(ref tableRef, pBytes[20 * stepSize]); + ulong r11 = Unsafe.Add(ref tableRef, pBytes[21 * stepSize]); + ulong r12 = Unsafe.Add(ref tableRef, pBytes[22 * stepSize]); + ulong r13 = Unsafe.Add(ref tableRef, pBytes[23 * stepSize]); + ulong r14 = Unsafe.Add(ref tableRef, pBytes[24 * stepSize]); + ulong r15 = Unsafe.Add(ref tableRef, pBytes[25 * stepSize]); + + var block2 = + (r03 << 60) + | (r04 << 55) + | (r05 << 50) + | (r06 << 45) + | (r07 << 40) + | (r08 << 35) + | (r09 << 30) + | (r10 << 25) + | (r11 << 20) + | (r12 << 15) + | (r13 << 10) + | (r14 << 5) + | r15; + + Unsafe.WriteUnaligned(ref Unsafe.Add(ref ulidRef, 8), BinaryPrimitives.ReverseEndianness(block2)); + } + + return result; + } + + /// + /// Parses a string representation of a ULID and returns the corresponding ULID instance. + /// + /// The string representation of the ULID to parse. + /// Ignored. The ULID is always formatted in its canonical Crockford's Base32 format. + /// A new instance parsed from the specified string. +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public static Ulid Parse(string s, IFormatProvider? provider = null) + => ParseCore(s.AsSpan()); + + /// + /// Attempts to parse a string representation of a ULID into a instance. + /// + /// The string representation of the ULID to parse. + /// Ignored. The ULID is always formatted in its canonical Crockford's Base32 format. + /// When this method returns, contains the parsed value if the parse was successful; otherwise, the default value of . + /// true if the parsing was successful; otherwise, false. +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, out Ulid result) + => TryParse(s.AsSpan(), provider, out result); + + /// + /// Attempts to parse a ULID from a read-only span of characters. + /// + /// The read-only span of characters to parse. + /// Ignored. The ULID is always formatted in its canonical Crockford's Base32 format. + /// When the method returns, contains the parsed ULID if the operation succeeds, or the default value if it fails. + /// true if the parsing operation succeeded; otherwise, false. +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, out Ulid result) + { + try + { + result = ParseCore(s); + return true; + } + catch + { + result = default; + return false; + } + } + + /// + /// Attempts to parse a ULID from the specified span of bytes. + /// + /// The span of bytes containing the ULID representation to parse. + /// Ignored. The ULID is always formatted in its canonical Crockford's Base32 format. + /// When the method returns, contains the parsed ULID if parsing was successful; otherwise, the default value for ULID. + /// true if parsing was successful; otherwise, false. +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, out Ulid result) + { + try + { + result = ParseCore(s); + return true; + } + catch + { + result = default; + return false; + } + } + + /// + /// Attempts to format the current instance of into the provided character span. + /// + /// A span of characters where the formatted will be written, if successful. + /// The number of characters written to the destination span. + /// Ignored. The ULID is always formatted in its canonical Crockford's Base32 format. + /// Ignored. The ULID is always formatted in its canonical Crockford's Base32 format. + /// + /// true if the formatting is successful and the destination span is large enough to contain the formatted data; otherwise, false. + /// +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public bool TryFormat( + Span destination, + out int charsWritten, + ReadOnlySpan format, + IFormatProvider? provider = null + ) + { + if (destination.Length < UlidStringLength) + { + charsWritten = 0; + return false; + } + + Fill(destination, _base32Chars); + charsWritten = UlidStringLength; + return true; + } + + /// + /// Attempts to format the current Ulid instance as a sequence of bytes. + /// + /// The span of bytes to write the formatted Ulid into. + /// When this method returns, contains the number of bytes that were written to the span. + /// Ignored. The ULID is always formatted in its canonical Crockford's Base32 format. + /// Ignored. The ULID is always formatted in its canonical Crockford's Base32 format. + /// + /// true if the formatting was successful; false if the destination span was too short to contain the formatted value. + /// +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public bool TryFormat( + Span destination, + out int bytesWritten, + ReadOnlySpan format, + IFormatProvider? provider = null + ) + { + if (destination.Length < UlidStringLength) + { + bytesWritten = 0; + return false; + } + + Fill(destination, _base32Bytes); + bytesWritten = UlidStringLength; + return true; + } + +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveOptimization)] +#endif + private void Fill(Span span, T[] map) where T: unmanaged + { + // Encode randomness + span[25] = map[_r9 & 0x1F]; // [11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][111|11111|] + span[24] = map[((_r8 & 0x3) << 3) | (_r9 >> 5)]; // [11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][111111|11][111|11111] + span[23] = map[(_r8 >> 2) & 0x1F]; // [11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][1|11111|11][11111111] + span[22] = map[((_r7 & 0xF) << 1) | (_r8 >> 7)]; // [11111111][11111111][11111111][11111111][11111111][11111111][11111111][1111|1111][1|1111111][11111111] + span[21] = map[((_r6 & 0x1) << 4) | (_r7 >> 4)]; // [11111111][11111111][11111111][11111111][11111111][11111111][1111111|1][1111|1111][11111111][11111111] + span[20] = map[(_r6 >> 1) & 0x1F]; // [11111111][11111111][11111111][11111111][11111111][11111111][11|11111|1][11111111][11111111][11111111] + span[19] = map[((_r5 & 0x7) << 2) | (_r6 >> 6)]; // [11111111][11111111][11111111][11111111][11111111][11111|111][11|111111][11111111][11111111][11111111] + span[18] = map[(_r5 >> 3) & 0x1F]; // [11111111][11111111][11111111][11111111][11111111][|11111|111][11111111][11111111][11111111][11111111] + span[17] = map[_r4 & 0x1F]; // [11111111][11111111][11111111][11111111][111|11111|][11111111][11111111][11111111][11111111][11111111] + span[16] = map[((_r3 & 0x3) << 3) | (_r4 >> 5)]; // [11111111][11111111][11111111][11111111][111111|11][111|11111][11111111][11111111][11111111][11111111] + span[15] = map[(_r3 >> 2) & 0x1F]; // [11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111] + span[14] = map[((_r2 & 0xF) << 1) | (_r3 >> 7)]; // [11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111] + span[13] = map[((_r1 & 0x1) << 4) | (_r2 >> 4)]; // [11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111] + span[12] = map[(_r1 >> 1) & 0x1F]; // [11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111] + span[11] = map[((_r0 & 0x7) << 2) | (_r1 >> 6)]; // [11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111] + span[10] = map[(_r0 >> 3) & 0x1F]; // [|11111|111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111] + + // Encode timestamp + span[9] = map[_t5 & 0x1F]; // 00[11111111][11111111][11111111][11111111][11111111][111|11111|] + span[8] = map[((_t4 & 0x3) << 3) | (_t5 >> 5)]; // 00[11111111][11111111][11111111][11111111][111111|11][111|11111] + span[7] = map[(_t4 >> 2) & 0x1F]; // 00[11111111][11111111][11111111][11111111][1|11111|11][11111111] + span[6] = map[((_t3 & 0xF) << 1) | (_t4 >> 7)]; // 00[11111111][11111111][11111111][1111|1111][1|1111111][11111111] + span[5] = map[((_t2 & 0x1) << 4) | (_t3 >> 4)]; // 00[11111111][11111111][1111111|1][1111|1111][11111111][11111111] + span[4] = map[(_t2 >> 1) & 0x1F]; // 00[11111111][11111111][11|11111|1][11111111][11111111][11111111] + span[3] = map[((_t1 & 0x7) << 2) | (_t2 >> 6)]; // 00[11111111][11111|111][11|111111][11111111][11111111][11111111] + span[2] = map[_t1 >> 3]; // 00[11111111][|11111|111][11111111][11111111][11111111][11111111] + span[1] = map[_t0 & 0x1F]; // 00[111|11111|][11111111][11111111][11111111][11111111][11111111] + span[0] = map[_t0 >> 5]; // |00[111|11111][11111111][11111111][11111111][11111111][11111111] + } + + /// + /// Allows implicit conversion of to . + /// + /// + /// +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public static implicit operator string(Ulid ulid) => ulid.ToString(); + + /// + /// Allows implicit conversion of to . + /// + /// + /// +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public static implicit operator Ulid(string str) => Parse(str); +} \ No newline at end of file diff --git a/src/ByteAether.Ulid2/Ulid.cs b/src/ByteAether.Ulid2/Ulid.cs new file mode 100644 index 0000000..5f287c0 --- /dev/null +++ b/src/ByteAether.Ulid2/Ulid.cs @@ -0,0 +1,162 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Serialization; +#if NETCOREAPP +using System.Text.Json.Serialization; +#endif + +namespace ByteAether.Ulid2; + +/// +/// Represents a Universally Unique Lexicographically Sortable Identifier (ULID). +/// +/// +/// A ULID is a 128-bit identifier that is sortable by time and consists of a timestamp and random components.
+/// For more information, visit the GitHub repository. +///
+#if NETCOREAPP +[JsonConverter(typeof(UlidJsonConverter))] +#endif +[TypeConverter(typeof(UlidTypeConverter))] +[StructLayout(LayoutKind.Explicit)] +public readonly partial struct Ulid +{ + private const byte _ulidSizeTime = 6; + private const byte _ulidSizeRandom = 10; + private const byte _ulidSize = _ulidSizeTime + _ulidSizeRandom; + + [FieldOffset(00)] private readonly byte _t0; + [FieldOffset(01)] private readonly byte _t1; + [FieldOffset(02)] private readonly byte _t2; + [FieldOffset(03)] private readonly byte _t3; + [FieldOffset(04)] private readonly byte _t4; + [FieldOffset(05)] private readonly byte _t5; + + [FieldOffset(06)] private readonly byte _r0; + [FieldOffset(07)] private readonly byte _r1; + [FieldOffset(08)] private readonly byte _r2; + [FieldOffset(09)] private readonly byte _r3; + [FieldOffset(10)] private readonly byte _r4; + [FieldOffset(11)] private readonly byte _r5; + [FieldOffset(12)] private readonly byte _r6; + [FieldOffset(13)] private readonly byte _r7; + [FieldOffset(14)] private readonly byte _r8; + [FieldOffset(15)] private readonly byte _r9; + + /// + /// Gets the random component of the ULID as a byte array. + /// + /// + /// The random component consists of the last 10 bytes of the ULID and is generated randomly to ensure uniqueness.
+ /// This component does not encode any timestamp or other structured information. + ///
+ /// + /// A byte array containing 10 random bytes that represent the random portion of the ULID. + /// + [IgnoreDataMember] + public ReadOnlySpan Random + { +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + get => AsByteSpan()[_ulidSizeTime..]; + } + + /// + /// Gets the time component of the ULID as a byte array. + /// + /// + /// The time component consists of the first 6 bytes of the ULID and is generated during ULID creation. + /// + /// + /// A byte array containing 6 time bytes that represent the time portion of the ULID. + /// + [IgnoreDataMember] + public ReadOnlySpan TimeBytes + { +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + get => AsByteSpan()[.._ulidSizeTime]; + } + + /// + /// Gets the timestamp component of the ULID as a . + /// + /// + /// The timestamp component represents the number of milliseconds since the Unix epoch + /// (1970-01-01T00:00:00Z). It is stored in the first 6 bytes of the ULID and ensures + /// lexicographical sorting by time.
+ /// The timestamp is extracted in a way that is compatible with both little-endian and big-endian systems. + ///
+ /// + /// A representing the timestamp portion of the ULID. + /// + [IgnoreDataMember] + public DateTimeOffset Time + { +#if NET5_0_OR_GREATER + [SkipLocalsInit] +#endif +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + get + { + // Combine the 6 bytes into a 48-bit timestamp (big-endian order) + var time = + ((long)_t0 << 40) | + ((long)_t1 << 32) | + ((long)_t2 << 24) | + ((long)_t3 << 16) | + ((long)_t4 << 8) | + _t5 + ; + + return DateTimeOffset.FromUnixTimeMilliseconds(time); + } + } + + /// + /// Creates a read-only span of bytes representing the current instance of the struct. + /// + /// + /// A that points to the raw byte representation of the current struct. + /// +#if NET5_0_OR_GREATER + [SkipLocalsInit] +#endif +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public unsafe ReadOnlySpan AsByteSpan() + => new(Unsafe.AsPointer(ref Unsafe.AsRef(in this)), _ulidSize); + + /// + /// Converts the ULID to a byte array. + /// + /// A byte array representing the ULID. +#if NET5_0_OR_GREATER + [SkipLocalsInit] +#endif +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public byte[] ToByteArray() + { + var bytes = new byte[_ulidSize]; + Unsafe.WriteUnaligned(ref bytes[0], this); + return bytes; + } +} \ No newline at end of file diff --git a/src/ByteAether.Ulid2/UlidJsonConverter.cs b/src/ByteAether.Ulid2/UlidJsonConverter.cs new file mode 100644 index 0000000..9ea1a36 --- /dev/null +++ b/src/ByteAether.Ulid2/UlidJsonConverter.cs @@ -0,0 +1,69 @@ +#if NETCOREAPP +using System.Buffers; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace ByteAether.Ulid2; + +/// +/// A custom JSON converter for the type. +/// +public class UlidJsonConverter : JsonConverter +{ + /// + public override Ulid Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + try + { + if (reader.TokenType is not JsonTokenType.String and not JsonTokenType.PropertyName) + { + throw new JsonException("Expected string or property name"); + } + + if (reader.HasValueSequence) + { + var byteSequence = reader.ValueSequence; + if (byteSequence.Length != Ulid.UlidStringLength) + { + throw new JsonException($"Ulid invalid: length must be {Ulid.UlidStringLength}"); + } + + Span byteSpan = stackalloc byte[Ulid.UlidStringLength]; + byteSequence.CopyTo(byteSpan); + return Ulid.Parse(byteSpan); + } + else + { + var byteSpan = reader.ValueSpan; + return Ulid.Parse(byteSpan); + } + } + catch (FormatException ex) + { + throw new JsonException($"Ulid invalid: length must be {Ulid.UlidStringLength}", ex); + } + } + + /// + public override void Write(Utf8JsonWriter writer, Ulid ulid, JsonSerializerOptions options) + { + Span ulidString = stackalloc byte[Ulid.UlidStringLength]; + ulid.TryFormat(ulidString, out _, []); + writer.WriteStringValue(ulidString); + } + +#if NET6_0_OR_GREATER + /// + public override void WriteAsPropertyName(Utf8JsonWriter writer, Ulid ulid, JsonSerializerOptions options) + { + Span ulidString = stackalloc byte[Ulid.UlidStringLength]; + ulid.TryFormat(ulidString, out _, []); + writer.WritePropertyName(ulidString); + } + + /// + public override Ulid ReadAsPropertyName(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => Read(ref reader, typeToConvert, options); +#endif +} +#endif \ No newline at end of file diff --git a/src/ByteAether.Ulid2/UlidTypeConverter.cs b/src/ByteAether.Ulid2/UlidTypeConverter.cs new file mode 100644 index 0000000..0701199 --- /dev/null +++ b/src/ByteAether.Ulid2/UlidTypeConverter.cs @@ -0,0 +1,48 @@ +#if !NET6_0_OR_GREATER +// ReSharper disable NullnessAnnotationConflictWithJetBrainsAnnotations +// ReSharper disable AssignNullToNotNullAttribute +// ReSharper disable ReturnTypeCanBeNotNullable +#endif + +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; + +namespace ByteAether.Ulid2; + +/// +/// A type converter for the type. +/// +public class UlidTypeConverter : TypeConverter +{ + private static readonly Type[] _convertibleTypes = [typeof(string), typeof(byte[]), typeof(Guid)]; + + /// + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) + => _convertibleTypes.Contains(sourceType) + || base.CanConvertFrom(context, sourceType); + + /// + public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) + => value switch + { + string s => Ulid.Parse(s), + byte[] b => Ulid.New(b), + Guid guid => Ulid.New(guid), + _ => base.ConvertFrom(context, culture, value), + }; + + /// + public override bool CanConvertTo(ITypeDescriptorContext? context, [NotNullWhen(true)] Type? destinationType) + => _convertibleTypes.Contains(destinationType) + || base.CanConvertTo(context, destinationType); + + /// + public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType) + => value is Ulid ulid + ? destinationType == typeof(string) ? ulid.ToString() + : destinationType == typeof(byte[]) ? ulid.ToByteArray() + : destinationType == typeof(Guid) ? ulid.ToGuid() + : base.ConvertTo(context, culture, value, destinationType) + : base.ConvertTo(context, culture, value, destinationType); +} \ No newline at end of file From c5df2005fccd4e030c12c3cd8a47ee7ecdb7f574 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joonatan=20Uusv=C3=A4li?= Date: Mon, 6 Jul 2026 19:57:22 +0300 Subject: [PATCH 2/5] Refactored FillRandom monotonic incrementation logic. Improved `long timestamp` argument parsing into time-part bytes. --- src/ByteAether.Ulid2/Ulid.New.cs | 124 ++++++++++--------------------- 1 file changed, 38 insertions(+), 86 deletions(-) diff --git a/src/ByteAether.Ulid2/Ulid.New.cs b/src/ByteAether.Ulid2/Ulid.New.cs index 5e975e0..d479471 100644 --- a/src/ByteAether.Ulid2/Ulid.New.cs +++ b/src/ByteAether.Ulid2/Ulid.New.cs @@ -109,14 +109,12 @@ public static Ulid New(long timestamp, GenerationOptions? options = null) ref var ulidRef = ref Unsafe.As(ref ulid); // Fill timestamp - BinaryPrimitives.WriteUInt64BigEndian( -#if NETCOREAPP - MemoryMarshal.CreateSpan(ref ulidRef, 8), -#else - Compatibility.MemoryMarshal.CreateSpan(ref ulidRef, 8), -#endif - (ulong)timestamp << 16 - ); + var ts = (ulong)timestamp << 16; + if (BitConverter.IsLittleEndian) + { + ts = BinaryPrimitives.ReverseEndianness(ts); + } + Unsafe.WriteUnaligned(ref ulidRef, ts); FillRandom(ref ulidRef, timestamp, options ?? DefaultGenerationOptions); @@ -152,15 +150,14 @@ public static Ulid New(long timestamp, Span random) ref var ulidRef = ref Unsafe.As(ref ulid); // Fill timestamp - BinaryPrimitives.WriteUInt64BigEndian( -#if NETCOREAPP - MemoryMarshal.CreateSpan(ref ulidRef, 8), -#else - Compatibility.MemoryMarshal.CreateSpan(ref ulidRef, 8), -#endif - (ulong)timestamp << 16 - ); + var ts = (ulong)timestamp << 16; + if (BitConverter.IsLittleEndian) + { + ts = BinaryPrimitives.ReverseEndianness(ts); + } + Unsafe.WriteUnaligned(ref ulidRef, ts); + // Fill random Unsafe.CopyBlockUnaligned( ref Unsafe.Add(ref ulidRef, _ulidSizeTime), ref random.GetPinnableReference(), @@ -245,19 +242,27 @@ private static void FillRandom(ref byte ulidBytesRef, long timestamp, Generation if (monotonicity == GenerationOptions.MonotonicityOptions.MonotonicIncrement) { - IncrementByOne(ref lastUlidRef); + LastUlidIncrement(0); } else { // We can use the random bytes of incomplete ULID for the random increment span var tempSpan = #if NETCOREAPP - MemoryMarshal.CreateSpan(ref ulidBytesRandomRef, (int)monotonicity); + MemoryMarshal.CreateSpan(ref ulidBytesRandomRef, sizeof(uint)); #else - Compatibility.MemoryMarshal.CreateSpan(ref ulidBytesRandomRef, (int)monotonicity); + Compatibility.MemoryMarshal.CreateSpan(ref ulidBytesRandomRef, sizeof(uint)); #endif - options.IncrementRandomSource.GetBytes(tempSpan); - IncrementByByteSpan(ref lastUlidRef, tempSpan); + options.IncrementRandomSource.GetBytes(tempSpan[..(int)monotonicity]); + var increment = BinaryPrimitives.ReadUInt32LittleEndian(tempSpan); + + var totalBitsToKeep = (int)monotonicity * sizeof(byte); + + // Shift 1UL left by the total bits, then subtract 1 to create the bitmask + var mask = (uint)((1UL << totalBitsToKeep) - 1); + increment &= mask; + + LastUlidIncrement(increment); } // Copy full last ULID back to generated ULID @@ -274,77 +279,24 @@ private static void FillRandom(ref byte ulidBytesRef, long timestamp, Generation #else [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif - private static void IncrementByOne(ref byte buffer) - { - const int lastIdx = _ulidSize - 1; - - ushort carry = 1; - ref var currentRef = ref Unsafe.Add(ref buffer, lastIdx); - - for (var i = lastIdx; i >= 0; i--) - { - var val = (ushort)(currentRef + carry); - currentRef = (byte)val; // Implicit & 0xFF - carry = (ushort)(val >> 8); - - if (carry == 0) - { - return; - } - - currentRef = ref Unsafe.Subtract(ref currentRef, 1); - } - - throw new OverflowException("Addition resulted in a value larger than the target span's capacity."); - } - -#if NET5_0_OR_GREATER - [SkipLocalsInit] -#endif -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - private static void IncrementByByteSpan(ref byte targetRef, ReadOnlySpan source) + private static void LastUlidIncrement(uint addition) { - ushort carry = 1; - ushort sum; - var lengthDifference = _ulidSize - source.Length; + var increment = (ulong)addition + 1; // carry = 1 is built-in + var part1 = BinaryPrimitives.ReverseEndianness(_state.LastUlidPart1); + var newPart1 = part1 + increment; + _state.LastUlidPart1 = BinaryPrimitives.ReverseEndianness(newPart1); - if (source.Length != 0) + if (newPart1 >= part1) // Overflow carried over to part0 { - for (var i = _ulidSize - 1; i >= lengthDifference; --i) - { - var sourceIdx = i - lengthDifference; - - ref var targetByteRef = ref Unsafe.Add(ref targetRef, i); - var byteFromSource = source[sourceIdx]; - - sum = (ushort)(targetByteRef + byteFromSource + carry); - targetByteRef = (byte)sum; // Implicit & 0xFF - carry = (byte)(sum >> 8); - } - - if (carry == 0) - { - return; - } + return; } - for (var i = lengthDifference - 1; i >= 0; --i) + var part0 = BinaryPrimitives.ReverseEndianness(_state.LastUlidPart0); + part0++; + _state.LastUlidPart0 = BinaryPrimitives.ReverseEndianness(part0); + if (part0 == 0) { - ref var targetByteRef = ref Unsafe.Add(ref targetRef, i); - sum = (ushort)(targetByteRef + carry); - targetByteRef = (byte)sum; // Implicit & 0xFF - carry = (ushort)(sum >> 8); - - if (carry == 0) - { - return; - } + throw new OverflowException("Addition resulted in a ULID value larger than the absolute maximum ULID value."); } - - throw new OverflowException("Addition resulted in a value larger than the target span's capacity."); } } \ No newline at end of file From 634b3b3d1f90914ffb71f65eefd96ba942bd33e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joonatan=20Uusv=C3=A4li?= Date: Mon, 6 Jul 2026 20:20:16 +0300 Subject: [PATCH 3/5] Proper endianness handling throughout the ULID implementation. --- src/ByteAether.Ulid2/Ulid.Comparable.cs | 8 ++++---- src/ByteAether.Ulid2/Ulid.New.cs | 25 ++++++++----------------- src/ByteAether.Ulid2/Ulid.String.cs | 6 +++--- src/ByteAether.Ulid2/Ulid.Util.cs | 13 +++++++++++++ 4 files changed, 28 insertions(+), 24 deletions(-) create mode 100644 src/ByteAether.Ulid2/Ulid.Util.cs diff --git a/src/ByteAether.Ulid2/Ulid.Comparable.cs b/src/ByteAether.Ulid2/Ulid.Comparable.cs index fd31fac..d4350d8 100644 --- a/src/ByteAether.Ulid2/Ulid.Comparable.cs +++ b/src/ByteAether.Ulid2/Ulid.Comparable.cs @@ -104,16 +104,16 @@ private static int CompareToCore(in Ulid left, in Ulid right) ref var rA = ref Unsafe.As(ref Unsafe.AsRef(in left)); ref var rB = ref Unsafe.As(ref Unsafe.AsRef(in right)); - var a = BinaryPrimitives.ReverseEndianness(rA); - var b = BinaryPrimitives.ReverseEndianness(rB); + var a = ReverseOnLittleEndian(rA); + var b = ReverseOnLittleEndian(rB); if (a != b) { return a < b ? -1 : 1; } - a = BinaryPrimitives.ReverseEndianness(Unsafe.Add(ref rA, 1)); - b = BinaryPrimitives.ReverseEndianness(Unsafe.Add(ref rB, 1)); + a = ReverseOnLittleEndian(Unsafe.Add(ref rA, 1)); + b = ReverseOnLittleEndian(Unsafe.Add(ref rB, 1)); if (a != b) { diff --git a/src/ByteAether.Ulid2/Ulid.New.cs b/src/ByteAether.Ulid2/Ulid.New.cs index d479471..960305d 100644 --- a/src/ByteAether.Ulid2/Ulid.New.cs +++ b/src/ByteAether.Ulid2/Ulid.New.cs @@ -110,10 +110,7 @@ public static Ulid New(long timestamp, GenerationOptions? options = null) // Fill timestamp var ts = (ulong)timestamp << 16; - if (BitConverter.IsLittleEndian) - { - ts = BinaryPrimitives.ReverseEndianness(ts); - } + ts = ReverseOnLittleEndian(ts); Unsafe.WriteUnaligned(ref ulidRef, ts); FillRandom(ref ulidRef, timestamp, options ?? DefaultGenerationOptions); @@ -151,10 +148,7 @@ public static Ulid New(long timestamp, Span random) // Fill timestamp var ts = (ulong)timestamp << 16; - if (BitConverter.IsLittleEndian) - { - ts = BinaryPrimitives.ReverseEndianness(ts); - } + ts = ReverseOnLittleEndian(ts); Unsafe.WriteUnaligned(ref ulidRef, ts); // Fill random @@ -214,9 +208,7 @@ private static void FillRandom(ref byte ulidBytesRef, long timestamp, Generation { // Read the last timestamp (from bytes 0-7 of "last ULID") // Shift it to get 48 bits. - var lastTime = BitConverter.IsLittleEndian - ? BinaryPrimitives.ReverseEndianness(_state.LastUlidPart0) - : _state.LastUlidPart0; + var lastTime = ReverseOnLittleEndian(_state.LastUlidPart0); lastTime >>= 16; // If the timestamp is bigger than the last one, generate a new ULID @@ -256,9 +248,8 @@ private static void FillRandom(ref byte ulidBytesRef, long timestamp, Generation options.IncrementRandomSource.GetBytes(tempSpan[..(int)monotonicity]); var increment = BinaryPrimitives.ReadUInt32LittleEndian(tempSpan); + // The tempSpan may contain garbage, so mask that out var totalBitsToKeep = (int)monotonicity * sizeof(byte); - - // Shift 1UL left by the total bits, then subtract 1 to create the bitmask var mask = (uint)((1UL << totalBitsToKeep) - 1); increment &= mask; @@ -282,18 +273,18 @@ private static void FillRandom(ref byte ulidBytesRef, long timestamp, Generation private static void LastUlidIncrement(uint addition) { var increment = (ulong)addition + 1; // carry = 1 is built-in - var part1 = BinaryPrimitives.ReverseEndianness(_state.LastUlidPart1); + var part1 = ReverseOnLittleEndian(_state.LastUlidPart1); var newPart1 = part1 + increment; - _state.LastUlidPart1 = BinaryPrimitives.ReverseEndianness(newPart1); + _state.LastUlidPart1 = ReverseOnLittleEndian(newPart1); if (newPart1 >= part1) // Overflow carried over to part0 { return; } - var part0 = BinaryPrimitives.ReverseEndianness(_state.LastUlidPart0); + var part0 = ReverseOnLittleEndian(_state.LastUlidPart0); part0++; - _state.LastUlidPart0 = BinaryPrimitives.ReverseEndianness(part0); + _state.LastUlidPart0 = ReverseOnLittleEndian(part0); if (part0 == 0) { throw new OverflowException("Addition resulted in a ULID value larger than the absolute maximum ULID value."); diff --git a/src/ByteAether.Ulid2/Ulid.String.cs b/src/ByteAether.Ulid2/Ulid.String.cs index df09faa..33c8085 100644 --- a/src/ByteAether.Ulid2/Ulid.String.cs +++ b/src/ByteAether.Ulid2/Ulid.String.cs @@ -212,7 +212,7 @@ private static unsafe Ulid ParseCore(ReadOnlySpan input) | (r02 << 1) | (r03 >> 4); - Unsafe.WriteUnaligned(ref Unsafe.Add(ref ulidRef, 0), BinaryPrimitives.ReverseEndianness(block1)); + Unsafe.WriteUnaligned(ref Unsafe.Add(ref ulidRef, 0), ReverseOnLittleEndian(block1)); // Second block - ulong 64 bits ulong r04 = Unsafe.Add(ref tableRef, pBytes[14 * stepSize]); @@ -243,7 +243,7 @@ private static unsafe Ulid ParseCore(ReadOnlySpan input) | (r14 << 5) | r15; - Unsafe.WriteUnaligned(ref Unsafe.Add(ref ulidRef, 8), BinaryPrimitives.ReverseEndianness(block2)); + Unsafe.WriteUnaligned(ref Unsafe.Add(ref ulidRef, 8), ReverseOnLittleEndian(block2)); } return result; @@ -429,7 +429,7 @@ private void Fill(Span span, T[] map) where T: unmanaged span[3] = map[((_t1 & 0x7) << 2) | (_t2 >> 6)]; // 00[11111111][11111|111][11|111111][11111111][11111111][11111111] span[2] = map[_t1 >> 3]; // 00[11111111][|11111|111][11111111][11111111][11111111][11111111] span[1] = map[_t0 & 0x1F]; // 00[111|11111|][11111111][11111111][11111111][11111111][11111111] - span[0] = map[_t0 >> 5]; // |00[111|11111][11111111][11111111][11111111][11111111][11111111] + span[0] = map[_t0 >> 5]; // |00[111|11111][11111111][11111111][11111111][11111111][11111111] } /// diff --git a/src/ByteAether.Ulid2/Ulid.Util.cs b/src/ByteAether.Ulid2/Ulid.Util.cs new file mode 100644 index 0000000..36f1fd2 --- /dev/null +++ b/src/ByteAether.Ulid2/Ulid.Util.cs @@ -0,0 +1,13 @@ +using System.Buffers.Binary; +using System.Runtime.CompilerServices; + +namespace ByteAether.Ulid2; + +public readonly partial struct Ulid +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong ReverseOnLittleEndian(ulong value) + => BitConverter.IsLittleEndian + ? BinaryPrimitives.ReverseEndianness(value) + : value; +} \ No newline at end of file From db906cd60f35938f7ca8e10a64a8bb56f7820f4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joonatan=20Uusv=C3=A4li?= Date: Tue, 7 Jul 2026 16:25:06 +0300 Subject: [PATCH 4/5] Finalizing Performance optimization. --- .../ByteAether.Benchmarks.csproj | 1 - src/ByteAether.Ulid.Benchmarks/Program.cs | 9 +- src/ByteAether.Ulid.Tests/Ulid.New.Tests.cs | 4 +- src/ByteAether.Ulid.sln | 6 - .../Compatibility/MemoryMarshal.cs | 36 -- .../LowLatencyLock.cs | 2 +- src/ByteAether.Ulid/Ulid.Comparable.cs | 11 +- src/ByteAether.Ulid/Ulid.New.cs | 230 +++------ src/ByteAether.Ulid/Ulid.String.cs | 6 +- .../Ulid.Util.cs | 2 +- src/ByteAether.Ulid2/ByteAether.Ulid2.csproj | 63 --- src/ByteAether.Ulid2/Compatibility/Index.cs | 176 ------- .../Compatibility/IsExternalInit.cs | 28 -- .../Compatibility/MemoryMarshal.cs | 30 -- .../Compatibility/NullableAttributes.cs | 208 -------- .../RandomNumberGeneratorExtensions.cs | 29 -- src/ByteAether.Ulid2/Compatibility/Range.cs | 141 ------ .../CryptographicallySecureRandomProvider.cs | 22 - src/ByteAether.Ulid2/IRandomProvider.cs | 18 - src/ByteAether.Ulid2/PACKAGE.md | 205 -------- src/ByteAether.Ulid2/PseudoRandomProvider.cs | 33 -- src/ByteAether.Ulid2/Ulid.Boundaries.cs | 68 --- src/ByteAether.Ulid2/Ulid.Comparable.cs | 125 ----- src/ByteAether.Ulid2/Ulid.Equatable.cs | 125 ----- src/ByteAether.Ulid2/Ulid.Guid.cs | 134 ----- src/ByteAether.Ulid2/Ulid.IsValid.cs | 128 ----- .../Ulid.New.GenerationOptions.cs | 142 ------ src/ByteAether.Ulid2/Ulid.New.cs | 293 ----------- src/ByteAether.Ulid2/Ulid.Obsolete.cs | 102 ---- src/ByteAether.Ulid2/Ulid.String.cs | 458 ------------------ src/ByteAether.Ulid2/Ulid.cs | 162 ------- src/ByteAether.Ulid2/UlidJsonConverter.cs | 69 --- src/ByteAether.Ulid2/UlidTypeConverter.cs | 48 -- 33 files changed, 90 insertions(+), 3024 deletions(-) rename src/{ByteAether.Ulid2 => ByteAether.Ulid}/LowLatencyLock.cs (95%) rename src/{ByteAether.Ulid2 => ByteAether.Ulid}/Ulid.Util.cs (88%) delete mode 100644 src/ByteAether.Ulid2/ByteAether.Ulid2.csproj delete mode 100644 src/ByteAether.Ulid2/Compatibility/Index.cs delete mode 100644 src/ByteAether.Ulid2/Compatibility/IsExternalInit.cs delete mode 100644 src/ByteAether.Ulid2/Compatibility/MemoryMarshal.cs delete mode 100644 src/ByteAether.Ulid2/Compatibility/NullableAttributes.cs delete mode 100644 src/ByteAether.Ulid2/Compatibility/RandomNumberGeneratorExtensions.cs delete mode 100644 src/ByteAether.Ulid2/Compatibility/Range.cs delete mode 100644 src/ByteAether.Ulid2/CryptographicallySecureRandomProvider.cs delete mode 100644 src/ByteAether.Ulid2/IRandomProvider.cs delete mode 100644 src/ByteAether.Ulid2/PACKAGE.md delete mode 100644 src/ByteAether.Ulid2/PseudoRandomProvider.cs delete mode 100644 src/ByteAether.Ulid2/Ulid.Boundaries.cs delete mode 100644 src/ByteAether.Ulid2/Ulid.Comparable.cs delete mode 100644 src/ByteAether.Ulid2/Ulid.Equatable.cs delete mode 100644 src/ByteAether.Ulid2/Ulid.Guid.cs delete mode 100644 src/ByteAether.Ulid2/Ulid.IsValid.cs delete mode 100644 src/ByteAether.Ulid2/Ulid.New.GenerationOptions.cs delete mode 100644 src/ByteAether.Ulid2/Ulid.New.cs delete mode 100644 src/ByteAether.Ulid2/Ulid.Obsolete.cs delete mode 100644 src/ByteAether.Ulid2/Ulid.String.cs delete mode 100644 src/ByteAether.Ulid2/Ulid.cs delete mode 100644 src/ByteAether.Ulid2/UlidJsonConverter.cs delete mode 100644 src/ByteAether.Ulid2/UlidTypeConverter.cs diff --git a/src/ByteAether.Ulid.Benchmarks/ByteAether.Benchmarks.csproj b/src/ByteAether.Ulid.Benchmarks/ByteAether.Benchmarks.csproj index ba6a5b7..6f08bcf 100644 --- a/src/ByteAether.Ulid.Benchmarks/ByteAether.Benchmarks.csproj +++ b/src/ByteAether.Ulid.Benchmarks/ByteAether.Benchmarks.csproj @@ -15,7 +15,6 @@ - diff --git a/src/ByteAether.Ulid.Benchmarks/Program.cs b/src/ByteAether.Ulid.Benchmarks/Program.cs index d5cc64a..feb3ec1 100644 --- a/src/ByteAether.Ulid.Benchmarks/Program.cs +++ b/src/ByteAether.Ulid.Benchmarks/Program.cs @@ -13,8 +13,8 @@ .HideColumns(Column.Job, Column.StdDev, Column.Median) ; -BenchmarkRunner.Run( - //typeof(Program).Assembly, +BenchmarkRunner.Run( + typeof(Program).Assembly, benchmarkConfig ); @@ -52,9 +52,6 @@ public class Generate public ByteAether.Ulid.Ulid ByteAetherUlid() => ByteAether.Ulid.Ulid.New(); [Benchmark] - public ByteAether.Ulid2.Ulid ByteAetherUlid2() => ByteAether.Ulid2.Ulid.New(); - - /*[Benchmark] public ByteAether.Ulid.Ulid ByteAetherUlidR1Bp() => ByteAether.Ulid.Ulid.New(_byteAetherUlidOptionsR1Bp); [Benchmark] @@ -70,7 +67,7 @@ public class Generate public NetUlid.Ulid NetUlid() => global::NetUlid.Ulid.Generate(); [Benchmark] - public NUlid.Ulid NUlid() => global::NUlid.Ulid.NewUlid(_nUlidRandomProvider);*/ + public NUlid.Ulid NUlid() => global::NUlid.Ulid.NewUlid(_nUlidRandomProvider); } [MemoryDiagnoser] diff --git a/src/ByteAether.Ulid.Tests/Ulid.New.Tests.cs b/src/ByteAether.Ulid.Tests/Ulid.New.Tests.cs index 96c8417..3b7c090 100644 --- a/src/ByteAether.Ulid.Tests/Ulid.New.Tests.cs +++ b/src/ByteAether.Ulid.Tests/Ulid.New.Tests.cs @@ -175,7 +175,7 @@ int incrementSize initialRandom[9] = 0xA; // 10 var increment = new byte[incrementSize]; - increment[incrementSize - 1] = 4; // 10 + 4 + increment[0] = 4; // 10 + 4 var incrementedRandom = new byte[10]; incrementedRandom[9] = 15; // 10 + 4 + 1 : +1 comes from base implementation of Ulid @@ -221,7 +221,7 @@ public void New_MonotonicRandom_ShouldCarryOverIncrement( initialRandom[9] = 0xFE; // Max - 2 var increment = new byte[incrementSize]; - increment[incrementSize - 1] = 0x01; // 1 as the other +1 comes from base implementation + increment[0] = 0x01; // 1 as the other +1 comes from base implementation var initialRandomProvider = new ControllableRandomProvider(initialRandom); var incrementRandomProvider = new ControllableRandomProvider(increment); diff --git a/src/ByteAether.Ulid.sln b/src/ByteAether.Ulid.sln index a898e6b..7fb400e 100644 --- a/src/ByteAether.Ulid.sln +++ b/src/ByteAether.Ulid.sln @@ -18,8 +18,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ByteAether.Ulid.Tests.AotConsole", "ByteAether.Ulid.Tests.AotConsole\ByteAether.Ulid.Tests.AotConsole.csproj", "{A16BAD2D-8F7F-49E5-880A-349E31F6E70E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ByteAether.Ulid2", "ByteAether.Ulid2\ByteAether.Ulid2.csproj", "{8B5C57B8-11A3-4D0B-A444-9BAC50A339B6}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -42,10 +40,6 @@ Global {A16BAD2D-8F7F-49E5-880A-349E31F6E70E}.Debug|Any CPU.Build.0 = Debug|Any CPU {A16BAD2D-8F7F-49E5-880A-349E31F6E70E}.Release|Any CPU.ActiveCfg = Release|Any CPU {A16BAD2D-8F7F-49E5-880A-349E31F6E70E}.Release|Any CPU.Build.0 = Release|Any CPU - {8B5C57B8-11A3-4D0B-A444-9BAC50A339B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8B5C57B8-11A3-4D0B-A444-9BAC50A339B6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8B5C57B8-11A3-4D0B-A444-9BAC50A339B6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8B5C57B8-11A3-4D0B-A444-9BAC50A339B6}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/ByteAether.Ulid/Compatibility/MemoryMarshal.cs b/src/ByteAether.Ulid/Compatibility/MemoryMarshal.cs index 8e7463b..898979b 100644 --- a/src/ByteAether.Ulid/Compatibility/MemoryMarshal.cs +++ b/src/ByteAether.Ulid/Compatibility/MemoryMarshal.cs @@ -5,30 +5,11 @@ #pragma warning disable // https://github.com/dotnet/runtime/blob/8d796d8e60a5236cbd5f113ead1d3831064cdba1/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/MemoryMarshal.cs#L226 -// ref T GetArrayDataReference(T[] array) is original. namespace ByteAether.Ulid.Compatibility; public static class MemoryMarshal { - /// - /// Returns a reference to the 0th element of . If the array is empty, returns a reference to where the 0th element - /// would have been stored. Such a reference may be used for pinning but must never be dereferenced. - /// - /// is . - /// - /// This method does not perform array variance checks. The caller must manually perform any array variance checks - /// if the caller wishes to write to the returned reference. - /// - public static unsafe ref T GetArrayDataReference(T[] array) - where T : unmanaged - { - fixed (T* ptr = array) - { - return ref Unsafe.AsRef(ptr); - } - } - /// /// Creates a new span over a portion of a regular managed object. This can be useful /// if part of a managed object represents a "fixed array." This is dangerous because the @@ -45,22 +26,5 @@ public static unsafe ref T GetArrayDataReference(T[] array) [MethodImpl(MethodImplOptions.AggressiveInlining)] public unsafe static Span CreateSpan(scoped ref T reference, int length) => new Span(Unsafe.AsPointer(ref reference), length); - - /// - /// Creates a new read-only span over a portion of a regular managed object. This can be useful - /// if part of a managed object represents a "fixed array." This is dangerous because the - /// is not checked. - /// - /// A reference to data. - /// The number of elements the memory contains. - /// A read-only span representing the specified reference and length. - /// - /// This method should be used with caution. It is dangerous because the length argument is not checked. - /// Even though the ref is annotated as scoped, it will be stored into the returned span, and the lifetime - /// of the returned span will not be validated for safety, even by span-aware languages. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public unsafe static ReadOnlySpan CreateReadOnlySpan(scoped ref T reference, int length) => - new ReadOnlySpan(Unsafe.AsPointer(ref reference), length); } #endif \ No newline at end of file diff --git a/src/ByteAether.Ulid2/LowLatencyLock.cs b/src/ByteAether.Ulid/LowLatencyLock.cs similarity index 95% rename from src/ByteAether.Ulid2/LowLatencyLock.cs rename to src/ByteAether.Ulid/LowLatencyLock.cs index 21da602..41f5aa1 100644 --- a/src/ByteAether.Ulid2/LowLatencyLock.cs +++ b/src/ByteAether.Ulid/LowLatencyLock.cs @@ -3,7 +3,7 @@ using System.Runtime.InteropServices; #endif -namespace ByteAether.Ulid2; +namespace ByteAether.Ulid; internal struct LowLatencyLock { diff --git a/src/ByteAether.Ulid/Ulid.Comparable.cs b/src/ByteAether.Ulid/Ulid.Comparable.cs index 9df8c95..12d32cd 100644 --- a/src/ByteAether.Ulid/Ulid.Comparable.cs +++ b/src/ByteAether.Ulid/Ulid.Comparable.cs @@ -1,5 +1,4 @@ -using System.Buffers.Binary; -using System.Runtime.CompilerServices; +using System.Runtime.CompilerServices; #if NET7_0_OR_GREATER using System.Numerics; #endif @@ -104,16 +103,16 @@ private static int CompareToCore(in Ulid left, in Ulid right) ref var rA = ref Unsafe.As(ref Unsafe.AsRef(in left)); ref var rB = ref Unsafe.As(ref Unsafe.AsRef(in right)); - var a = BinaryPrimitives.ReverseEndianness(rA); - var b = BinaryPrimitives.ReverseEndianness(rB); + var a = ReverseOnLittleEndian(rA); + var b = ReverseOnLittleEndian(rB); if (a != b) { return a < b ? -1 : 1; } - a = BinaryPrimitives.ReverseEndianness(Unsafe.Add(ref rA, 1)); - b = BinaryPrimitives.ReverseEndianness(Unsafe.Add(ref rB, 1)); + a = ReverseOnLittleEndian(Unsafe.Add(ref rA, 1)); + b = ReverseOnLittleEndian(Unsafe.Add(ref rB, 1)); if (a != b) { diff --git a/src/ByteAether.Ulid/Ulid.New.cs b/src/ByteAether.Ulid/Ulid.New.cs index 825cf6f..d924832 100644 --- a/src/ByteAether.Ulid/Ulid.New.cs +++ b/src/ByteAether.Ulid/Ulid.New.cs @@ -17,8 +17,6 @@ public readonly partial struct Ulid /// public static GenerationOptions DefaultGenerationOptions { get; set; } = new(); - private static readonly byte[] _lastUlid = new byte[_ulidSize]; - // Constant for Unix Epoch (1970-01-01 UTC) in Ticks private const long _unixEpochTicks = 621355968000000000; @@ -111,14 +109,9 @@ public static Ulid New(long timestamp, GenerationOptions? options = null) ref var ulidRef = ref Unsafe.As(ref ulid); // Fill timestamp - BinaryPrimitives.WriteUInt64BigEndian( -#if NETCOREAPP - MemoryMarshal.CreateSpan(ref ulidRef, 8), -#else - Compatibility.MemoryMarshal.CreateSpan(ref ulidRef, 8), -#endif - (ulong)timestamp << 16 - ); + var ts = (ulong)timestamp << 16; + ts = ReverseOnLittleEndian(ts); + Unsafe.WriteUnaligned(ref ulidRef, ts); FillRandom(ref ulidRef, timestamp, options ?? DefaultGenerationOptions); @@ -154,15 +147,11 @@ public static Ulid New(long timestamp, Span random) ref var ulidRef = ref Unsafe.As(ref ulid); // Fill timestamp - BinaryPrimitives.WriteUInt64BigEndian( -#if NETCOREAPP - MemoryMarshal.CreateSpan(ref ulidRef, 8), -#else - Compatibility.MemoryMarshal.CreateSpan(ref ulidRef, 8), -#endif - (ulong)timestamp << 16 - ); + var ts = (ulong)timestamp << 16; + ts = ReverseOnLittleEndian(ts); + Unsafe.WriteUnaligned(ref ulidRef, ts); + // Fill random Unsafe.CopyBlockUnaligned( ref Unsafe.Add(ref ulidRef, _ulidSizeTime), ref random.GetPinnableReference(), @@ -172,32 +161,23 @@ ref random.GetPinnableReference(), return ulid; } - private static int _lastUlidLock; - -#if NET5_0_OR_GREATER - [SkipLocalsInit] -#endif -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - private static void AcquireSpinLock() + // Separates Lock and LastUlid into different cache lines to prevent "false sharing" + // x64 has 64-byte cache lines; ARM64 (e.g., Apple Silicon) has 128-byte cache lines + [StructLayout(LayoutKind.Explicit)] + private class State { - // Hot-path - if (Interlocked.CompareExchange(ref _lastUlidLock, 1, 0) == 0) - { - return; - } - - // Spin until the lock is acquired - var spinner = new SpinWait(); - while (Interlocked.CompareExchange(ref _lastUlidLock, 1, 0) == 1) - { - spinner.SpinOnce(); - } + // Cache Line 1 + [FieldOffset(16)] public LowLatencyLock Lock; + + // Cache Line 2(ARM64)/3(x64) +#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value + [FieldOffset(16+128)] public ulong LastUlidPart0; + [FieldOffset(16+128+8)] public ulong LastUlidPart1; +#pragma warning restore CS0649 // Field is never assigned to, and will always have its default value } + private static readonly State _state = new(); + #if NET5_0_OR_GREATER [SkipLocalsInit] #endif @@ -207,85 +187,78 @@ private static void AcquireSpinLock() private static void FillRandom(ref byte ulidBytesRef, long timestamp, GenerationOptions options) { // Calculate offset to a random part - ref var randomPartRef = ref Unsafe.Add(ref ulidBytesRef, _ulidSizeTime); + ref var ulidBytesRandomRef = ref Unsafe.Add(ref ulidBytesRef, _ulidSizeTime); + var monotonicity = options.Monotonicity; - if (options.Monotonicity == GenerationOptions.MonotonicityOptions.NonMonotonic) + if (monotonicity == GenerationOptions.MonotonicityOptions.NonMonotonic) { options.InitialRandomSource.GetBytes( #if NETCOREAPP - MemoryMarshal.CreateSpan(ref randomPartRef, _ulidSizeRandom) + MemoryMarshal.CreateSpan(ref ulidBytesRandomRef, _ulidSizeRandom) #else - Compatibility.MemoryMarshal.CreateSpan(ref randomPartRef, _ulidSizeRandom) + Compatibility.MemoryMarshal.CreateSpan(ref ulidBytesRandomRef, _ulidSizeRandom) #endif ); return; } - ref var lastUlidRef = -#if NETCOREAPP - ref MemoryMarshal.GetArrayDataReference(_lastUlid); -#else - ref Compatibility.MemoryMarshal.GetArrayDataReference(_lastUlid); -#endif - - ref var lastRandomRef = ref Unsafe.Add(ref lastUlidRef, _ulidSizeTime); + ref var lastUlidRef = ref Unsafe.As(ref _state.LastUlidPart0); - AcquireSpinLock(); - try + using(_state.Lock.Enter()) { - // Read the last timestamp raw (from bytes 0-7 of lastUlid) + // Read the last timestamp (from bytes 0-7 of "last ULID") // Shift it to get 48 bits. - var lastTime = (long)( - BinaryPrimitives.ReadUInt64BigEndian( + var lastTime = ReverseOnLittleEndian(_state.LastUlidPart0); + lastTime >>= 16; + + // If the timestamp is bigger than the last one, generate a new ULID + if (timestamp > (long)lastTime) + { + // We work on "generated ULID", then copy it into "last ULID" + + // Generate a new random to the generated ULID + options.InitialRandomSource.GetBytes( #if NETCOREAPP - MemoryMarshal.CreateReadOnlySpan(ref lastUlidRef, sizeof(ulong)) + MemoryMarshal.CreateSpan(ref ulidBytesRandomRef, _ulidSizeRandom) #else - Compatibility.MemoryMarshal.CreateReadOnlySpan(ref lastUlidRef, sizeof(ulong)) + Compatibility.MemoryMarshal.CreateSpan(ref ulidBytesRandomRef, _ulidSizeRandom) #endif - ) >> 16 - ); + ); - // If the timestamp is the same or lesser than the last one, increment the last ULID by one - if (timestamp <= lastTime) + // Copy full generated ULID back to last ULID + Unsafe.CopyBlock(ref lastUlidRef, ref ulidBytesRef, _ulidSize); + } + else // Otherwise, increment the last ULID { - if (options.Monotonicity == GenerationOptions.MonotonicityOptions.MonotonicIncrement) + // We work on "last ULID", then copy it into "generated ULID" + + if (monotonicity == GenerationOptions.MonotonicityOptions.MonotonicIncrement) { - IncrementByOne(ref lastUlidRef); + LastUlidIncrement(0); } else { // We can use the random bytes of incomplete ULID for the random increment span var tempSpan = #if NETCOREAPP - MemoryMarshal.CreateSpan(ref randomPartRef, (int)options.Monotonicity); + MemoryMarshal.CreateSpan(ref ulidBytesRandomRef, sizeof(uint)); #else - Compatibility.MemoryMarshal.CreateSpan(ref randomPartRef, (int)options.Monotonicity); + Compatibility.MemoryMarshal.CreateSpan(ref ulidBytesRandomRef, sizeof(uint)); #endif - options.IncrementRandomSource.GetBytes(tempSpan); - IncrementByByteSpan(ref lastUlidRef, tempSpan); + options.IncrementRandomSource.GetBytes(tempSpan[..(int)monotonicity]); + var increment = BinaryPrimitives.ReadUInt32LittleEndian(tempSpan); + + // The tempSpan may contain garbage, so mask that out + var totalBitsToKeep = (int)monotonicity * 8; + var mask = (uint)((1UL << totalBitsToKeep) - 1); + increment &= mask; + + LastUlidIncrement(increment); } - } - else // Otherwise, generate a new ULID - { - // Copy timestamp from the incomplete ULID - Unsafe.CopyBlock(ref lastUlidRef, ref ulidBytesRef, _ulidSizeTime); - // Generate a new random to the last ULID - options.InitialRandomSource.GetBytes( -#if NETCOREAPP - MemoryMarshal.CreateSpan(ref lastRandomRef, _ulidSizeRandom) -#else - Compatibility.MemoryMarshal.CreateSpan(ref lastRandomRef, _ulidSizeRandom) -#endif - ); + // Copy full last ULID back to generated ULID + Unsafe.CopyBlock(ref ulidBytesRef, ref lastUlidRef, _ulidSize); } - - Unsafe.CopyBlock(ref ulidBytesRef, ref lastUlidRef, _ulidSize); - } - finally - { - // Release the spinlock - Volatile.Write(ref _lastUlidLock, 0); } } @@ -297,77 +270,24 @@ private static void FillRandom(ref byte ulidBytesRef, long timestamp, Generation #else [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif - private static void IncrementByOne(ref byte buffer) - { - const int lastIdx = _ulidSize - 1; - - ushort carry = 1; - ref var currentRef = ref Unsafe.Add(ref buffer, lastIdx); - - for (var i = lastIdx; i >= 0; i--) - { - var val = (ushort)(currentRef + carry); - currentRef = (byte)val; // Implicit & 0xFF - carry = (ushort)(val >> 8); - - if (carry == 0) - { - return; - } - - currentRef = ref Unsafe.Subtract(ref currentRef, 1); - } - - throw new OverflowException("Addition resulted in a value larger than the target span's capacity."); - } - -#if NET5_0_OR_GREATER - [SkipLocalsInit] -#endif -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - private static void IncrementByByteSpan(ref byte targetRef, ReadOnlySpan source) + private static void LastUlidIncrement(uint addition) { - ushort carry = 1; - ushort sum; - var lengthDifference = _ulidSize - source.Length; + var increment = (ulong)addition + 1; // carry = 1 is built-in + var part1 = ReverseOnLittleEndian(_state.LastUlidPart1); + var newPart1 = part1 + increment; + _state.LastUlidPart1 = ReverseOnLittleEndian(newPart1); - if (source.Length != 0) + if (newPart1 >= part1) { - for (var i = _ulidSize - 1; i >= lengthDifference; --i) - { - var sourceIdx = i - lengthDifference; - - ref var targetByteRef = ref Unsafe.Add(ref targetRef, i); - var byteFromSource = source[sourceIdx]; - - sum = (ushort)(targetByteRef + byteFromSource + carry); - targetByteRef = (byte)sum; // Implicit & 0xFF - carry = (byte)(sum >> 8); - } - - if (carry == 0) - { - return; - } + return; } - for (var i = lengthDifference - 1; i >= 0; --i) + var part0 = ReverseOnLittleEndian(_state.LastUlidPart0); + part0++; + _state.LastUlidPart0 = ReverseOnLittleEndian(part0); + if (part0 == 0) { - ref var targetByteRef = ref Unsafe.Add(ref targetRef, i); - sum = (ushort)(targetByteRef + carry); - targetByteRef = (byte)sum; // Implicit & 0xFF - carry = (ushort)(sum >> 8); - - if (carry == 0) - { - return; - } + throw new OverflowException("Addition resulted in a ULID value larger than the absolute maximum ULID value."); } - - throw new OverflowException("Addition resulted in a value larger than the target span's capacity."); } } \ No newline at end of file diff --git a/src/ByteAether.Ulid/Ulid.String.cs b/src/ByteAether.Ulid/Ulid.String.cs index f186911..5cedc47 100644 --- a/src/ByteAether.Ulid/Ulid.String.cs +++ b/src/ByteAether.Ulid/Ulid.String.cs @@ -212,7 +212,7 @@ private static unsafe Ulid ParseCore(ReadOnlySpan input) | (r02 << 1) | (r03 >> 4); - Unsafe.WriteUnaligned(ref Unsafe.Add(ref ulidRef, 0), BinaryPrimitives.ReverseEndianness(block1)); + Unsafe.WriteUnaligned(ref Unsafe.Add(ref ulidRef, 0), ReverseOnLittleEndian(block1)); // Second block - ulong 64 bits ulong r04 = Unsafe.Add(ref tableRef, pBytes[14 * stepSize]); @@ -243,7 +243,7 @@ private static unsafe Ulid ParseCore(ReadOnlySpan input) | (r14 << 5) | r15; - Unsafe.WriteUnaligned(ref Unsafe.Add(ref ulidRef, 8), BinaryPrimitives.ReverseEndianness(block2)); + Unsafe.WriteUnaligned(ref Unsafe.Add(ref ulidRef, 8), ReverseOnLittleEndian(block2)); } return result; @@ -429,7 +429,7 @@ private void Fill(Span span, T[] map) where T: unmanaged span[3] = map[((_t1 & 0x7) << 2) | (_t2 >> 6)]; // 00[11111111][11111|111][11|111111][11111111][11111111][11111111] span[2] = map[_t1 >> 3]; // 00[11111111][|11111|111][11111111][11111111][11111111][11111111] span[1] = map[_t0 & 0x1F]; // 00[111|11111|][11111111][11111111][11111111][11111111][11111111] - span[0] = map[_t0 >> 5]; // |00[111|11111][11111111][11111111][11111111][11111111][11111111] + span[0] = map[_t0 >> 5]; // |00[111|11111][11111111][11111111][11111111][11111111][11111111] } /// diff --git a/src/ByteAether.Ulid2/Ulid.Util.cs b/src/ByteAether.Ulid/Ulid.Util.cs similarity index 88% rename from src/ByteAether.Ulid2/Ulid.Util.cs rename to src/ByteAether.Ulid/Ulid.Util.cs index 36f1fd2..f3eef03 100644 --- a/src/ByteAether.Ulid2/Ulid.Util.cs +++ b/src/ByteAether.Ulid/Ulid.Util.cs @@ -1,7 +1,7 @@ using System.Buffers.Binary; using System.Runtime.CompilerServices; -namespace ByteAether.Ulid2; +namespace ByteAether.Ulid; public readonly partial struct Ulid { diff --git a/src/ByteAether.Ulid2/ByteAether.Ulid2.csproj b/src/ByteAether.Ulid2/ByteAether.Ulid2.csproj deleted file mode 100644 index dc56f79..0000000 --- a/src/ByteAether.Ulid2/ByteAether.Ulid2.csproj +++ /dev/null @@ -1,63 +0,0 @@ - - - - net10.0;net9.0;net8.0;net7.0;net6.0;net5.0;netstandard2.1;netstandard2.0 - latest - library - - enable - enable - true - true - - true - true - true - - true - true - snupkg - - ByteAether.Ulid - High-Performance ULID for .NET - A fast, spec-compliant ULID (Universally Unique Lexicographically Sortable Identifier) implementation for modern .NET. Optimized for database primary keys, Native AOT, zero-allocation, GUID interoperability, and high-performance serialization. - Joonatan Uusväli - ByteAether - © $(Authors), $(Company). All rights reserved. - - https://github.com/ByteAether/Ulid - git - - ByteAether.Ulid2 - ulid;id;uuid;guid;id-generator;unique-id;aot-ready;efcore;dotnet;csharp - $(RepositoryUrl)/tree/v$(PackageVersion) - See $(RepositoryUrl)/releases/tag/v$(PackageVersion) - PACKAGE.md - MIT - logo.png - - - - - - - - - - - - - - True - \ - - - True - \ - - - True - \ - - - - diff --git a/src/ByteAether.Ulid2/Compatibility/Index.cs b/src/ByteAether.Ulid2/Compatibility/Index.cs deleted file mode 100644 index 8626651..0000000 --- a/src/ByteAether.Ulid2/Compatibility/Index.cs +++ /dev/null @@ -1,176 +0,0 @@ -#if NETSTANDARD2_0 - -// ReSharper disable All -#pragma warning disable -// https://github.com/dotnet/runtime/blob/v9.0.0/src/libraries/System.Private.CoreLib/src/System/Index.cs - -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; - -namespace System -{ - /// Represent a type can be used to index a collection either from the start or the end. - /// - /// Index is used by the C# compiler to support the new index syntax - /// - /// int[] someArray = new int[5] { 1, 2, 3, 4, 5 } ; - /// int lastElement = someArray[^1]; // lastElement = 5 - /// - /// -#if SYSTEM_PRIVATE_CORELIB || MICROSOFT_BCL_MEMORY - public -#else - internal -#endif - readonly struct Index : IEquatable - { - private readonly int _value; - - /// Construct an Index using a value and indicating if the index is from the start or from the end. - /// The index value. it has to be zero or positive number. - /// Indicating if the index is from the start or from the end. - /// - /// If the Index constructed from the end, index value 1 means pointing at the last element and index value 0 means pointing at beyond last element. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Index(int value, bool fromEnd = false) - { - if (value < 0) - { - ThrowValueArgumentOutOfRange_NeedNonNegNumException(); - } - - if (fromEnd) - _value = ~value; - else - _value = value; - } - - // The following private constructors mainly created for perf reason to avoid the checks - private Index(int value) - { - _value = value; - } - - /// Create an Index pointing at first element. - public static Index Start => new Index(0); - - /// Create an Index pointing at beyond last element. - public static Index End => new Index(~0); - - /// Create an Index from the start at the position indicated by the value. - /// The index value from the start. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Index FromStart(int value) - { - if (value < 0) - { - ThrowValueArgumentOutOfRange_NeedNonNegNumException(); - } - - return new Index(value); - } - - /// Create an Index from the end at the position indicated by the value. - /// The index value from the end. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Index FromEnd(int value) - { - if (value < 0) - { - ThrowValueArgumentOutOfRange_NeedNonNegNumException(); - } - - return new Index(~value); - } - - /// Returns the index value. - public int Value - { - get - { - if (_value < 0) - return ~_value; - else - return _value; - } - } - - /// Indicates whether the index is from the start or the end. - public bool IsFromEnd => _value < 0; - - /// Calculate the offset from the start using the giving collection length. - /// The length of the collection that the Index will be used with. length has to be a positive value - /// - /// For performance reason, we don't validate the input length parameter and the returned offset value against negative values. - /// we don't validate either the returned offset is greater than the input length. - /// It is expected Index will be used with collections which always have non negative length/count. If the returned offset is negative and - /// then used to index a collection will get out of range exception which will be same affect as the validation. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int GetOffset(int length) - { - int offset = _value; - if (IsFromEnd) - { - // offset = length - (~value) - // offset = length + (~(~value) + 1) - // offset = length + value + 1 - - offset += length + 1; - } - return offset; - } - - /// Indicates whether the current Index object is equal to another object of the same type. - /// An object to compare with this object - public override bool Equals([NotNullWhen(true)] object? value) => value is Index && _value == ((Index)value)._value; - - /// Indicates whether the current Index object is equal to another Index object. - /// An object to compare with this object - public bool Equals(Index other) => _value == other._value; - - /// Returns the hash code for this instance. - public override int GetHashCode() => _value; - - /// Converts integer number to an Index. - public static implicit operator Index(int value) => FromStart(value); - - /// Converts the value of the current Index object to its equivalent string representation. - public override string ToString() - { - if (IsFromEnd) - return ToStringFromEnd(); - - return ((uint)Value).ToString(); - } - - private static void ThrowValueArgumentOutOfRange_NeedNonNegNumException() - { -#if SYSTEM_PRIVATE_CORELIB - throw new ArgumentOutOfRangeException("value", SR.ArgumentOutOfRange_NeedNonNegNum); -#else - throw new ArgumentOutOfRangeException("value", "value must be non-negative"); -#endif - } - - private string ToStringFromEnd() - { -#if (!NETSTANDARD2_0 && !NETFRAMEWORK) - Span span = stackalloc char[11]; // 1 for ^ and 10 for longest possible uint value - bool formatted = ((uint)Value).TryFormat(span.Slice(1), out int charsWritten); - Debug.Assert(formatted); - span[0] = '^'; - return new string(span.Slice(0, charsWritten + 1)); -#else - return '^' + Value.ToString(); -#endif - } - } -} - -#endif \ No newline at end of file diff --git a/src/ByteAether.Ulid2/Compatibility/IsExternalInit.cs b/src/ByteAether.Ulid2/Compatibility/IsExternalInit.cs deleted file mode 100644 index c90ddb0..0000000 --- a/src/ByteAether.Ulid2/Compatibility/IsExternalInit.cs +++ /dev/null @@ -1,28 +0,0 @@ -#if !NET5_0_OR_GREATER - -// ReSharper disable All -#pragma warning disable -// https://github.com/dotnet/runtime/blob/v9.0.0/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/IsExternalInit.cs - -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.ComponentModel; - -namespace System.Runtime.CompilerServices -{ - /// - /// Reserved to be used by the compiler for tracking metadata. - /// This class should not be used by developers in source code. - /// - [EditorBrowsable(EditorBrowsableState.Never)] -#if SYSTEM_PRIVATE_CORELIB - public -#else - internal -#endif - static class IsExternalInit - { - } -} -#endif \ No newline at end of file diff --git a/src/ByteAether.Ulid2/Compatibility/MemoryMarshal.cs b/src/ByteAether.Ulid2/Compatibility/MemoryMarshal.cs deleted file mode 100644 index ffe520d..0000000 --- a/src/ByteAether.Ulid2/Compatibility/MemoryMarshal.cs +++ /dev/null @@ -1,30 +0,0 @@ -#if NETSTANDARD -using System.Runtime.CompilerServices; - -// ReSharper disable All -#pragma warning disable - -// https://github.com/dotnet/runtime/blob/8d796d8e60a5236cbd5f113ead1d3831064cdba1/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/MemoryMarshal.cs#L226 - -namespace ByteAether.Ulid2.Compatibility; - -public static class MemoryMarshal -{ - /// - /// Creates a new span over a portion of a regular managed object. This can be useful - /// if part of a managed object represents a "fixed array." This is dangerous because the - /// is not checked. - /// - /// A reference to data. - /// The number of elements the memory contains. - /// A span representing the specified reference and length. - /// - /// This method should be used with caution. It is dangerous because the length argument is not checked. - /// Even though the ref is annotated as scoped, it will be stored into the returned span, and the lifetime - /// of the returned span will not be validated for safety, even by span-aware languages. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public unsafe static Span CreateSpan(scoped ref T reference, int length) => - new Span(Unsafe.AsPointer(ref reference), length); -} -#endif \ No newline at end of file diff --git a/src/ByteAether.Ulid2/Compatibility/NullableAttributes.cs b/src/ByteAether.Ulid2/Compatibility/NullableAttributes.cs deleted file mode 100644 index 8d14d62..0000000 --- a/src/ByteAether.Ulid2/Compatibility/NullableAttributes.cs +++ /dev/null @@ -1,208 +0,0 @@ -#if NETSTANDARD2_0 - -// ReSharper disable All -#pragma warning disable -// https://github.com/dotnet/runtime/blob/v9.0.0/src/libraries/System.Private.CoreLib/src/System/Diagnostics/CodeAnalysis/NullableAttributes.cs - -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace System.Diagnostics.CodeAnalysis -{ -#if !NETSTANDARD2_1 - /// Specifies that null is allowed as an input even if the corresponding type disallows it. - [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] -#if SYSTEM_PRIVATE_CORELIB - public -#else - internal -#endif - sealed class AllowNullAttribute : Attribute - { } - - /// Specifies that null is disallowed as an input even if the corresponding type allows it. - [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] -#if SYSTEM_PRIVATE_CORELIB - public -#else - internal -#endif - sealed class DisallowNullAttribute : Attribute - { } - - /// Specifies that an output may be null even if the corresponding type disallows it. - [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] -#if SYSTEM_PRIVATE_CORELIB - public -#else - internal -#endif - sealed class MaybeNullAttribute : Attribute - { } - - /// Specifies that an output will not be null even if the corresponding type allows it. Specifies that an input argument was not null when the call returns. - [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] -#if SYSTEM_PRIVATE_CORELIB - public -#else - internal -#endif - sealed class NotNullAttribute : Attribute - { } - - /// Specifies that when a method returns , the parameter may be null even if the corresponding type disallows it. - [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] -#if SYSTEM_PRIVATE_CORELIB - public -#else - internal -#endif - sealed class MaybeNullWhenAttribute : Attribute - { - /// Initializes the attribute with the specified return value condition. - /// - /// The return value condition. If the method returns this value, the associated parameter may be null. - /// - public MaybeNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; - - /// Gets the return value condition. - public bool ReturnValue { get; } - } - - /// Specifies that when a method returns , the parameter will not be null even if the corresponding type allows it. - [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] -#if SYSTEM_PRIVATE_CORELIB - public -#else - internal -#endif - sealed class NotNullWhenAttribute : Attribute - { - /// Initializes the attribute with the specified return value condition. - /// - /// The return value condition. If the method returns this value, the associated parameter will not be null. - /// - public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; - - /// Gets the return value condition. - public bool ReturnValue { get; } - } - - /// Specifies that the output will be non-null if the named parameter is non-null. - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)] -#if SYSTEM_PRIVATE_CORELIB - public -#else - internal -#endif - sealed class NotNullIfNotNullAttribute : Attribute - { - /// Initializes the attribute with the associated parameter name. - /// - /// The associated parameter name. The output will be non-null if the argument to the parameter specified is non-null. - /// - public NotNullIfNotNullAttribute(string parameterName) => ParameterName = parameterName; - - /// Gets the associated parameter name. - public string ParameterName { get; } - } - - /// Applied to a method that will never return under any circumstance. - [AttributeUsage(AttributeTargets.Method, Inherited = false)] -#if SYSTEM_PRIVATE_CORELIB - public -#else - internal -#endif - sealed class DoesNotReturnAttribute : Attribute - { } - - /// Specifies that the method will not return if the associated Boolean parameter is passed the specified value. - [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] -#if SYSTEM_PRIVATE_CORELIB - public -#else - internal -#endif - sealed class DoesNotReturnIfAttribute : Attribute - { - /// Initializes the attribute with the specified parameter value. - /// - /// The condition parameter value. Code after the method will be considered unreachable by diagnostics if the argument to - /// the associated parameter matches this value. - /// - public DoesNotReturnIfAttribute(bool parameterValue) => ParameterValue = parameterValue; - - /// Gets the condition parameter value. - public bool ParameterValue { get; } - } -#endif - - /// Specifies that the method or property will ensure that the listed field and property members have not-null values. - [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] -#if SYSTEM_PRIVATE_CORELIB - public -#else - internal -#endif - sealed class MemberNotNullAttribute : Attribute - { - /// Initializes the attribute with a field or property member. - /// - /// The field or property member that is promised to be not-null. - /// - public MemberNotNullAttribute(string member) => Members = [member]; - - /// Initializes the attribute with the list of field and property members. - /// - /// The list of field and property members that are promised to be not-null. - /// - public MemberNotNullAttribute(params string[] members) => Members = members; - - /// Gets field or property member names. - public string[] Members { get; } - } - - /// Specifies that the method or property will ensure that the listed field and property members have not-null values when returning with the specified return value condition. - [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] -#if SYSTEM_PRIVATE_CORELIB - public -#else - internal -#endif - sealed class MemberNotNullWhenAttribute : Attribute - { - /// Initializes the attribute with the specified return value condition and a field or property member. - /// - /// The return value condition. If the method returns this value, the associated field or property member will not be null. - /// - /// - /// The field or property member that is promised to be not-null. - /// - public MemberNotNullWhenAttribute(bool returnValue, string member) - { - ReturnValue = returnValue; - Members = [member]; - } - - /// Initializes the attribute with the specified return value condition and list of field and property members. - /// - /// The return value condition. If the method returns this value, the associated field and property members will not be null. - /// - /// - /// The list of field and property members that are promised to be not-null. - /// - public MemberNotNullWhenAttribute(bool returnValue, params string[] members) - { - ReturnValue = returnValue; - Members = members; - } - - /// Gets the return value condition. - public bool ReturnValue { get; } - - /// Gets field or property member names. - public string[] Members { get; } - } -} -#endif \ No newline at end of file diff --git a/src/ByteAether.Ulid2/Compatibility/RandomNumberGeneratorExtensions.cs b/src/ByteAether.Ulid2/Compatibility/RandomNumberGeneratorExtensions.cs deleted file mode 100644 index 81a3445..0000000 --- a/src/ByteAether.Ulid2/Compatibility/RandomNumberGeneratorExtensions.cs +++ /dev/null @@ -1,29 +0,0 @@ -#if NETSTANDARD2_0 -using System.Buffers; -using System.Runtime.CompilerServices; -using System.Security.Cryptography; - -// ReSharper disable once CheckNamespace -namespace ByteAether.Ulid2; - -internal static class RandomNumberGeneratorExtensions -{ - // In NetStandard 2.0, RandomNumberGenerator.GetBytes() does not support Span overloads. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void GetBytes(this RandomNumberGenerator rng, Span buffer) - { - var rndInc = ArrayPool.Shared.Rent(buffer.Length); - rng.GetBytes(rndInc, 0, buffer.Length); - new ReadOnlySpan(rndInc, 0, buffer.Length).CopyTo(buffer); - } - - // In NetStandard 2.0, Random.NextBytes() does not support Span overloads. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void NextBytes(this Random rng, Span buffer) - { - var rndInc = ArrayPool.Shared.Rent(buffer.Length); - rng.NextBytes(rndInc); - new ReadOnlySpan(rndInc, 0, buffer.Length).CopyTo(buffer); - } -} -#endif \ No newline at end of file diff --git a/src/ByteAether.Ulid2/Compatibility/Range.cs b/src/ByteAether.Ulid2/Compatibility/Range.cs deleted file mode 100644 index c99e735..0000000 --- a/src/ByteAether.Ulid2/Compatibility/Range.cs +++ /dev/null @@ -1,141 +0,0 @@ -#if NETSTANDARD2_0 - -// ReSharper disable All -#pragma warning disable -// https://github.com/dotnet/runtime/blob/v9.0.0/src/libraries/System.Private.CoreLib/src/System/Range.cs - -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; - -namespace System -{ - /// Represent a range has start and end indexes. - /// - /// Range is used by the C# compiler to support the range syntax. - /// - /// int[] someArray = new int[5] { 1, 2, 3, 4, 5 }; - /// int[] subArray1 = someArray[0..2]; // { 1, 2 } - /// int[] subArray2 = someArray[1..^0]; // { 2, 3, 4, 5 } - /// - /// -#if SYSTEM_PRIVATE_CORELIB || MICROSOFT_BCL_MEMORY - public -#else - internal -#endif - readonly struct Range : IEquatable - { - /// Represent the inclusive start index of the Range. - public Index Start { get; } - - /// Represent the exclusive end index of the Range. - public Index End { get; } - - /// Construct a Range object using the start and end indexes. - /// Represent the inclusive start index of the range. - /// Represent the exclusive end index of the range. - public Range(Index start, Index end) - { - Start = start; - End = end; - } - - /// Indicates whether the current Range object is equal to another object of the same type. - /// An object to compare with this object - public override bool Equals([NotNullWhen(true)] object? value) => - value is Range r && - r.Start.Equals(Start) && - r.End.Equals(End); - - /// Indicates whether the current Range object is equal to another Range object. - /// An object to compare with this object - public bool Equals(Range other) => other.Start.Equals(Start) && other.End.Equals(End); - - /// Returns the hash code for this instance. - public override int GetHashCode() - { -#if (!NETSTANDARD2_0 && !NETFRAMEWORK) - return HashCode.Combine(Start.GetHashCode(), End.GetHashCode()); -#else - //https://github.com/dotnet/runtime/blob/v9.0.0/src/libraries/System.Private.CoreLib/src/System/Numerics/Hashing/HashHelpers.cs - var h1 = Start.GetHashCode(); - var h2 = End.GetHashCode(); - // RyuJIT optimizes this to use the ROL instruction - // Related GitHub pull request: https://github.com/dotnet/coreclr/pull/1830 - uint rol5 = ((uint)h1 << 5) | ((uint)h1 >> 27); - return ((int)rol5 + h1) ^ h2; -#endif - } - - /// Converts the value of the current Range object to its equivalent string representation. - public override string ToString() - { -#if (!NETSTANDARD2_0 && !NETFRAMEWORK) - Span span = stackalloc char[2 + (2 * 11)]; // 2 for "..", then for each index 1 for '^' and 10 for longest possible uint - int pos = 0; - - if (Start.IsFromEnd) - { - span[0] = '^'; - pos = 1; - } - bool formatted = ((uint)Start.Value).TryFormat(span.Slice(pos), out int charsWritten); - Debug.Assert(formatted); - pos += charsWritten; - - span[pos++] = '.'; - span[pos++] = '.'; - - if (End.IsFromEnd) - { - span[pos++] = '^'; - } - formatted = ((uint)End.Value).TryFormat(span.Slice(pos), out charsWritten); - Debug.Assert(formatted); - pos += charsWritten; - - return new string(span.Slice(0, pos)); -#else - return Start.ToString() + ".." + End.ToString(); -#endif - } - - /// Create a Range object starting from start index to the end of the collection. - public static Range StartAt(Index start) => new Range(start, Index.End); - - /// Create a Range object starting from first element in the collection to the end Index. - public static Range EndAt(Index end) => new Range(Index.Start, end); - - /// Create a Range object starting from first element to the end. - public static Range All => new Range(Index.Start, Index.End); - - /// Calculate the start offset and length of range object using a collection length. - /// The length of the collection that the range will be used with. length has to be a positive value. - /// - /// For performance reason, we don't validate the input length parameter against negative values. - /// It is expected Range will be used with collections which always have non negative length/count. - /// We validate the range is inside the length scope though. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public (int Offset, int Length) GetOffsetAndLength(int length) - { - int start = Start.GetOffset(length); - int end = End.GetOffset(length); - - if ((uint)end > (uint)length || (uint)start > (uint)end) - { - ThrowArgumentOutOfRangeException(); - } - - return (start, end - start); - } - - private static void ThrowArgumentOutOfRangeException() - { - throw new ArgumentOutOfRangeException("length"); - } - } -} - -#endif \ No newline at end of file diff --git a/src/ByteAether.Ulid2/CryptographicallySecureRandomProvider.cs b/src/ByteAether.Ulid2/CryptographicallySecureRandomProvider.cs deleted file mode 100644 index 908d692..0000000 --- a/src/ByteAether.Ulid2/CryptographicallySecureRandomProvider.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Runtime.CompilerServices; -using System.Security.Cryptography; - -namespace ByteAether.Ulid2; - -/// -/// Provides cryptographically secure random number generation functionality.
-/// Implements the interface to generate random bytes -/// securely using a system-provided implementation of the . -///
-public readonly struct CryptographicallySecureRandomProvider : IRandomProvider -{ - private static readonly RandomNumberGenerator _rng = RandomNumberGenerator.Create(); - - /// -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - public void GetBytes(Span buffer) => _rng.GetBytes(buffer); -} \ No newline at end of file diff --git a/src/ByteAether.Ulid2/IRandomProvider.cs b/src/ByteAether.Ulid2/IRandomProvider.cs deleted file mode 100644 index 584ba2e..0000000 --- a/src/ByteAether.Ulid2/IRandomProvider.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace ByteAether.Ulid2; - -/// -/// Implementations of this interface dictate the method by which randomness -/// is produced. -/// -public interface IRandomProvider -{ - /// - /// Fills the specified buffer with random byte values.
- /// The implementation determines the source of randomness. - ///
- /// - /// The span of bytes to be filled with random data. The length of - /// the buffer determines how many bytes are generated. - /// - public void GetBytes(Span buffer); -} \ No newline at end of file diff --git a/src/ByteAether.Ulid2/PACKAGE.md b/src/ByteAether.Ulid2/PACKAGE.md deleted file mode 100644 index 0070c8e..0000000 --- a/src/ByteAether.Ulid2/PACKAGE.md +++ /dev/null @@ -1,205 +0,0 @@ -# ULID -*from ByteAether* - -[![License](https://img.shields.io/github/license/ByteAether/Ulid?logo=github&label=License)](https://github.com/ByteAether/Ulid/blob/main/LICENSE) -[![NuGet Version](https://img.shields.io/nuget/v/ByteAether.Ulid?logo=nuget&label=Version)](https://www.nuget.org/packages/ByteAether.Ulid/) -[![NuGet Downloads](https://img.shields.io/nuget/dt/ByteAether.Ulid?logo=nuget&label=Downloads)](https://www.nuget.org/packages/ByteAether.Ulid/) -[![GitHub Build Status](https://img.shields.io/github/actions/workflow/status/ByteAether/Ulid/build-and-test.yml?logo=github&label=Build%20%26%20Test)](https://github.com/ByteAether/Ulid/actions/workflows/build-and-test.yml) -[![GitHub Security](https://img.shields.io/github/actions/workflow/status/ByteAether/Ulid/codeql.yml?logo=github&label=Security%20Validation)](https://github.com/ByteAether/Ulid/actions/workflows/codeql.yml) - -A high-performance, fully compliant .NET implementation of ULIDs (Universally Unique Lexicographically Sortable Identifiers), adhering to the [official ULID specification](https://github.com/ulid/spec). - -For more detailed documentation, visit our [GitHub repository](https://github.com/ByteAether/Ulid). - -## Features - -![.NET AOT Ready](https://img.shields.io/badge/.NET-AOT_Ready-blue) -![.NET 10.0](https://img.shields.io/badge/.NET-10.0-brightgreen) -![.NET 9.0](https://img.shields.io/badge/.NET-9.0-brightgreen) -![.NET 8.0](https://img.shields.io/badge/.NET-8.0-brightgreen) -![.NET 7.0](https://img.shields.io/badge/.NET-7.0-green) -![.NET 6.0](https://img.shields.io/badge/.NET-6.0-green) -![.NET 5.0](https://img.shields.io/badge/.NET-5.0-yellow) -![.NET Standard 2.1](https://img.shields.io/badge/.NET-Standard_2.1-yellow) -![.NET Standard 2.0](https://img.shields.io/badge/.NET-Standard_2.0-green) - -- **Universally Unique**: Ensures global uniqueness across systems. -- **Sortable**: Lexicographically ordered for time-based sorting. -- **Lock-Free Synchronization**: Monotonic generation utilizes a high-performance, **lock-free compare-and-exchange (CAS)** approach. -- **Specification-Compliant**: Fully adheres to the ULID specification. -- **Interoperable**: Includes conversion methods to and from GUIDs, [Crockford's Base32](https://www.crockford.com/base32.html) strings, and byte arrays. -- **Ahead-of-Time (AoT) Compilation Compatible**: Fully compatible with AoT compilation for improved startup performance and smaller binary sizes. -- **Error-Free Generation**: Prevents `OverflowException` by incrementing the timestamp component when the random part overflows, ensuring continuous unique ULID generation. - -## Installation - -Install the latest stable package via NuGet: -```sh -dotnet add package ByteAether.Ulid -``` -To install a specific [preview version](https://www.nuget.org/packages/ByteAether.Ulid/absoluteLatest), use the `--version` option: -```sh -dotnet add package ByteAether.Ulid --version -``` - -## Usage - -Here is a basic example of how to use the ULID implementation: -```csharp -using System; -using ByteAether.Ulid; - -// Create a new ULID -var ulid = Ulid.New(); - -// Convert to byte array and back -byte[] byteArray = ulid.ToByteArray(); -var ulidFromByteArray = Ulid.New(byteArray); - -// Convert to GUID and back -Guid guid = ulid.ToGuid(); -var ulidFromGuid = Ulid.New(guid); - -// Convert to string and back -string ulidString = ulid.ToString(); -var ulidFromString = Ulid.Parse(ulidString); - -Console.WriteLine($"ULID: {ulid}, GUID: {guid}, String: {ulidString}"); -``` - -## API - -The `Ulid` implementation provides the following properties and methods: - -### Creation - -- `Ulid.New(GenerationOptions? options = null)`\ - Generates a new ULID using default generation options. Accepts an optional `GenerationOptions` parameter to customize the generation behavior. -- `Ulid.New(DateTimeOffset dateTimeOffset, GenerationOptions? options = null)`\ - Generates a new ULID using the specified `DateTimeOffset` and default generation options. Accepts an optional `GenerationOptions` parameter to customize the generation behavior. -- `Ulid.New(long timestamp, GenerationOptions? options = null)`\ - Generates a new ULID using the specified Unix timestamp in milliseconds (`long`) and default generation options. Accepts an optional `GenerationOptions` parameter to customize the generation behavior. -- `Ulid.New(DateTimeOffset dateTimeOffset, ReadOnlySpan random)`\ - Generates a new ULID using the specified `DateTimeOffset` and a pre-existing random byte array. -- `Ulid.New(long timestamp, ReadOnlySpan random)`\ - Generates a new ULID using the specified Unix timestamp in milliseconds (`long`) and a pre-existing random byte array. -- `Ulid.New(ReadOnlySpan bytes)`\ - Creates a ULID from an existing byte array. -- `Ulid.New(Guid guid)`\ - Create from existing `Guid`. -- `Ulid.MinAt(DateTimeOffset datetime)`\ - Creates the minimum possible ULID value for the specified `DateTimeOffset`. -- `Ulid.MinAt(long timestamp)`\ - Creates the minimum possible ULID value for the specified Unix timestamp in milliseconds (`long`). -- `Ulid.MaxAt(DateTimeOffset datetime)`\ - Creates the maximum possible ULID value for the specified `DateTimeOffset`. -- `Ulid.MaxAt(long timestamp)`\ - Creates the maximum possible ULID value for the specified Unix timestamp in milliseconds (`long`). - -### Checking Validity - -- `Ulid.IsValid(string ulidString)`\ - Validates if the given string is a valid ULID. -- `Ulid.IsValid(ReadOnlySpan ulidString)`\ - Validates if the given span of characters is a valid ULID. -- `Ulid.IsValid(ReadOnlySpan ulidBytes)`\ - Validates if the given byte array represents a valid ULID. - -### Parsing - -- `Ulid.Parse(ReadOnlySpan chars, IFormatProvider? provider = null)`\ - Parses a ULID from a character span in canonical format. The `IFormatProvider` is ignored. -- `Ulid.TryParse(ReadOnlySpan s, IFormatProvider? provider, out Ulid result)`\ - Tries to parse a ULID from a character span in canonical format. Returns `true` if successful. -- `Ulid.Parse(string s, IFormatProvider? provider = null)`\ - Parses a ULID from a string in canonical format. The `IFormatProvider` is ignored. -- `Ulid.TryParse(string? s, IFormatProvider? provider, out Ulid result)`\ - Tries to parse a ULID from a string in canonical format. Returns `true` if successful. - -### Properties - -- `Ulid.MinValue`\ - Represents an empty ULID, equivalent to `default(Ulid)` and `Ulid.New(new byte[16])`. -- `Ulid.MaxValue`\ - Represents the maximum possible value for a ULID (all bytes set to `0xFF`). -- `Ulid.Empty`\ - Alias for `Ulid.MinValue`. -- `Ulid.DefaultGenerationOptions`\ - Default configuration for ULID generation when no options are provided by the `Ulid.New(...)` call. -- `.Time`\ - Gets the timestamp component of the ULID as a `DateTimeOffset`. -- `.TimeBytes`\ - Gets the time component of the ULID as a `ReadOnlySpan`. -- `.Random`\ - Gets the random component of the ULID as a `ReadOnlySpan`. - -### Conversions & Interoperability - -- `.AsByteSpan()`\ - Provides a `ReadOnlySpan` representing the contents of the ULID. -- `.ToByteArray()`\ - Converts the ULID to a byte array. -- `.ToGuid()`\ - Converts the ULID to a `Guid`. -- `.ToString(string? format = null, IFormatProvider? formatProvider = null)`\ - Converts the ULID to a canonical string representation. Format arguments are ignored. -- Provides implicit operators to and from `Guid` and `string`. - -### Comparison Operators & .NET Interfaces - -- Supports all comparison operators:\ - `==`, `!=`, `<`, `<=`, `>`, `>=`. -- Implements standard comparison and equality methods:\ - `CompareTo`, `Equals`, `GetHashCode`. -- Implements the following .NET standard interfaces:\ - `IMinMaxValue`, `IEquatable`, `IIEqualityComparer`, `IComparable`, `IComparable`, `IComparisonOperators`, `IFormattable`, `IParsable`, `ISpanFormattable`, `ISpanParsable`, `IUtf8SpanFormattable`, `IUtf8SpanParsable`. - -### GenerationOptions - -The `GenerationOptions` class provides detailed configuration for ULID generation, with the following key properties: - -- `Monotonicity`\ - Controls the behavior of ULID generation when multiple identifiers are created within the same millisecond. It determines whether ULIDs are strictly increasing or allow for random ordering within that millisecond. Available options include: `NonMonotonic`, `MonotonicIncrement` (default), `MonotonicRandom1Byte`, `MonotonicRandom2Byte`, `MonotonicRandom3Byte`, `MonotonicRandom4Byte`. - -- `InitialRandomSource`\ - An `IRandomProvider` for generating the random bytes of a ULID. The default `CryptographicallySecureRandomProvider` ensures robust, unpredictable ULIDs using a cryptographically secure generator. - -- `IncrementRandomSource`\ - An `IRandomProvider` that provides randomness for monotonic random increments. The default `PseudoRandomProvider` is a faster, non-cryptographically secure source optimized for this specific purpose. - -This library comes with two default `IRandomProvider` implementations: - -- **`CryptographicallySecureRandomProvider`**\ - Utilizes `System.Security.Cryptography.RandomNumberGenerator` for high-quality, cryptographically secure random data. -- **`PseudoRandomProvider`**\ - A faster, non-cryptographically secure option based on `System.Random`, ideal for performance-critical scenarios where cryptographic security is not required for random increments. - -Custom `IRandomProvider` implementations can also be created. - -## Integration with Other Libraries - -### ASP.NET Core - -Supports seamless integration as a route or query parameter with built-in `TypeConverter`. - -### System.Text.Json - -Includes a `JsonConverter` for easy serialization and deserialization. - -### Other Libraries - -Check out [README in GitHub repository](https://github.com/ByteAether/Ulid/blob/main/README.md) for examples to integrate with Entity Framework Core, Dapper, MessagePack, and Newtonsoft.Json. - -## Prior Art - -Much of this implementation is either based on or inspired by existing works. This library is standing on the shoulders of giants. - - * [NetUlid](https://github.com/ultimicro/netulid) - * [Ulid](https://github.com/Cysharp/Ulid) - * [NUlid](https://github.com/RobThree/NUlid) - * [Official ULID specification](https://github.com/ulid/spec) - * [Crockford's Base32](https://www.crockford.com/base32.html) - -## License - -This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. diff --git a/src/ByteAether.Ulid2/PseudoRandomProvider.cs b/src/ByteAether.Ulid2/PseudoRandomProvider.cs deleted file mode 100644 index 0ab114d..0000000 --- a/src/ByteAether.Ulid2/PseudoRandomProvider.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Runtime.CompilerServices; - -namespace ByteAether.Ulid2; - -/// -/// Provides a pseudo-random number generator implementation for the -/// interface using the shared instance -/// of . -/// -public readonly struct PseudoRandomProvider : IRandomProvider -{ -#if NET6_0_OR_GREATER - private static Random _rng - { -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - get => Random.Shared; - } -#else - private static readonly Random _rng = new(); -#endif - - /// -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - public void GetBytes(Span buffer) => _rng.NextBytes(buffer); -} \ No newline at end of file diff --git a/src/ByteAether.Ulid2/Ulid.Boundaries.cs b/src/ByteAether.Ulid2/Ulid.Boundaries.cs deleted file mode 100644 index c4b0773..0000000 --- a/src/ByteAether.Ulid2/Ulid.Boundaries.cs +++ /dev/null @@ -1,68 +0,0 @@ -#if NET7_0_OR_GREATER -using System.Numerics; -#endif - -namespace ByteAether.Ulid2; - -public readonly partial struct Ulid -#if NET7_0_OR_GREATER - : IMinMaxValue -#endif -{ - private static readonly byte[] _randomMin = Enumerable.Repeat((byte)0x00, _ulidSizeRandom).ToArray(); - private static readonly byte[] _randomMax = Enumerable.Repeat((byte)0xFF, _ulidSizeRandom).ToArray(); - - /// Gets the minimum value of the ULID type. - /// - /// The field is a ULID with all components set to zero. - /// It can be used as a default or placeholder value. - /// - public static Ulid MinValue { get; } = default; - - /// - /// Represents the maximum possible value for a ULID. - /// - /// - /// The field is a ULID where all byte components are set to their highest possible value (0xFF). - /// It can be used as a sentinel or boundary value in comparison operations or range validations. - /// - public static Ulid MaxValue { get; } = New(Enumerable.Repeat((byte)0xFF, _ulidSize).ToArray()); - - /// - /// Represents an empty ULID value. - /// - /// - /// The field is a ULID with all components set to zero. - /// It can be used as a default or placeholder value. - /// It is equivalent to , but is provided for clarity. - /// - public static Ulid Empty => MinValue; - - /// - /// Creates the minimum possible value for the specified timestamp. - /// - /// The timestamp used to create the minimum value. - /// The minimum value for the given timestamp. - public static Ulid MinAt(long timestamp) => New(timestamp, _randomMin); - - /// - /// Creates the minimum possible value for the specified timestamp. - /// - /// The timestamp used to create the minimum value. - /// The minimum value for the given timestamp. - public static Ulid MinAt(DateTimeOffset datetime) => New(datetime, _randomMin); - - /// - /// Creates the maximum possible value for the specified timestamp. - /// - /// The timestamp used to create the maximum value. - /// The maximum value for the given timestamp. - public static Ulid MaxAt(long timestamp) => New(timestamp, _randomMax); - - /// - /// Creates the maximum possible value for the specified timestamp. - /// - /// The timestamp used to create the maximum value. - /// The maximum value for the given timestamp. - public static Ulid MaxAt(DateTimeOffset datetime) => New(datetime, _randomMax); -} \ No newline at end of file diff --git a/src/ByteAether.Ulid2/Ulid.Comparable.cs b/src/ByteAether.Ulid2/Ulid.Comparable.cs deleted file mode 100644 index d4350d8..0000000 --- a/src/ByteAether.Ulid2/Ulid.Comparable.cs +++ /dev/null @@ -1,125 +0,0 @@ -using System.Buffers.Binary; -using System.Runtime.CompilerServices; -#if NET7_0_OR_GREATER -using System.Numerics; -#endif - -namespace ByteAether.Ulid2; -public readonly partial struct Ulid : IComparable, IComparable -#if NET7_0_OR_GREATER - , IComparisonOperators -#endif -{ - /// - /// Determines whether the value of the left ULID is less than the value of the right ULID. - /// - /// The first ULID to compare. - /// The second ULID to compare. - /// True if the value of the left ULID is less than the value of the right ULID; otherwise, false. -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - public static bool operator <(Ulid left, Ulid right) - => left.CompareTo(right) < 0; - - /// - /// Determines whether the value of the left ULID is less than or equal to the value of the right ULID. - /// - /// The first ULID to compare. - /// The second ULID to compare. - /// True if the value of the left ULID is less than or equal to the value of the right ULID; otherwise, false. -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - public static bool operator <=(Ulid left, Ulid right) - => left.CompareTo(right) <= 0; - - /// - /// Determines whether the value of the left ULID is greater than the value of the right ULID. - /// - /// The first ULID to compare. - /// The second ULID to compare. - /// True if the value of the left ULID is greater than the value of the right ULID; otherwise, false. -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - public static bool operator >(Ulid left, Ulid right) - => left.CompareTo(right) > 0; - - /// - /// Determines whether the value of the left ULID is greater than or equal to the value of the right ULID. - /// - /// The first ULID to compare. - /// The second ULID to compare. - /// True if the value of the left ULID is greater than or equal to the value of the right ULID; otherwise, false. -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - public static bool operator >=(Ulid left, Ulid right) - => left.CompareTo(right) >= 0; - - /// -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - public int CompareTo(object? obj) - { - if (obj == null) - { - return 1; - } - - if (obj is not Ulid ulid) - { - throw new ArgumentException($"The value is not an instance of {nameof(Ulid)}.", nameof(obj)); - } - - return CompareTo(ulid); - } - - /// -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - public int CompareTo(Ulid other) - => CompareToCore(this, other); - -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveOptimization)] -#endif - private static int CompareToCore(in Ulid left, in Ulid right) - { - ref var rA = ref Unsafe.As(ref Unsafe.AsRef(in left)); - ref var rB = ref Unsafe.As(ref Unsafe.AsRef(in right)); - - var a = ReverseOnLittleEndian(rA); - var b = ReverseOnLittleEndian(rB); - - if (a != b) - { - return a < b ? -1 : 1; - } - - a = ReverseOnLittleEndian(Unsafe.Add(ref rA, 1)); - b = ReverseOnLittleEndian(Unsafe.Add(ref rB, 1)); - - if (a != b) - { - return a < b ? -1 : 1; - } - - return 0; - } -} \ No newline at end of file diff --git a/src/ByteAether.Ulid2/Ulid.Equatable.cs b/src/ByteAether.Ulid2/Ulid.Equatable.cs deleted file mode 100644 index 523d96a..0000000 --- a/src/ByteAether.Ulid2/Ulid.Equatable.cs +++ /dev/null @@ -1,125 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; -#if NET7_0_OR_GREATER -using System.Numerics; -using System.Runtime.Intrinsics; -#elif NETCOREAPP3_0_OR_GREATER -using System.Runtime.Intrinsics; -using System.Runtime.Intrinsics.X86; -#endif - -namespace ByteAether.Ulid2; - -public readonly partial struct Ulid : IEquatable, IEqualityComparer -#if NET7_0_OR_GREATER - , IEqualityOperators // Keeping this here for clarity -#endif -{ - /// -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - public int GetHashCode(Ulid ulid) => ulid.GetHashCode(); - - /// -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - public bool Equals(Ulid x, Ulid y) => EqualsCore(x, y); - - /// -#if NET5_0_OR_GREATER - [SkipLocalsInit] -#endif -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - public override int GetHashCode() - { - ref var rA = ref Unsafe.As(ref Unsafe.AsRef(in this)); - return rA ^ Unsafe.Add(ref rA, 1) ^ Unsafe.Add(ref rA, 2) ^ Unsafe.Add(ref rA, 3); - } - - /// -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - public bool Equals(Ulid other) - => EqualsCore(this, other); - - /// -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - public override bool Equals([NotNullWhen(true)] object? obj) - => obj is Ulid ulid && EqualsCore(this, ulid); - - /// - /// Determines whether two specified ULIDs have the same value. - /// - /// The first ULID to compare. - /// The second ULID to compare. - /// True if the value of the left ULID is equal to the value of the right ULID; otherwise, false. - -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - public static bool operator ==(Ulid left, Ulid right) - => EqualsCore(left, right); - - /// - /// Determines whether two specified ULIDs have different values. - /// - /// The first ULID to compare. - /// The second ULID to compare. - /// True if the value of the left ULID is not equal to the value of the right ULID; otherwise, false. - public static bool operator !=(Ulid left, Ulid right) - => !EqualsCore(left, right); - -#if NET5_0_OR_GREATER - [SkipLocalsInit] -#endif -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveOptimization)] -#endif - private static bool EqualsCore(in Ulid left, in Ulid right) - { -#if NET7_0_OR_GREATER - if (Vector128.IsHardwareAccelerated) - { - var vA = Vector128.LoadUnsafe(ref Unsafe.As(ref Unsafe.AsRef(in left))); - var vB = Vector128.LoadUnsafe(ref Unsafe.As(ref Unsafe.AsRef(in right))); - return vA == vB; - } -#elif NETCOREAPP3_0_OR_GREATER - if (Sse2.IsSupported) - { - var vA = Unsafe.ReadUnaligned>(ref Unsafe.As(ref Unsafe.AsRef(in left))); - var vB = Unsafe.ReadUnaligned>(ref Unsafe.As(ref Unsafe.AsRef(in right))); - return Sse2.MoveMask(Sse2.CompareEqual(vA, vB)) == 0xFFFF; - } -#endif - - ref var rA = ref Unsafe.As(ref Unsafe.AsRef(in left)); - ref var rB = ref Unsafe.As(ref Unsafe.AsRef(in right)); - - // XOR-compare instead of 2x 64bit long compare with AND - // Branchless XOR-compare is faster (0.1787ns vs. 0.2463ns) - var xor0 = rA ^ rB; - var xor1 = Unsafe.Add(ref rA, 1) ^ Unsafe.Add(ref rB, 1); - - return (xor0 | xor1) == 0; - } -} \ No newline at end of file diff --git a/src/ByteAether.Ulid2/Ulid.Guid.cs b/src/ByteAether.Ulid2/Ulid.Guid.cs deleted file mode 100644 index bbb8cbe..0000000 --- a/src/ByteAether.Ulid2/Ulid.Guid.cs +++ /dev/null @@ -1,134 +0,0 @@ -using System.Buffers.Binary; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -#if NETCOREAPP -using System.Runtime.Intrinsics; -#if !NET7_0_OR_GREATER -using System.Runtime.Intrinsics.X86; -#endif -#endif - -namespace ByteAether.Ulid2; - -#if NET8_0_OR_GREATER -// We need to target netstandard2.1, so keep using ref for MemoryMarshal.Write -// CS9191: The 'ref' modifier for argument 2 corresponding to the 'in' parameter is equivalent to 'in'. Consider using 'in' instead. -#pragma warning disable CS9191 -#endif - -public readonly partial struct Ulid -{ - /// - /// Creates a new ULID using the specified GUID. - /// - /// The GUID to initialize the ULID with. -#if NET5_0_OR_GREATER - [SkipLocalsInit] -#endif -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - public static Ulid New(Guid guid) - => BitConverter.IsLittleEndian - ? Shuffle(ref guid) - : Unsafe.As(ref guid); - - /// - /// Converts the ULID to a GUID. - /// - /// A GUID representing the ULID. -#if NET5_0_OR_GREATER - [SkipLocalsInit] -#endif -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - public Guid ToGuid() - => BitConverter.IsLittleEndian - ? Shuffle(ref Unsafe.AsRef(in this)) - : Unsafe.As(ref Unsafe.AsRef(in this)); - - /// - /// Implicitly converts a ULID to a GUID. - /// - /// The ULID to convert. - /// A GUID representing the ULID. -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - public static implicit operator Guid(Ulid ulid) => ulid.ToGuid(); - - /// - /// Implicitly converts a GUID to a ULID. - /// - /// The GUID to convert. - /// A ULID representing the GUID. -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - public static implicit operator Ulid(Guid guid) => New(guid); - -#if NETCOREAPP - private static readonly Vector128 _shuffleMask - = Vector128.Create((byte)3, 2, 1, 0, 5, 4, 7, 6, 8, 9, 10, 11, 12, 13, 14, 15); -#endif - - // HACK: We assume the layout of a Guid is the following: - // Int32, Int16, Int16, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8 - // Source: https://github.com/dotnet/runtime/blob/5c4686f831d34c2c127e943d0f0d144793eeb0ad/src/libraries/System.Private.CoreLib/src/System/Guid.cs - // More info: https://stackoverflow.com/questions/10190817/guid-byte-order-in-net/10191075#10191075 -#if NET5_0_OR_GREATER - [SkipLocalsInit] -#endif -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - private static TOut Shuffle(ref TIn bytes) - { -#if NET7_0_OR_GREATER - if (Vector128.IsHardwareAccelerated) - { - var vector = Unsafe.As>(ref bytes); - vector = Vector128.Shuffle(vector, _shuffleMask); - return Unsafe.As, TOut>(ref vector); - } -#elif NETCOREAPP3_0_OR_GREATER - if (Ssse3.IsSupported) - { - var vector = Unsafe.As>(ref bytes); - vector = Ssse3.Shuffle(vector, _shuffleMask); - return Unsafe.As, TOut>(ref vector); - } -#endif - - // |A|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P| - // |D|C|B|A|... - // ...|F|E|H|G|... - // ...|I|J|K|L|M|N|O|P| - Span result = new byte[_ulidSize]; - - ref var ptr = ref Unsafe.As(ref bytes); - var lower = BinaryPrimitives.ReverseEndianness(ptr); - - ptr = ref Unsafe.Add(ref ptr, 1); - var upper = ((ptr & 0x00_FF_00_FF) << 8) | ((ptr & 0xFF_00_FF_00) >> 8); - - ref var upperBytes = ref Unsafe.As(ref Unsafe.Add(ref ptr, 1)); - - MemoryMarshal.Write(result, ref lower); - MemoryMarshal.Write(result[4..], ref upper); - MemoryMarshal.Write(result[8..], ref upperBytes); - - return Unsafe.As(ref result.GetPinnableReference()); - } -} \ No newline at end of file diff --git a/src/ByteAether.Ulid2/Ulid.IsValid.cs b/src/ByteAether.Ulid2/Ulid.IsValid.cs deleted file mode 100644 index af67298..0000000 --- a/src/ByteAether.Ulid2/Ulid.IsValid.cs +++ /dev/null @@ -1,128 +0,0 @@ -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -namespace ByteAether.Ulid2; - -public readonly partial struct Ulid -{ - /// - /// Validates if the given string is a valid ULID. - /// - /// The ULID string to validate. - /// - /// true if the string is a valid ULID, false otherwise. - /// -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - public static bool IsValid(string ulidString) => IsValid(ulidString.AsSpan()); - - /// - /// Validates if the given span of characters is a valid ULID. - /// - /// The ULID character span to validate. - /// - /// true if the character span is a valid ULID, false otherwise. - /// -#if NET5_0_OR_GREATER - [SkipLocalsInit] -#endif -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveOptimization)] -#endif - public static unsafe bool IsValid(ReadOnlySpan ulidString) - { - if (ulidString.Length != UlidStringLength) // 26 - { - return false; - } - - fixed (char* src = &MemoryMarshal.GetReference(ulidString)) - { - // 1. Fast check for the first character (prevent 128-bit overflow) - uint c0 = src[0]; - if (c0 > 255 || _inverseBase32[c0] > 7) - { - return false; - } - - fixed (byte* table = _inverseBase32) - { - // Block A: Indices 1 to 6 - if (((src[1] | src[2] | src[3] | src[4] | src[5] | src[6]) & 0xFF00) != 0) - { - return false; - } - - if ( - table[src[1]] == 255 || table[src[2]] == 255 || table[src[3]] == 255 - || table[src[4]] == 255 || table[src[5]] == 255 || table[src[6]] == 255 - ) - { - return false; - } - - // Block B: Indices 7 to 12 - if (((src[7] | src[8] | src[9] | src[10] | src[11] | src[12]) & 0xFF00) != 0) - { - return false; - } - - if ( - table[src[7]] == 255 || table[src[8]] == 255 || table[src[9]] == 255 - || table[src[10]] == 255 || table[src[11]] == 255 || table[src[12]] == 255 - ) - { - return false; - } - - // Block C: Indices 13 to 18 - if (((src[13] | src[14] | src[15] | src[16] | src[17] | src[18]) & 0xFF00) != 0) - { - return false; - } - - if ( - table[src[13]] == 255 || table[src[14]] == 255 || table[src[15]] == 255 - || table[src[16]] == 255 || table[src[17]] == 255 || table[src[18]] == 255 - ) - { - return false; - } - - // Block D: Indices 19 to 25 - if (((src[19] | src[20] | src[21] | src[22] | src[23] | src[24] | src[25]) & 0xFF00) != 0) - { - return false; - } - - if ( - table[src[19]] == 255 || table[src[20]] == 255 || table[src[21]] == 255 - || table[src[22]] == 255 || table[src[23]] == 255 || table[src[24]] == 255 - || table[src[25]] == 255 - ) - { - return false; - } - } - } - - return true; - } - - /// - /// Validates if the given byte array represents a valid ULID. - /// - /// The byte array to validate. - /// - /// true if the byte array is a valid ULID, false otherwise. - /// -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - public static bool IsValid(ReadOnlySpan ulidBytes) => ulidBytes.Length == _ulidSize; -} \ No newline at end of file diff --git a/src/ByteAether.Ulid2/Ulid.New.GenerationOptions.cs b/src/ByteAether.Ulid2/Ulid.New.GenerationOptions.cs deleted file mode 100644 index 5a5a7a4..0000000 --- a/src/ByteAether.Ulid2/Ulid.New.GenerationOptions.cs +++ /dev/null @@ -1,142 +0,0 @@ -namespace ByteAether.Ulid2; - -public readonly partial struct Ulid -{ - /// - /// Configuration options for ULID generation. - /// - public record GenerationOptions - { - /// - /// Monotonicity behavior for ULID generation. - /// - /// - /// The enum provides various options to configure - /// the generation of ULIDs with respect to their monotonic properties.
- /// These options determine how the ULID sequence behaves in scenarios - /// where time does not progress or progresses non-linearly. - ///
- public enum MonotonicityOptions - { - /// - /// ULIDs are generated in a completely non-monotonic manner. - /// - /// - /// When is used, ULIDs are created - /// without any monotonic guarantees. The random component of the ULID is - /// entirely random, and the sequence does not ensure order or incrementality. - /// - NonMonotonic = -1, - - /// - /// ULIDs are generated with a strictly monotonic increment in the random component. - /// - /// - /// When is used, the random portion of the ULID is - /// adjusted to ensure strict monotonic progression. This guarantees that the sequence of generated - /// ULIDs is always ordered and incremental, making it suitable for scenarios where strict ordering - /// is required without introducing additional randomness.
- /// This is the default behavior when monotonicity settings are not explicitly defined. - ///
- MonotonicIncrement = 0, - - /// - /// Generates ULIDs with monotonicity by adding a random 8-bit value to the existing random component. - /// - /// - /// When is used, a random value between 1 and 256 - /// is added to the existing random component. This addition may cause carries across all bytes of - /// the random component, ensuring the resulting ULID is greater than the previous one while - /// maintaining a degree of randomness in the increment. - /// - MonotonicRandom1Byte = 1, - - /// - /// Generates ULIDs with monotonicity by adding a random 16-bit value to the existing random component. - /// - /// - /// When is used, a random value between 1 and 65 536 - /// is added to the existing random component. This addition may cause carries across all bytes of - /// the random component, ensuring the resulting ULID is greater than the previous one while - /// providing a larger range of possible increments. - /// - MonotonicRandom2Byte = 2, - - /// - /// Generates ULIDs with monotonicity by adding a random 24-bit value to the existing random component. - /// - /// - /// When is used, a random value between 1 and 16 777 216 - /// is added to the existing random component. This addition may cause carries across all bytes of - /// the random component, ensuring the resulting ULID is greater than the previous one while - /// providing a significantly larger range of possible increments. - /// - MonotonicRandom3Byte = 3, - - /// - /// Generates ULIDs with monotonicity by adding a random 32-bit value to the existing random component. - /// - /// - /// When is used, a random value between 1 and 4 294 967 296 - /// is added to the existing random component. This addition may cause carries across all bytes of - /// the random component, ensuring the resulting ULID is greater than the previous one while - /// providing the maximum range of possible increments. - /// - MonotonicRandom4Byte = 4, - } - - /// - /// Monotonicity behavior for ULID generation. - /// - /// - /// This property determines how the timestamp and randomness components - /// will behave in regard to ordering and predictability:
- /// - NonMonotonic: No monotonic guarantees, Random part will be fully randomized.
- /// - MonotonicIncrement: Guarantees monotonic ordering by incrementing - /// the previous random by one if the same timestamp is generated consecutively.
- /// - MonotonicRandom1Byte to MonotonicRandom4Byte: Ensures monotonicity by introducing a - /// randomized 1 to 4 bytes value as an increment to the previous Random part when timestamps are identical. - ///
- /// - /// A value of the enum that specifies the monotonicity behavior. - /// Defaults to . - /// - public MonotonicityOptions Monotonicity - { - get; - init => field = Enum.IsDefined(typeof(MonotonicityOptions), value) - ? value - : throw new ArgumentOutOfRangeException(nameof(Monotonicity), value, "Invalid monotonicity option."); - } = MonotonicityOptions.MonotonicIncrement; - - /// - /// Initial random source used for ULID generation. - /// - /// - /// This property specifies the random number generator to be used for the initial - /// randomness during ULID creation. - /// - /// - /// An instance of a class that implements the interface - /// to provide the random number generation logic for the initial randomness component.
- /// Defaults to . - ///
- public IRandomProvider InitialRandomSource { get; init; } = new CryptographicallySecureRandomProvider(); - - /// - /// Random source used during monotonic ULID generation when timestamps - /// are identical and incremental randomness is required. - /// - /// - /// Specifies the random provider used to supply entropy for the Random component - /// during monotonic increments when consecutive ULIDs share the same timestamp.
- /// It is used in maintaining monotonicity while ensuring random variation in ULID values. - ///
- /// - /// An implementation of the interface that provides - /// randomness for monotonic increments.
- /// Defaults to an instance of . - ///
- public IRandomProvider IncrementRandomSource { get; init; } = new PseudoRandomProvider(); - }; -} \ No newline at end of file diff --git a/src/ByteAether.Ulid2/Ulid.New.cs b/src/ByteAether.Ulid2/Ulid.New.cs deleted file mode 100644 index 960305d..0000000 --- a/src/ByteAether.Ulid2/Ulid.New.cs +++ /dev/null @@ -1,293 +0,0 @@ -using System.Buffers.Binary; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -namespace ByteAether.Ulid2; - -public readonly partial struct Ulid -{ - /// - /// The default used for generating new ULIDs. - /// - /// - /// Allows customization of the generation behavior for all new ULIDs.
- /// It includes settings for monotonicity and the source of randomness for initial and - /// incremented scenarios during the generation of the ULID. Modifying this property - /// affects the global default behavior for ULID generation across the application. - ///
- public static GenerationOptions DefaultGenerationOptions { get; set; } = new(); - - // Constant for Unix Epoch (1970-01-01 UTC) in Ticks - private const long _unixEpochTicks = 621355968000000000; - - /// - /// Initializes a new instance of the struct using the specified byte array. - /// - /// The byte array to initialize the with. - /// Given bytes as an instance. -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - public static Ulid New(ReadOnlySpan bytes) - => MemoryMarshal.Read(bytes); - - /// - /// Creates a new with the current timestamp. - /// - /// - /// If null (default), the value of is used.
- /// Otherwise, uses the specified to control the ULID generation behavior. - /// - /// A new instance. -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - public static Ulid New(GenerationOptions? options = null) - // We can avoid Offset-related allocations by using DateTime over DateTimeOffset - // For public API, DateTimeOffset is an official recommendation - => New((DateTime.UtcNow.Ticks - _unixEpochTicks) / TimeSpan.TicksPerMillisecond, options); - - /// - /// Creates a new with the specified timestamp. - /// - /// The timestamp to use for the . - /// - /// If null (default), the value of is used.
- /// Otherwise, uses the specified to control the ULID generation behavior. - /// - /// A new instance. -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - public static Ulid New(DateTimeOffset dateTimeOffset, GenerationOptions? options = null) - => New(dateTimeOffset.ToUnixTimeMilliseconds(), options); - - /// - /// Creates a new with the specified timestamp. - /// - /// The timestamp to use for the . - /// - /// A span containing the random component of the .
- /// Must be at least 10 bytes long to populate the random component of the Ulid. - /// - /// A new instance. -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - public static Ulid New(DateTimeOffset dateTimeOffset, Span random) - => New(dateTimeOffset.ToUnixTimeMilliseconds(), random); - - /// - /// Creates a new with the specified timestamp in milliseconds. - /// - /// The timestamp in milliseconds to use for the . - /// - /// If null (default), the value of is used.
- /// Otherwise, uses the specified to control the ULID generation behavior. - /// - /// A new instance. -#if NET5_0_OR_GREATER - [SkipLocalsInit] -#endif -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - public static Ulid New(long timestamp, GenerationOptions? options = null) - { - Ulid ulid = default; - - ref var ulidRef = ref Unsafe.As(ref ulid); - - // Fill timestamp - var ts = (ulong)timestamp << 16; - ts = ReverseOnLittleEndian(ts); - Unsafe.WriteUnaligned(ref ulidRef, ts); - - FillRandom(ref ulidRef, timestamp, options ?? DefaultGenerationOptions); - - return ulid; - } - - /// - /// Creates a new instance of the struct using the specified timestamp and random byte sequence. - /// - /// - /// A 64-bit integer representing the timestamp in milliseconds since the Unix epoch (1970-01-01T00:00:00Z).
- /// This value will be encoded into the first 6 bytes of the . - /// - /// - /// A span containing the random component of the .
- /// It must be at least 10 bytes long, as the last 10 bytes of the are derived from this span. - /// - /// - /// A new instance composed of the given timestamp and random byte sequence. - /// -#if NET5_0_OR_GREATER - [SkipLocalsInit] -#endif -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - public static Ulid New(long timestamp, Span random) - { - Ulid ulid = default; - - ref var ulidRef = ref Unsafe.As(ref ulid); - - // Fill timestamp - var ts = (ulong)timestamp << 16; - ts = ReverseOnLittleEndian(ts); - Unsafe.WriteUnaligned(ref ulidRef, ts); - - // Fill random - Unsafe.CopyBlockUnaligned( - ref Unsafe.Add(ref ulidRef, _ulidSizeTime), - ref random.GetPinnableReference(), - _ulidSizeRandom - ); - - return ulid; - } - - // Separates Lock and LastUlid into different cache lines to prevent "false sharing" - // x64 has 64-byte cache lines; ARM64 (e.g., Apple Silicon) has 128-byte cache lines - [StructLayout(LayoutKind.Explicit)] - private class State - { - // Cache Line 1 - [FieldOffset(16)] public LowLatencyLock Lock; - - // Cache Line 2(ARM64)/3(x64) -#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value - [FieldOffset(16+128)] public ulong LastUlidPart0; - [FieldOffset(16+128+8)] public ulong LastUlidPart1; -#pragma warning restore CS0649 // Field is never assigned to, and will always have its default value - } - - private static readonly State _state = new(); - -#if NET5_0_OR_GREATER - [SkipLocalsInit] -#endif -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveOptimization)] -#endif - private static void FillRandom(ref byte ulidBytesRef, long timestamp, GenerationOptions options) - { - // Calculate offset to a random part - ref var ulidBytesRandomRef = ref Unsafe.Add(ref ulidBytesRef, _ulidSizeTime); - var monotonicity = options.Monotonicity; - - if (monotonicity == GenerationOptions.MonotonicityOptions.NonMonotonic) - { - options.InitialRandomSource.GetBytes( -#if NETCOREAPP - MemoryMarshal.CreateSpan(ref ulidBytesRandomRef, _ulidSizeRandom) -#else - Compatibility.MemoryMarshal.CreateSpan(ref ulidBytesRandomRef, _ulidSizeRandom) -#endif - ); - return; - } - - ref var lastUlidRef = ref Unsafe.As(ref _state.LastUlidPart0); - - using(_state.Lock.Enter()) - { - // Read the last timestamp (from bytes 0-7 of "last ULID") - // Shift it to get 48 bits. - var lastTime = ReverseOnLittleEndian(_state.LastUlidPart0); - lastTime >>= 16; - - // If the timestamp is bigger than the last one, generate a new ULID - if (timestamp > (long)lastTime) - { - // We work on "generated ULID", then copy it into "last ULID" - - // Generate a new random to the generated ULID - options.InitialRandomSource.GetBytes( -#if NETCOREAPP - MemoryMarshal.CreateSpan(ref ulidBytesRandomRef, _ulidSizeRandom) -#else - Compatibility.MemoryMarshal.CreateSpan(ref ulidBytesRandomRef, _ulidSizeRandom) -#endif - ); - - // Copy full generated ULID back to last ULID - Unsafe.CopyBlock(ref lastUlidRef, ref ulidBytesRef, _ulidSize); - } - else // Otherwise, increment the last ULID - { - // We work on "last ULID", then copy it into "generated ULID" - - if (monotonicity == GenerationOptions.MonotonicityOptions.MonotonicIncrement) - { - LastUlidIncrement(0); - } - else - { - // We can use the random bytes of incomplete ULID for the random increment span - var tempSpan = -#if NETCOREAPP - MemoryMarshal.CreateSpan(ref ulidBytesRandomRef, sizeof(uint)); -#else - Compatibility.MemoryMarshal.CreateSpan(ref ulidBytesRandomRef, sizeof(uint)); -#endif - options.IncrementRandomSource.GetBytes(tempSpan[..(int)monotonicity]); - var increment = BinaryPrimitives.ReadUInt32LittleEndian(tempSpan); - - // The tempSpan may contain garbage, so mask that out - var totalBitsToKeep = (int)monotonicity * sizeof(byte); - var mask = (uint)((1UL << totalBitsToKeep) - 1); - increment &= mask; - - LastUlidIncrement(increment); - } - - // Copy full last ULID back to generated ULID - Unsafe.CopyBlock(ref ulidBytesRef, ref lastUlidRef, _ulidSize); - } - } - } - -#if NET5_0_OR_GREATER - [SkipLocalsInit] -#endif -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - private static void LastUlidIncrement(uint addition) - { - var increment = (ulong)addition + 1; // carry = 1 is built-in - var part1 = ReverseOnLittleEndian(_state.LastUlidPart1); - var newPart1 = part1 + increment; - _state.LastUlidPart1 = ReverseOnLittleEndian(newPart1); - - if (newPart1 >= part1) // Overflow carried over to part0 - { - return; - } - - var part0 = ReverseOnLittleEndian(_state.LastUlidPart0); - part0++; - _state.LastUlidPart0 = ReverseOnLittleEndian(part0); - if (part0 == 0) - { - throw new OverflowException("Addition resulted in a ULID value larger than the absolute maximum ULID value."); - } - } -} \ No newline at end of file diff --git a/src/ByteAether.Ulid2/Ulid.Obsolete.cs b/src/ByteAether.Ulid2/Ulid.Obsolete.cs deleted file mode 100644 index 2c7e2be..0000000 --- a/src/ByteAether.Ulid2/Ulid.Obsolete.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; - -namespace ByteAether.Ulid2; - -public readonly partial struct Ulid -{ - /// - /// Represents the maximum possible value for a ULID. - /// - /// - /// The field is a ULID where all byte components are set to their highest possible value (0xFF). - /// It can be used as a sentinel or boundary value in comparison operations or range validations. - /// - [Obsolete("Use MaxValue instead.")] - public static Ulid Max => MaxValue; - - /// - /// Whether s should be generated in a monotonic manner by default.
- /// Initial value is set to true.
- /// This setting applies globally without any scoping. - ///
- /// - /// When set to true (default), s generated without explicitly specifying monotonicity - /// will ensure that they are monotonically increasing.
- /// When set to false, s generated without explicitly specifying monotonicity will be - /// generated with random value. - ///
- [Obsolete("Use DefaultGenerationOptions instead.")] - public static bool DefaultIsMonotonic - { - get => ObsoleteHelper.GetByGenerationOptions(DefaultGenerationOptions); - set => DefaultGenerationOptions = ObsoleteHelper.GetByBoolean(value); - } - - /// - /// Creates a new with the current timestamp. - /// - /// - /// If true, ensures the is monotonically increasing.
- /// If false, generates a random part in . - /// - /// A new instance. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - [Obsolete("Use method with DefaultGenerationOptions argument instead.")] - public static Ulid New(bool isMonotonic) - => New(ObsoleteHelper.GetByBoolean(isMonotonic)); - - /// - /// Creates a new with the specified timestamp. - /// - /// The timestamp to use for the . - /// - /// If true, ensures the is monotonically increasing.
- /// If false, generates a random part in . - /// - /// A new instance. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - [Obsolete("Use method with DefaultGenerationOptions argument instead.")] - public static Ulid New(DateTimeOffset dateTimeOffset, bool isMonotonic) - => New(dateTimeOffset, ObsoleteHelper.GetByBoolean(isMonotonic)); - - /// - /// Creates a new with the specified timestamp in milliseconds. - /// - /// The timestamp in milliseconds to use for the . - /// - /// If true, ensures the is monotonically increasing.
- /// If false, generates a random part in . - /// - /// A new instance. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - [Obsolete("Use method with DefaultGenerationOptions argument instead.")] - public static Ulid New(long timestamp, bool isMonotonic) - => New(timestamp, ObsoleteHelper.GetByBoolean(isMonotonic)); -} - -internal static class ObsoleteHelper -{ - private static readonly Ulid.GenerationOptions _monotonicDefaultOptions = new(); - - private static readonly Ulid.GenerationOptions _nonmonotonicDefaultOptions = new() - { - InitialRandomSource = new CryptographicallySecureRandomProvider(), - IncrementRandomSource = new PseudoRandomProvider(), // This has no effect - Monotonicity = Ulid.GenerationOptions.MonotonicityOptions.NonMonotonic - }; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - [return: NotNullIfNotNull(nameof(isMonotonic))] - public static Ulid.GenerationOptions? GetByBoolean(bool? isMonotonic) - => isMonotonic switch - { - true => _monotonicDefaultOptions, - false => _nonmonotonicDefaultOptions, - _ => null - }; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool GetByGenerationOptions(Ulid.GenerationOptions options) - => options.Monotonicity != Ulid.GenerationOptions.MonotonicityOptions.NonMonotonic; -} \ No newline at end of file diff --git a/src/ByteAether.Ulid2/Ulid.String.cs b/src/ByteAether.Ulid2/Ulid.String.cs deleted file mode 100644 index 33c8085..0000000 --- a/src/ByteAether.Ulid2/Ulid.String.cs +++ /dev/null @@ -1,458 +0,0 @@ -using System.Buffers.Binary; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Text; - -namespace ByteAether.Ulid2; - -[DebuggerDisplay("{ToString(),nq}")] -public readonly partial struct Ulid - : IFormattable -#if NET6_0_OR_GREATER - , ISpanFormattable -#if NET7_0_OR_GREATER - , IParsable // Keeping this here for clarity - , ISpanParsable -#if NET8_0_OR_GREATER - , IUtf8SpanFormattable - , IUtf8SpanParsable -#endif -#endif -#endif -{ - /// - /// The length of a ULID when encoded as a string in its canonical format. - /// - /// - /// A ULID string consists of 26 characters, encoded using Crockford's Base32 encoding. - /// - public const byte UlidStringLength = 26; - - private static readonly char[] _base32Chars = "0123456789ABCDEFGHJKMNPQRSTVWXYZ".ToCharArray(); - private static readonly byte[] _base32Bytes = Encoding.UTF8.GetBytes(_base32Chars); - private static readonly byte[] _inverseBase32 = - [ - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // controls - 255, // space - 255, // ! - 255, // " - 255, // # - 255, // $ - 255, // % - 255, // & - 255, // ' - 255, // ( - 255, // ) - 255, // * - 255, // + - 255, // , - 255, // - - 255, // . - 255, // / - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, // 0-9 - 255, 255, 255, 255, 255, 255, 255, // :-@ - 10, 11, 12, 13, 14, 15, 16, 17, // A-H - 1, // I - 18, 19, // J-K - 1, // L - 20, 21, // M-N - 0, // O - 22, 23, 24, 25, 26, // P-T - 255, // U - 27, 28, 29, 30, 31, // V-Z - 255, 255, 255, 255, 255, 255, // [-` - 10, 11, 12, 13, 14, 15, 16, 17, // a-h - 1, // i - 18, 19, // j-k - 1, // l - 20, 21, // m-n - 0, // o - 22, 23, 24, 25, 26, // p-t - 255, // u - 27, 28, 29, 30, 31, // v-z - // Pad with value 255 so the array size is 256 - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255 - ]; - - /// -#if NET5_0_OR_GREATER - [SkipLocalsInit] -#endif -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - public string ToString(string? format, IFormatProvider? formatProvider) => ToString(); - - /// - /// Returns a string representation of the current instance of in its canonical Crockford's Base32 format.' - /// - /// Crockford's Base32 representation of the ULID -#if NET5_0_OR_GREATER - [SkipLocalsInit] -#endif -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - public override string ToString() - { -#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP - return string.Create(UlidStringLength, this, static (span, ulid) => ulid.Fill(span, _base32Chars)); -#else - Span span = stackalloc char[UlidStringLength]; - Fill(span, _base32Chars); - return span.ToString(); -#endif - } - - /// - /// Parses a ULID from the provided read-only span of characters. - /// - /// The span of characters containing Crockford's Base32 representation of the ULID. - /// Ignored. The ULID is always formatted in its canonical Crockford's Base32 format. - /// A parsed instance of . - /// Thrown if the input span does not meet the ULID format requirements. -#if NET5_0_OR_GREATER - [SkipLocalsInit] -#endif -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - public static Ulid Parse(ReadOnlySpan chars, IFormatProvider? provider = null) - => ParseCore(chars); - - /// - /// Parses a ULID from a read-only span of bytes and returns the corresponding ULID value. - /// - /// The read-only span of bytes containing the ULID string representation in Crockford's Base32 format. - /// Ignored. The ULID is always formatted in its canonical Crockford's Base32 format. - /// The ULID parsed from the specified byte span. - /// Thrown if the input byte span does not contain a valid ULID string representation. -#if NET5_0_OR_GREATER - [SkipLocalsInit] -#endif -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - public static Ulid Parse(ReadOnlySpan bytes, IFormatProvider? provider = null) - => ParseCore(bytes); - -#if NET5_0_OR_GREATER - [SkipLocalsInit] -#endif -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveOptimization)] -#endif - private static unsafe Ulid ParseCore(ReadOnlySpan input) - where T : unmanaged - { - // Every T element is read as a byte. Other bits are ignored. - // We create 2 blocks of big-endian ulong values then reverse the endianness - // Creating a big-endian ulong and then reversing it is faster than creating directly a little-endian ulong - - if (input.Length != UlidStringLength) - { - throw new FormatException("The input sequence is not a valid ULID string representation."); - } - - var stepSize = sizeof(T); // We read the span as bytes and iterate by the size of an element - Ulid result = default; - - fixed (T* pSrc = &MemoryMarshal.GetReference(input)) - { - var pBytes = (byte*)pSrc; - ref var tableRef = ref _inverseBase32[0]; - ref var ulidRef = ref Unsafe.As(ref result); - - ulong t00 = Unsafe.Add(ref tableRef, pBytes[00 * stepSize]); - ulong t01 = Unsafe.Add(ref tableRef, pBytes[01 * stepSize]); - ulong t02 = Unsafe.Add(ref tableRef, pBytes[02 * stepSize]); - ulong t03 = Unsafe.Add(ref tableRef, pBytes[03 * stepSize]); - ulong t04 = Unsafe.Add(ref tableRef, pBytes[04 * stepSize]); - ulong t05 = Unsafe.Add(ref tableRef, pBytes[05 * stepSize]); - ulong t06 = Unsafe.Add(ref tableRef, pBytes[06 * stepSize]); - ulong t07 = Unsafe.Add(ref tableRef, pBytes[07 * stepSize]); - ulong t08 = Unsafe.Add(ref tableRef, pBytes[08 * stepSize]); - ulong t09 = Unsafe.Add(ref tableRef, pBytes[09 * stepSize]); - ulong r00 = Unsafe.Add(ref tableRef, pBytes[10 * stepSize]); - ulong r01 = Unsafe.Add(ref tableRef, pBytes[11 * stepSize]); - ulong r02 = Unsafe.Add(ref tableRef, pBytes[12 * stepSize]); - ulong r03 = Unsafe.Add(ref tableRef, pBytes[13 * stepSize]); - - var block1 = - (t00 << 61) - | (t01 << 56) - | (t02 << 51) - | (t03 << 46) - | (t04 << 41) - | (t05 << 36) - | (t06 << 31) - | (t07 << 26) - | (t08 << 21) - | (t09 << 16) - | (r00 << 11) - | (r01 << 6) - | (r02 << 1) - | (r03 >> 4); - - Unsafe.WriteUnaligned(ref Unsafe.Add(ref ulidRef, 0), ReverseOnLittleEndian(block1)); - - // Second block - ulong 64 bits - ulong r04 = Unsafe.Add(ref tableRef, pBytes[14 * stepSize]); - ulong r05 = Unsafe.Add(ref tableRef, pBytes[15 * stepSize]); - ulong r06 = Unsafe.Add(ref tableRef, pBytes[16 * stepSize]); - ulong r07 = Unsafe.Add(ref tableRef, pBytes[17 * stepSize]); - ulong r08 = Unsafe.Add(ref tableRef, pBytes[18 * stepSize]); - ulong r09 = Unsafe.Add(ref tableRef, pBytes[19 * stepSize]); - ulong r10 = Unsafe.Add(ref tableRef, pBytes[20 * stepSize]); - ulong r11 = Unsafe.Add(ref tableRef, pBytes[21 * stepSize]); - ulong r12 = Unsafe.Add(ref tableRef, pBytes[22 * stepSize]); - ulong r13 = Unsafe.Add(ref tableRef, pBytes[23 * stepSize]); - ulong r14 = Unsafe.Add(ref tableRef, pBytes[24 * stepSize]); - ulong r15 = Unsafe.Add(ref tableRef, pBytes[25 * stepSize]); - - var block2 = - (r03 << 60) - | (r04 << 55) - | (r05 << 50) - | (r06 << 45) - | (r07 << 40) - | (r08 << 35) - | (r09 << 30) - | (r10 << 25) - | (r11 << 20) - | (r12 << 15) - | (r13 << 10) - | (r14 << 5) - | r15; - - Unsafe.WriteUnaligned(ref Unsafe.Add(ref ulidRef, 8), ReverseOnLittleEndian(block2)); - } - - return result; - } - - /// - /// Parses a string representation of a ULID and returns the corresponding ULID instance. - /// - /// The string representation of the ULID to parse. - /// Ignored. The ULID is always formatted in its canonical Crockford's Base32 format. - /// A new instance parsed from the specified string. -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - public static Ulid Parse(string s, IFormatProvider? provider = null) - => ParseCore(s.AsSpan()); - - /// - /// Attempts to parse a string representation of a ULID into a instance. - /// - /// The string representation of the ULID to parse. - /// Ignored. The ULID is always formatted in its canonical Crockford's Base32 format. - /// When this method returns, contains the parsed value if the parse was successful; otherwise, the default value of . - /// true if the parsing was successful; otherwise, false. -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, out Ulid result) - => TryParse(s.AsSpan(), provider, out result); - - /// - /// Attempts to parse a ULID from a read-only span of characters. - /// - /// The read-only span of characters to parse. - /// Ignored. The ULID is always formatted in its canonical Crockford's Base32 format. - /// When the method returns, contains the parsed ULID if the operation succeeds, or the default value if it fails. - /// true if the parsing operation succeeded; otherwise, false. -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, out Ulid result) - { - try - { - result = ParseCore(s); - return true; - } - catch - { - result = default; - return false; - } - } - - /// - /// Attempts to parse a ULID from the specified span of bytes. - /// - /// The span of bytes containing the ULID representation to parse. - /// Ignored. The ULID is always formatted in its canonical Crockford's Base32 format. - /// When the method returns, contains the parsed ULID if parsing was successful; otherwise, the default value for ULID. - /// true if parsing was successful; otherwise, false. -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, out Ulid result) - { - try - { - result = ParseCore(s); - return true; - } - catch - { - result = default; - return false; - } - } - - /// - /// Attempts to format the current instance of into the provided character span. - /// - /// A span of characters where the formatted will be written, if successful. - /// The number of characters written to the destination span. - /// Ignored. The ULID is always formatted in its canonical Crockford's Base32 format. - /// Ignored. The ULID is always formatted in its canonical Crockford's Base32 format. - /// - /// true if the formatting is successful and the destination span is large enough to contain the formatted data; otherwise, false. - /// -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - public bool TryFormat( - Span destination, - out int charsWritten, - ReadOnlySpan format, - IFormatProvider? provider = null - ) - { - if (destination.Length < UlidStringLength) - { - charsWritten = 0; - return false; - } - - Fill(destination, _base32Chars); - charsWritten = UlidStringLength; - return true; - } - - /// - /// Attempts to format the current Ulid instance as a sequence of bytes. - /// - /// The span of bytes to write the formatted Ulid into. - /// When this method returns, contains the number of bytes that were written to the span. - /// Ignored. The ULID is always formatted in its canonical Crockford's Base32 format. - /// Ignored. The ULID is always formatted in its canonical Crockford's Base32 format. - /// - /// true if the formatting was successful; false if the destination span was too short to contain the formatted value. - /// -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - public bool TryFormat( - Span destination, - out int bytesWritten, - ReadOnlySpan format, - IFormatProvider? provider = null - ) - { - if (destination.Length < UlidStringLength) - { - bytesWritten = 0; - return false; - } - - Fill(destination, _base32Bytes); - bytesWritten = UlidStringLength; - return true; - } - -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveOptimization)] -#endif - private void Fill(Span span, T[] map) where T: unmanaged - { - // Encode randomness - span[25] = map[_r9 & 0x1F]; // [11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][111|11111|] - span[24] = map[((_r8 & 0x3) << 3) | (_r9 >> 5)]; // [11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][111111|11][111|11111] - span[23] = map[(_r8 >> 2) & 0x1F]; // [11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][1|11111|11][11111111] - span[22] = map[((_r7 & 0xF) << 1) | (_r8 >> 7)]; // [11111111][11111111][11111111][11111111][11111111][11111111][11111111][1111|1111][1|1111111][11111111] - span[21] = map[((_r6 & 0x1) << 4) | (_r7 >> 4)]; // [11111111][11111111][11111111][11111111][11111111][11111111][1111111|1][1111|1111][11111111][11111111] - span[20] = map[(_r6 >> 1) & 0x1F]; // [11111111][11111111][11111111][11111111][11111111][11111111][11|11111|1][11111111][11111111][11111111] - span[19] = map[((_r5 & 0x7) << 2) | (_r6 >> 6)]; // [11111111][11111111][11111111][11111111][11111111][11111|111][11|111111][11111111][11111111][11111111] - span[18] = map[(_r5 >> 3) & 0x1F]; // [11111111][11111111][11111111][11111111][11111111][|11111|111][11111111][11111111][11111111][11111111] - span[17] = map[_r4 & 0x1F]; // [11111111][11111111][11111111][11111111][111|11111|][11111111][11111111][11111111][11111111][11111111] - span[16] = map[((_r3 & 0x3) << 3) | (_r4 >> 5)]; // [11111111][11111111][11111111][11111111][111111|11][111|11111][11111111][11111111][11111111][11111111] - span[15] = map[(_r3 >> 2) & 0x1F]; // [11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111] - span[14] = map[((_r2 & 0xF) << 1) | (_r3 >> 7)]; // [11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111] - span[13] = map[((_r1 & 0x1) << 4) | (_r2 >> 4)]; // [11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111] - span[12] = map[(_r1 >> 1) & 0x1F]; // [11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111] - span[11] = map[((_r0 & 0x7) << 2) | (_r1 >> 6)]; // [11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111] - span[10] = map[(_r0 >> 3) & 0x1F]; // [|11111|111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111] - - // Encode timestamp - span[9] = map[_t5 & 0x1F]; // 00[11111111][11111111][11111111][11111111][11111111][111|11111|] - span[8] = map[((_t4 & 0x3) << 3) | (_t5 >> 5)]; // 00[11111111][11111111][11111111][11111111][111111|11][111|11111] - span[7] = map[(_t4 >> 2) & 0x1F]; // 00[11111111][11111111][11111111][11111111][1|11111|11][11111111] - span[6] = map[((_t3 & 0xF) << 1) | (_t4 >> 7)]; // 00[11111111][11111111][11111111][1111|1111][1|1111111][11111111] - span[5] = map[((_t2 & 0x1) << 4) | (_t3 >> 4)]; // 00[11111111][11111111][1111111|1][1111|1111][11111111][11111111] - span[4] = map[(_t2 >> 1) & 0x1F]; // 00[11111111][11111111][11|11111|1][11111111][11111111][11111111] - span[3] = map[((_t1 & 0x7) << 2) | (_t2 >> 6)]; // 00[11111111][11111|111][11|111111][11111111][11111111][11111111] - span[2] = map[_t1 >> 3]; // 00[11111111][|11111|111][11111111][11111111][11111111][11111111] - span[1] = map[_t0 & 0x1F]; // 00[111|11111|][11111111][11111111][11111111][11111111][11111111] - span[0] = map[_t0 >> 5]; // |00[111|11111][11111111][11111111][11111111][11111111][11111111] - } - - /// - /// Allows implicit conversion of to . - /// - /// - /// -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - public static implicit operator string(Ulid ulid) => ulid.ToString(); - - /// - /// Allows implicit conversion of to . - /// - /// - /// -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - public static implicit operator Ulid(string str) => Parse(str); -} \ No newline at end of file diff --git a/src/ByteAether.Ulid2/Ulid.cs b/src/ByteAether.Ulid2/Ulid.cs deleted file mode 100644 index 5f287c0..0000000 --- a/src/ByteAether.Ulid2/Ulid.cs +++ /dev/null @@ -1,162 +0,0 @@ -using System.ComponentModel; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Runtime.Serialization; -#if NETCOREAPP -using System.Text.Json.Serialization; -#endif - -namespace ByteAether.Ulid2; - -/// -/// Represents a Universally Unique Lexicographically Sortable Identifier (ULID). -/// -/// -/// A ULID is a 128-bit identifier that is sortable by time and consists of a timestamp and random components.
-/// For more information, visit the GitHub repository. -///
-#if NETCOREAPP -[JsonConverter(typeof(UlidJsonConverter))] -#endif -[TypeConverter(typeof(UlidTypeConverter))] -[StructLayout(LayoutKind.Explicit)] -public readonly partial struct Ulid -{ - private const byte _ulidSizeTime = 6; - private const byte _ulidSizeRandom = 10; - private const byte _ulidSize = _ulidSizeTime + _ulidSizeRandom; - - [FieldOffset(00)] private readonly byte _t0; - [FieldOffset(01)] private readonly byte _t1; - [FieldOffset(02)] private readonly byte _t2; - [FieldOffset(03)] private readonly byte _t3; - [FieldOffset(04)] private readonly byte _t4; - [FieldOffset(05)] private readonly byte _t5; - - [FieldOffset(06)] private readonly byte _r0; - [FieldOffset(07)] private readonly byte _r1; - [FieldOffset(08)] private readonly byte _r2; - [FieldOffset(09)] private readonly byte _r3; - [FieldOffset(10)] private readonly byte _r4; - [FieldOffset(11)] private readonly byte _r5; - [FieldOffset(12)] private readonly byte _r6; - [FieldOffset(13)] private readonly byte _r7; - [FieldOffset(14)] private readonly byte _r8; - [FieldOffset(15)] private readonly byte _r9; - - /// - /// Gets the random component of the ULID as a byte array. - /// - /// - /// The random component consists of the last 10 bytes of the ULID and is generated randomly to ensure uniqueness.
- /// This component does not encode any timestamp or other structured information. - ///
- /// - /// A byte array containing 10 random bytes that represent the random portion of the ULID. - /// - [IgnoreDataMember] - public ReadOnlySpan Random - { -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - get => AsByteSpan()[_ulidSizeTime..]; - } - - /// - /// Gets the time component of the ULID as a byte array. - /// - /// - /// The time component consists of the first 6 bytes of the ULID and is generated during ULID creation. - /// - /// - /// A byte array containing 6 time bytes that represent the time portion of the ULID. - /// - [IgnoreDataMember] - public ReadOnlySpan TimeBytes - { -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - get => AsByteSpan()[.._ulidSizeTime]; - } - - /// - /// Gets the timestamp component of the ULID as a . - /// - /// - /// The timestamp component represents the number of milliseconds since the Unix epoch - /// (1970-01-01T00:00:00Z). It is stored in the first 6 bytes of the ULID and ensures - /// lexicographical sorting by time.
- /// The timestamp is extracted in a way that is compatible with both little-endian and big-endian systems. - ///
- /// - /// A representing the timestamp portion of the ULID. - /// - [IgnoreDataMember] - public DateTimeOffset Time - { -#if NET5_0_OR_GREATER - [SkipLocalsInit] -#endif -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - get - { - // Combine the 6 bytes into a 48-bit timestamp (big-endian order) - var time = - ((long)_t0 << 40) | - ((long)_t1 << 32) | - ((long)_t2 << 24) | - ((long)_t3 << 16) | - ((long)_t4 << 8) | - _t5 - ; - - return DateTimeOffset.FromUnixTimeMilliseconds(time); - } - } - - /// - /// Creates a read-only span of bytes representing the current instance of the struct. - /// - /// - /// A that points to the raw byte representation of the current struct. - /// -#if NET5_0_OR_GREATER - [SkipLocalsInit] -#endif -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - public unsafe ReadOnlySpan AsByteSpan() - => new(Unsafe.AsPointer(ref Unsafe.AsRef(in this)), _ulidSize); - - /// - /// Converts the ULID to a byte array. - /// - /// A byte array representing the ULID. -#if NET5_0_OR_GREATER - [SkipLocalsInit] -#endif -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - public byte[] ToByteArray() - { - var bytes = new byte[_ulidSize]; - Unsafe.WriteUnaligned(ref bytes[0], this); - return bytes; - } -} \ No newline at end of file diff --git a/src/ByteAether.Ulid2/UlidJsonConverter.cs b/src/ByteAether.Ulid2/UlidJsonConverter.cs deleted file mode 100644 index 9ea1a36..0000000 --- a/src/ByteAether.Ulid2/UlidJsonConverter.cs +++ /dev/null @@ -1,69 +0,0 @@ -#if NETCOREAPP -using System.Buffers; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace ByteAether.Ulid2; - -/// -/// A custom JSON converter for the type. -/// -public class UlidJsonConverter : JsonConverter -{ - /// - public override Ulid Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - try - { - if (reader.TokenType is not JsonTokenType.String and not JsonTokenType.PropertyName) - { - throw new JsonException("Expected string or property name"); - } - - if (reader.HasValueSequence) - { - var byteSequence = reader.ValueSequence; - if (byteSequence.Length != Ulid.UlidStringLength) - { - throw new JsonException($"Ulid invalid: length must be {Ulid.UlidStringLength}"); - } - - Span byteSpan = stackalloc byte[Ulid.UlidStringLength]; - byteSequence.CopyTo(byteSpan); - return Ulid.Parse(byteSpan); - } - else - { - var byteSpan = reader.ValueSpan; - return Ulid.Parse(byteSpan); - } - } - catch (FormatException ex) - { - throw new JsonException($"Ulid invalid: length must be {Ulid.UlidStringLength}", ex); - } - } - - /// - public override void Write(Utf8JsonWriter writer, Ulid ulid, JsonSerializerOptions options) - { - Span ulidString = stackalloc byte[Ulid.UlidStringLength]; - ulid.TryFormat(ulidString, out _, []); - writer.WriteStringValue(ulidString); - } - -#if NET6_0_OR_GREATER - /// - public override void WriteAsPropertyName(Utf8JsonWriter writer, Ulid ulid, JsonSerializerOptions options) - { - Span ulidString = stackalloc byte[Ulid.UlidStringLength]; - ulid.TryFormat(ulidString, out _, []); - writer.WritePropertyName(ulidString); - } - - /// - public override Ulid ReadAsPropertyName(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - => Read(ref reader, typeToConvert, options); -#endif -} -#endif \ No newline at end of file diff --git a/src/ByteAether.Ulid2/UlidTypeConverter.cs b/src/ByteAether.Ulid2/UlidTypeConverter.cs deleted file mode 100644 index 0701199..0000000 --- a/src/ByteAether.Ulid2/UlidTypeConverter.cs +++ /dev/null @@ -1,48 +0,0 @@ -#if !NET6_0_OR_GREATER -// ReSharper disable NullnessAnnotationConflictWithJetBrainsAnnotations -// ReSharper disable AssignNullToNotNullAttribute -// ReSharper disable ReturnTypeCanBeNotNullable -#endif - -using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; - -namespace ByteAether.Ulid2; - -/// -/// A type converter for the type. -/// -public class UlidTypeConverter : TypeConverter -{ - private static readonly Type[] _convertibleTypes = [typeof(string), typeof(byte[]), typeof(Guid)]; - - /// - public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) - => _convertibleTypes.Contains(sourceType) - || base.CanConvertFrom(context, sourceType); - - /// - public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) - => value switch - { - string s => Ulid.Parse(s), - byte[] b => Ulid.New(b), - Guid guid => Ulid.New(guid), - _ => base.ConvertFrom(context, culture, value), - }; - - /// - public override bool CanConvertTo(ITypeDescriptorContext? context, [NotNullWhen(true)] Type? destinationType) - => _convertibleTypes.Contains(destinationType) - || base.CanConvertTo(context, destinationType); - - /// - public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType) - => value is Ulid ulid - ? destinationType == typeof(string) ? ulid.ToString() - : destinationType == typeof(byte[]) ? ulid.ToByteArray() - : destinationType == typeof(Guid) ? ulid.ToGuid() - : base.ConvertTo(context, culture, value, destinationType) - : base.ConvertTo(context, culture, value, destinationType); -} \ No newline at end of file From dc2a9effb55dd6ea47bd30e4aadad8f6ff40202c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joonatan=20Uusv=C3=A4li?= Date: Tue, 7 Jul 2026 17:47:56 +0300 Subject: [PATCH 5/5] Updated benchmarks. --- README.md | 134 +++++++++++++++++++++++++++--------------------------- 1 file changed, 67 insertions(+), 67 deletions(-) diff --git a/README.md b/README.md index c822a2e..6614a76 100644 --- a/README.md +++ b/README.md @@ -439,7 +439,7 @@ Benchmark scenarios also include comparisons against `Guid`, where functionality The following benchmarks were performed: ``` BenchmarkDotNet v0.15.8, Windows 10 (10.0.19044.7417/21H2/November2021Update) -AMD Ryzen 7 3700X 3.60GHz, 1 CPU, 12 logical and 6 physical cores +AMD Ryzen 7 3700X 3.60GHz, 1 CPU, 8 logical and 4 physical cores .NET SDK 10.0.301 [Host] : .NET 10.0.9 (10.0.9, 10.0.926.27113), X64 RyuJIT x86-64-v3 DefaultJob : .NET 10.0.9 (10.0.9, 10.0.926.27113), X64 RyuJIT x86-64-v3 @@ -448,72 +448,72 @@ Job=DefaultJob | Type | Method | Mean | Error | Gen0 | Allocated | |---------------- |------------------- |------------:|----------:|-------:|----------:| -| Generate | ByteAetherUlid | 41.0279 ns | 0.0968 ns | - | - | -| Generate | ByteAetherUlidR1Bp | 47.2891 ns | 0.1171 ns | - | - | -| Generate | ByteAetherUlidR4Bp | 51.9968 ns | 0.0910 ns | - | - | -| Generate | ByteAetherUlidR1Bc | 90.0880 ns | 0.1864 ns | - | - | -| Generate | ByteAetherUlidR4Bc | 96.0395 ns | 0.2255 ns | - | - | -| Generate | NetUlid *(1) | 158.6095 ns | 0.9265 ns | 0.0095 | 80 B | -| Generate | NUlid *(2) | 48.7143 ns | 0.0948 ns | - | - | - -| GenerateNonMono | ByteAetherUlid | 90.1055 ns | 0.3176 ns | - | - | -| GenerateNonMono | ByteAetherUlidP | 41.1849 ns | 0.0736 ns | - | - | -| GenerateNonMono | Ulid *(3,4) | 39.2001 ns | 0.1008 ns | - | - | -| GenerateNonMono | NUlid | 93.0571 ns | 0.1699 ns | - | - | -| GenerateNonMono | Guid *(5) | 47.1776 ns | 0.0932 ns | - | - | -| GenerateNonMono | GuidV7 *(3,5) | 77.4890 ns | 0.2267 ns | - | - | - -| FromByteArray | ByteAetherUlid | 0.8044 ns | 0.0070 ns | - | - | -| FromByteArray | NetUlid | 1.4115 ns | 0.0111 ns | - | - | -| FromByteArray | Ulid | 1.1808 ns | 0.0080 ns | - | - | -| FromByteArray | NUlid | 1.1515 ns | 0.0063 ns | - | - | -| FromByteArray | Guid | 1.0440 ns | 0.0059 ns | - | - | - -| FromGuid | ByteAetherUlid | 0.8255 ns | 0.0057 ns | - | - | -| FromGuid | NetUlid | 2.0028 ns | 0.0373 ns | - | - | -| FromGuid | Ulid | 1.9495 ns | 0.0116 ns | - | - | -| FromGuid | NUlid | 0.9240 ns | 0.0183 ns | - | - | - -| FromString | ByteAetherUlid | 14.4357 ns | 0.0392 ns | - | - | -| FromString | NetUlid | 27.2159 ns | 0.0690 ns | - | - | -| FromString | Ulid | 16.9972 ns | 0.0311 ns | - | - | -| FromString | NUlid | 53.9897 ns | 0.1649 ns | 0.0086 | 72 B | -| FromString | Guid | 21.8639 ns | 0.1228 ns | - | - | - -| ToByteArray | ByteAetherUlid | 4.7470 ns | 0.1274 ns | 0.0048 | 40 B | -| ToByteArray | AsByteSpan *(6) | 0.7736 ns | 0.0051 ns | - | - | -| ToByteArray | NetUlid | 9.4871 ns | 0.1054 ns | 0.0048 | 40 B | -| ToByteArray | Ulid | 4.7189 ns | 0.1066 ns | 0.0048 | 40 B | -| ToByteArray | NUlid | 8.7597 ns | 0.1460 ns | 0.0048 | 40 B | - -| ToGuid | ByteAetherUlid | 0.8170 ns | 0.0070 ns | - | - | -| ToGuid | NetUlid | 10.3155 ns | 0.0230 ns | - | - | -| ToGuid | Ulid | 0.9731 ns | 0.0074 ns | - | - | -| ToGuid | NUlid | 0.7636 ns | 0.0061 ns | - | - | - -| ToString | ByteAetherUlid | 21.5166 ns | 0.3377 ns | 0.0095 | 80 B | -| ToString | NetUlid | 27.3102 ns | 0.3004 ns | 0.0095 | 80 B | -| ToString | Ulid | 23.4614 ns | 0.2211 ns | 0.0095 | 80 B | -| ToString | NUlid | 29.4123 ns | 0.2632 ns | 0.0095 | 80 B | -| ToString | Guid | 10.2546 ns | 0.2493 ns | 0.0115 | 96 B | - -| CompareTo | ByteAetherUlid | 1.4082 ns | 0.0071 ns | - | - | -| CompareTo | NetUlid | 4.4499 ns | 0.0303 ns | - | - | -| CompareTo | Ulid | 6.6206 ns | 0.0352 ns | - | - | -| CompareTo | NUlid | 9.2860 ns | 0.0495 ns | - | - | -| CompareTo | Guid | 4.8326 ns | 0.0210 ns | - | - | - -| Equals | ByteAetherUlid | 1.0720 ns | 0.0135 ns | - | - | -| Equals | NetUlid | 1.9610 ns | 0.0155 ns | - | - | -| Equals | Ulid | 1.0432 ns | 0.0052 ns | - | - | -| Equals | NUlid | 1.0552 ns | 0.0103 ns | - | - | -| Equals | Guid | 1.1030 ns | 0.0107 ns | - | - | - -| GetHashCode | ByteAetherUlid | 0.9185 ns | 0.0053 ns | - | - | -| GetHashCode | NetUlid | 8.8601 ns | 0.0250 ns | - | - | -| GetHashCode | Ulid | 0.9362 ns | 0.0059 ns | - | - | -| GetHashCode | NUlid | 6.9396 ns | 0.0387 ns | - | - | -| GetHashCode | Guid | 0.9082 ns | 0.0049 ns | - | - | +| Generate | ByteAetherUlid | 40.5719 ns | 0.6657 ns | - | - | +| Generate | ByteAetherUlidR1Bp | 48.3766 ns | 0.1168 ns | - | - | +| Generate | ByteAetherUlidR4Bp | 49.9384 ns | 0.0914 ns | - | - | +| Generate | ByteAetherUlidR1Bc | 89.6066 ns | 0.6575 ns | - | - | +| Generate | ByteAetherUlidR4Bc | 91.5831 ns | 0.3683 ns | - | - | +| Generate | NetUlid *(1) | 162.1996 ns | 0.7979 ns | 0.0095 | 80 B | +| Generate | NUlid *(2) | 49.6330 ns | 0.1247 ns | - | - | + +| GenerateNonMono | ByteAetherUlid | 89.6904 ns | 0.5098 ns | - | - | +| GenerateNonMono | ByteAetherUlidP | 40.9386 ns | 0.1507 ns | - | - | +| GenerateNonMono | Ulid *(3,4) | 39.5954 ns | 0.0592 ns | - | - | +| GenerateNonMono | NUlid | 93.1371 ns | 0.4716 ns | - | - | +| GenerateNonMono | Guid *(5) | 47.6369 ns | 0.1340 ns | - | - | +| GenerateNonMono | GuidV7 *(3,5) | 78.6568 ns | 0.4788 ns | - | - | + +| FromByteArray | ByteAetherUlid | 0.7974 ns | 0.0076 ns | - | - | +| FromByteArray | NetUlid | 1.4321 ns | 0.0109 ns | - | - | +| FromByteArray | Ulid | 1.1894 ns | 0.0115 ns | - | - | +| FromByteArray | NUlid | 1.1603 ns | 0.0105 ns | - | - | +| FromByteArray | Guid | 1.0478 ns | 0.0084 ns | - | - | + +| FromGuid | ByteAetherUlid | 0.8286 ns | 0.0052 ns | - | - | +| FromGuid | NetUlid | 2.5073 ns | 0.1470 ns | - | - | +| FromGuid | Ulid | 2.3504 ns | 0.0776 ns | - | - | +| FromGuid | NUlid | 0.9781 ns | 0.0437 ns | - | - | + +| FromString | ByteAetherUlid | 15.2724 ns | 0.2616 ns | - | - | +| FromString | NetUlid | 28.4821 ns | 0.5618 ns | - | - | +| FromString | Ulid | 17.8202 ns | 0.2157 ns | - | - | +| FromString | NUlid | 64.0466 ns | 1.4103 ns | 0.0086 | 72 B | +| FromString | Guid | 22.5952 ns | 0.4522 ns | - | - | + +| ToByteArray | ByteAetherUlid | 4.6165 ns | 0.1121 ns | 0.0048 | 40 B | +| ToByteArray | AsByteSpan *(6) | 0.7794 ns | 0.0064 ns | - | - | +| ToByteArray | NetUlid | 9.4252 ns | 0.0894 ns | 0.0048 | 40 B | +| ToByteArray | Ulid | 4.5432 ns | 0.0997 ns | 0.0048 | 40 B | +| ToByteArray | NUlid | 8.5760 ns | 0.1168 ns | 0.0048 | 40 B | + +| ToGuid | ByteAetherUlid | 0.8083 ns | 0.0102 ns | - | - | +| ToGuid | NetUlid | 10.4080 ns | 0.0285 ns | - | - | +| ToGuid | Ulid | 1.2320 ns | 0.0102 ns | - | - | +| ToGuid | NUlid | 0.7692 ns | 0.0057 ns | - | - | + +| ToString | ByteAetherUlid | 19.1190 ns | 0.2176 ns | 0.0095 | 80 B | +| ToString | NetUlid | 23.7245 ns | 0.1987 ns | 0.0095 | 80 B | +| ToString | Ulid | 19.8952 ns | 0.2056 ns | 0.0095 | 80 B | +| ToString | NUlid | 29.1001 ns | 0.1223 ns | 0.0095 | 80 B | +| ToString | Guid | 7.9173 ns | 0.0257 ns | 0.0115 | 96 B | + +| CompareTo | ByteAetherUlid | 1.3642 ns | 0.0089 ns | - | - | +| CompareTo | NetUlid | 4.7076 ns | 0.0190 ns | - | - | +| CompareTo | Ulid | 6.7843 ns | 0.0188 ns | - | - | +| CompareTo | NUlid | 9.1551 ns | 0.0284 ns | - | - | +| CompareTo | Guid | 4.7391 ns | 0.0188 ns | - | - | + +| Equals | ByteAetherUlid | 1.1228 ns | 0.0164 ns | - | - | +| Equals | NetUlid | 1.9661 ns | 0.0091 ns | - | - | +| Equals | Ulid | 1.1092 ns | 0.0156 ns | - | - | +| Equals | NUlid | 1.0888 ns | 0.0063 ns | - | - | +| Equals | Guid | 1.0827 ns | 0.0085 ns | - | - | + +| GetHashCode | ByteAetherUlid | 0.9166 ns | 0.0046 ns | - | - | +| GetHashCode | NetUlid | 8.9307 ns | 0.0230 ns | - | - | +| GetHashCode | Ulid | 0.9288 ns | 0.0083 ns | - | - | +| GetHashCode | NUlid | 7.0085 ns | 0.0312 ns | - | - | +| GetHashCode | Guid | 0.9337 ns | 0.0068 ns | - | - | ``` Existing competitive libraries exhibit various deviations from the official ULID specification or present drawbacks: