From a770b16e523b901056167ab248d1ce551f9303a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joonatan=20Uusv=C3=A4li?= Date: Wed, 16 Apr 2025 14:57:02 +0300 Subject: [PATCH] Add DefaultIsMonotonic property to Ulid class Introduce a new static property `DefaultIsMonotonic` to the `Ulid` class, allowing users to set the default behavior for generating ULIDs. Update ULID creation methods to accept a nullable `isMonotonic` parameter for enhanced flexibility. Revise test cases in `UlidComparableTests` and `UlidNewTests` to validate the new behavior. Update documentation in `README.md` and `PACKAGE.md` to reflect these changes, ensuring accurate usage information. Add comments to clarify the implications of the new property and method parameters for improved code readability. --- README.md | 10 +- .../Ulid.Comparable.Tests.cs | 8 +- src/ByteAether.Ulid.Tests/Ulid.New.Tests.cs | 98 ++++++++++++++----- src/ByteAether.Ulid/PACKAGE.md | 10 +- src/ByteAether.Ulid/Ulid.New.cs | 76 +++++++++----- 5 files changed, 142 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index 362c18b..e65f5de 100644 --- a/README.md +++ b/README.md @@ -97,11 +97,13 @@ The `Ulid` implementation provides the following properties and methods: ### Creation -- `Ulid.New(bool isMonotonic = true)`\ -Generates a new ULID. If `isMonotonic` is `true`, ensures monotonicity during timestamp collisions. -- `Ulid.New(DateTimeOffset dateTimeOffset, bool isMonotonic = true)`\ +- `Ulid.DefaultIsMonotonic = true`\ +Sets the default behavior for generating ULIDs unless overridden during generation. If `true` (default), ensures monotonicity during timestamp collisions. +- `Ulid.New(bool? isMonotonic = null)`\ +Generates a new ULID. If `isMonotonic` is `null` (default), uses `Ulid.DefaultIsMonotonic` for monotonicity setting. +- `Ulid.New(DateTimeOffset dateTimeOffset, bool? isMonotonic = null)`\ Generates a new ULID using the specified `DateTimeOffset`. -- `Ulid.New(long timestamp, bool isMonotonic = true)`\ +- `Ulid.New(long timestamp, bool? isMonotonic = null)`\ Generates a new ULID using the specified Unix timestamp in milliseconds (`long`). - `Ulid.New(DateTimeOffset dateTimeOffset, Span random)`\ Generates a new ULID using the specified `DateTimeOffset` and a pre-existing random byte array. diff --git a/src/ByteAether.Ulid.Tests/Ulid.Comparable.Tests.cs b/src/ByteAether.Ulid.Tests/Ulid.Comparable.Tests.cs index 9ed2eff..4bc428a 100644 --- a/src/ByteAether.Ulid.Tests/Ulid.Comparable.Tests.cs +++ b/src/ByteAether.Ulid.Tests/Ulid.Comparable.Tests.cs @@ -24,8 +24,8 @@ public void CompareTo_SameUlid_ShouldReturnZero() public void CompareTo_CompareToNewerUlid_ShouldReturnNegative() { // Arrange - var ulid1 = Ulid.New(); - var ulid2 = Ulid.New(); + var ulid1 = Ulid.New(true); + var ulid2 = Ulid.New(true); // Act var comparisonResult = ulid1.CompareTo(ulid2); @@ -42,8 +42,8 @@ public void CompareTo_CompareToNewerUlid_ShouldReturnNegative() public void CompareTo_CompareToOlderUlid_ShouldReturnPositive() { // Arrange - var ulid1 = Ulid.New(); - var ulid2 = Ulid.New(); + var ulid1 = Ulid.New(true); + var ulid2 = Ulid.New(true); // Act var comparisonResult = ulid2.CompareTo(ulid1); diff --git a/src/ByteAether.Ulid.Tests/Ulid.New.Tests.cs b/src/ByteAether.Ulid.Tests/Ulid.New.Tests.cs index d2c22ba..b637a9b 100644 --- a/src/ByteAether.Ulid.Tests/Ulid.New.Tests.cs +++ b/src/ByteAether.Ulid.Tests/Ulid.New.Tests.cs @@ -37,57 +37,75 @@ public void New_WithDateTime_ShouldGenerateUniqueUlids(bool isMonotonic) { // Arrange var dateTimeOffset = DateTimeOffset.UtcNow; + var timestamp = dateTimeOffset.ToUnixTimeMilliseconds(); // Act var ulid1 = Ulid.New(dateTimeOffset, isMonotonic); var ulid2 = Ulid.New(dateTimeOffset, isMonotonic); // Assert - Assert.Equal(dateTimeOffset.ToUnixTimeMilliseconds(), ulid1.Time.ToUnixTimeMilliseconds()); - Assert.Equal(ulid1.Time, ulid2.Time); - Assert.Equal(ulid1.TimeBytes.ToArray(), ulid2.TimeBytes.ToArray()); - Assert.NotEqual(ulid1.Random.ToArray(), ulid2.Random.ToArray()); Assert.NotEqual(ulid1, ulid2); + + Assert.True(ulid1.Time.ToUnixTimeMilliseconds() <= ulid2.Time.ToUnixTimeMilliseconds()); + Assert.True(timestamp <= ulid1.Time.ToUnixTimeMilliseconds()); + Assert.True(timestamp <= ulid2.Time.ToUnixTimeMilliseconds()); + + if (isMonotonic) + { + Assert.True(MemoryExtensions.SequenceCompareTo(ulid1.AsByteSpan(), ulid2.AsByteSpan()) < 0); + Assert.True(ulid1 < ulid2); + } } - [Fact] - public void New_WithDateTimeAndRandom_ShouldGenerateSameUlid() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void New_WithTimestamp_ShouldGenerateUniqueUlids(bool isMonotonic) { // Arrange var dateTimeOffset = DateTimeOffset.UtcNow; - var random = new byte[10]; + var timestamp = dateTimeOffset.ToUnixTimeMilliseconds(); // Act - var ulid1 = Ulid.New(dateTimeOffset, random); - var ulid2 = Ulid.New(dateTimeOffset, random); + var ulid1 = Ulid.New(timestamp, isMonotonic); + var ulid2 = Ulid.New(timestamp, isMonotonic); // Assert - Assert.Equal(dateTimeOffset.ToUnixTimeMilliseconds(), ulid1.Time.ToUnixTimeMilliseconds()); - Assert.Equal(ulid1.Time, ulid2.Time); - Assert.Equal(ulid1.TimeBytes.ToArray(), ulid2.TimeBytes.ToArray()); - Assert.Equal(ulid1.Random.ToArray(), ulid2.Random.ToArray()); - Assert.Equal(ulid1, ulid2); + Assert.NotEqual(ulid1, ulid2); + + Assert.True(ulid1.Time.ToUnixTimeMilliseconds() <= ulid2.Time.ToUnixTimeMilliseconds()); + Assert.True(timestamp <= ulid1.Time.ToUnixTimeMilliseconds()); + Assert.True(timestamp <= ulid2.Time.ToUnixTimeMilliseconds()); + + if (isMonotonic) + { + Assert.True(MemoryExtensions.SequenceCompareTo(ulid1.AsByteSpan(), ulid2.AsByteSpan()) < 0); + Assert.True(ulid1 < ulid2); + } } - [Theory] - [InlineData(true)] - [InlineData(false)] - public void New_WithTimestamp_ShouldGenerateUniqueUlids(bool isMonotonic) + [Fact] + public void New_WithDateTimeAndRandom_ShouldGenerateSameUlid() { // Arrange var dateTimeOffset = DateTimeOffset.UtcNow; var timestamp = dateTimeOffset.ToUnixTimeMilliseconds(); + var random = new byte[10]; // Act - var ulid1 = Ulid.New(timestamp, isMonotonic); - var ulid2 = Ulid.New(timestamp, isMonotonic); + var ulid1 = Ulid.New(dateTimeOffset, random); + var ulid2 = Ulid.New(dateTimeOffset, random); // Assert + Assert.Equal(ulid1, ulid2); + Assert.Equal(timestamp, ulid1.Time.ToUnixTimeMilliseconds()); + Assert.Equal(timestamp, ulid2.Time.ToUnixTimeMilliseconds()); + Assert.Equal(ulid1.Time, ulid2.Time); Assert.Equal(ulid1.TimeBytes.ToArray(), ulid2.TimeBytes.ToArray()); - Assert.NotEqual(ulid1.Random.ToArray(), ulid2.Random.ToArray()); - Assert.NotEqual(ulid1, ulid2); + + Assert.Equal(ulid1.Random.ToArray(), ulid2.Random.ToArray()); } [Fact] @@ -103,10 +121,44 @@ public void New_WithTimestampAndRandom_ShouldGenerateSameUlid() var ulid2 = Ulid.New(timestamp, random); // Assert + Assert.Equal(ulid1, ulid2); + Assert.Equal(timestamp, ulid1.Time.ToUnixTimeMilliseconds()); + Assert.Equal(timestamp, ulid2.Time.ToUnixTimeMilliseconds()); + Assert.Equal(ulid1.Time, ulid2.Time); Assert.Equal(ulid1.TimeBytes.ToArray(), ulid2.TimeBytes.ToArray()); + Assert.Equal(ulid1.Random.ToArray(), ulid2.Random.ToArray()); - Assert.Equal(ulid1, ulid2); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + [InlineData(null, true)] + [InlineData(null, false)] + public void New_WithTimestampAndMonotonicSet_ShouldGenerateUniqueUlids(bool? isMonotonic, bool defaultIsMonotonic = true) + { + // Arrange + Ulid.DefaultIsMonotonic = defaultIsMonotonic; + var dateTimeOffset = DateTimeOffset.UtcNow; + var timestamp = dateTimeOffset.ToUnixTimeMilliseconds(); + + // Act + var ulid1 = Ulid.New(timestamp, isMonotonic); + var ulid2 = Ulid.New(timestamp, isMonotonic); + + // Assert + Assert.NotEqual(ulid1, ulid2); + + Assert.True(ulid1.Time.ToUnixTimeMilliseconds() <= ulid2.Time.ToUnixTimeMilliseconds()); + Assert.True(timestamp <= ulid1.Time.ToUnixTimeMilliseconds()); + Assert.True(timestamp <= ulid2.Time.ToUnixTimeMilliseconds()); + + if (isMonotonic ?? defaultIsMonotonic) + { + Assert.True(MemoryExtensions.SequenceCompareTo(ulid1.AsByteSpan(), ulid2.AsByteSpan()) < 0); + Assert.True(ulid1 < ulid2); + } } } diff --git a/src/ByteAether.Ulid/PACKAGE.md b/src/ByteAether.Ulid/PACKAGE.md index 628573a..bf654b5 100644 --- a/src/ByteAether.Ulid/PACKAGE.md +++ b/src/ByteAether.Ulid/PACKAGE.md @@ -68,11 +68,13 @@ The `Ulid` implementation provides the following properties and methods: ### Creation -- `Ulid.New(bool isMonotonic = true)`\ -Generates a new ULID. If `isMonotonic` is `true`, ensures monotonicity during timestamp collisions. -- `Ulid.New(DateTimeOffset dateTimeOffset, bool isMonotonic = true)`\ +- `Ulid.DefaultIsMonotonic = true`\ +Sets the default behavior for generating ULIDs unless overridden during generation. If `true` (default), ensures monotonicity during timestamp collisions. +- `Ulid.New(bool? isMonotonic = null)`\ +Generates a new ULID. If `isMonotonic` is `null` (default), uses `Ulid.DefaultIsMonotonic` for monotonicity setting. +- `Ulid.New(DateTimeOffset dateTimeOffset, bool? isMonotonic = null)`\ Generates a new ULID using the specified `DateTimeOffset`. -- `Ulid.New(long timestamp, bool isMonotonic = true)`\ +- `Ulid.New(long timestamp, bool? isMonotonic = null)`\ Generates a new ULID using the specified Unix timestamp in milliseconds (`long`). - `Ulid.New(DateTimeOffset dateTimeOffset, Span random)`\ Generates a new ULID using the specified `DateTimeOffset` and a pre-existing random byte array. diff --git a/src/ByteAether.Ulid/Ulid.New.cs b/src/ByteAether.Ulid/Ulid.New.cs index 87c6aab..585fdba 100644 --- a/src/ByteAether.Ulid/Ulid.New.cs +++ b/src/ByteAether.Ulid/Ulid.New.cs @@ -9,6 +9,19 @@ namespace ByteAether.Ulid; public readonly partial struct Ulid { + /// + /// 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. + ///
+ public static bool DefaultIsMonotonic { get; set; } = true; + private static readonly byte[] _lastUlid = new byte[_ulidSize]; private static readonly RandomNumberGenerator _rng = RandomNumberGenerator.Create(); @@ -19,56 +32,69 @@ public readonly partial struct Ulid #endif /// - /// Initializes a new instance of the struct using the specified byte array. + /// Initializes a new instance of the struct using the specified byte array. /// - /// The byte array to initialize the ULID with. + /// The byte array to initialize the with. + /// Given bytes as an instance. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Ulid New(ReadOnlySpan bytes) => MemoryMarshal.Read(bytes); /// - /// Creates a new ULID with the current timestamp. + /// Creates a new with the current timestamp. /// - /// If true, ensures the ULID is monotonically increasing. - /// A new ULID instance. + /// + /// If null (default), the value of is used to determine monotonicity.
+ /// If true, ensures the is monotonically increasing.
+ /// If false, generates a random part in . + /// + /// A new instance. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Ulid New(bool isMonotonic = true) + public static Ulid New(bool? isMonotonic = null) => New(DateTimeOffset.UtcNow, isMonotonic); /// - /// Creates a new ULID with the specified timestamp. + /// Creates a new with the specified timestamp. /// - /// The timestamp to use for the ULID. - /// If true, ensures the ULID is monotonically increasing. - /// A new ULID instance. + /// The timestamp to use for the . + /// + /// If null (default), the value of is used to determine monotonicity.
+ /// If true, ensures the is monotonically increasing.
+ /// If false, generates a random part in . + /// + /// A new instance. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Ulid New(DateTimeOffset dateTimeOffset, bool isMonotonic = true) + public static Ulid New(DateTimeOffset dateTimeOffset, bool? isMonotonic = null) => New(dateTimeOffset.ToUnixTimeMilliseconds(), isMonotonic); /// - /// Creates a new ULID with the specified timestamp. + /// Creates a new with the specified timestamp. /// - /// The timestamp to use for the ULID. + /// The timestamp to use for the . /// - /// A span containing the random component of the ULID. - /// It must be at least 10 bytes long, as the last 10 bytes of the ULID are derived from this span. + /// 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 ULID instance. + /// A new instance. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Ulid New(DateTimeOffset dateTimeOffset, Span random) => New(dateTimeOffset.ToUnixTimeMilliseconds(), random); /// - /// Creates a new ULID with the specified timestamp in milliseconds. + /// Creates a new with the specified timestamp in milliseconds. /// - /// The timestamp in milliseconds to use for the ULID. - /// If true, ensures the ULID is monotonically increasing. - /// A new ULID instance. + /// The timestamp in milliseconds to use for the . + /// + /// If null (default), the value of is used to determine monotonicity.
+ /// If true, ensures the is monotonically increasing.
+ /// If false, generates a random part in . + /// + /// A new instance. #if NET5_0_OR_GREATER [SkipLocalsInit] #endif [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Ulid New(long timestamp, bool isMonotonic = true) + public static Ulid New(long timestamp, bool? isMonotonic = null) { Ulid ulid = default; @@ -77,7 +103,7 @@ public static Ulid New(long timestamp, bool isMonotonic = true) var ulidBytes = new Span(Unsafe.AsPointer(ref Unsafe.AsRef(in ulid)), _ulidSize); FillTime(ulidBytes, timestamp); - FillRandom(ulidBytes, isMonotonic); + FillRandom(ulidBytes, isMonotonic ?? DefaultIsMonotonic); } return ulid; @@ -88,11 +114,11 @@ public static Ulid New(long timestamp, bool isMonotonic = true) /// /// /// 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 ULID. + /// This value will be encoded into the first 6 bytes of the . /// /// - /// A span containing the random component of the ULID. - /// It must be at least 10 bytes long, as the last 10 bytes of the ULID are derived from this span. + /// 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.