Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<byte> random)`\
Generates a new ULID using the specified `DateTimeOffset` and a pre-existing random byte array.
Expand Down
8 changes: 4 additions & 4 deletions src/ByteAether.Ulid.Tests/Ulid.Comparable.Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand Down
98 changes: 75 additions & 23 deletions src/ByteAether.Ulid.Tests/Ulid.New.Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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);
}
}
}
10 changes: 6 additions & 4 deletions src/ByteAether.Ulid/PACKAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<byte> random)`\
Generates a new ULID using the specified `DateTimeOffset` and a pre-existing random byte array.
Expand Down
76 changes: 51 additions & 25 deletions src/ByteAether.Ulid/Ulid.New.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,19 @@ namespace ByteAether.Ulid;

public readonly partial struct Ulid
{
/// <summary>
/// Whether <see cref="Ulid"/>s should be generated in a monotonic manner by default.<br />
/// Initial value is set to <c>true</c>.<br/>
/// <b>This setting applies globally without any scoping.</b>
/// </summary>
/// <remarks>
/// When set to <c>true</c> (default), <see cref="Ulid"/>s generated without explicitly specifying monotonicity
/// will ensure that they are monotonically increasing.<br />
/// When set to <c>false</c>, <see cref="Ulid"/>s generated without explicitly specifying monotonicity will be
/// generated with random <see cref="Random" /> value.
/// </remarks>
public static bool DefaultIsMonotonic { get; set; } = true;

private static readonly byte[] _lastUlid = new byte[_ulidSize];
private static readonly RandomNumberGenerator _rng = RandomNumberGenerator.Create();

Expand All @@ -19,56 +32,69 @@ public readonly partial struct Ulid
#endif

/// <summary>
/// Initializes a new instance of the <see cref="ByteAether.Ulid"/> struct using the specified byte array.
/// Initializes a new instance of the <see cref="Ulid"/> struct using the specified byte array.
/// </summary>
/// <param name="bytes">The byte array to initialize the ULID with.</param>
/// <param name="bytes">The byte array to initialize the <see cref="Ulid"/> with.</param>
/// <returns>Given bytes as an <see cref="Ulid"/> instance.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Ulid New(ReadOnlySpan<byte> bytes)
=> MemoryMarshal.Read<Ulid>(bytes);

/// <summary>
/// Creates a new ULID with the current timestamp.
/// Creates a new <see cref="Ulid"/> with the current timestamp.
/// </summary>
/// <param name="isMonotonic">If true, ensures the ULID is monotonically increasing.</param>
/// <returns>A new ULID instance.</returns>
/// <param name="isMonotonic">
/// If <c>null</c> (default), the value of <see cref="DefaultIsMonotonic"/> is used to determine monotonicity.<br />
/// If <c>true</c>, ensures the <see cref="Ulid"/> is monotonically increasing.<br />
/// If <c>false</c>, generates a random <see cref="Random" /> part in <see cref="Ulid"/>.
/// </param>
/// <returns>A new <see cref="Ulid"/> instance.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Ulid New(bool isMonotonic = true)
public static Ulid New(bool? isMonotonic = null)
=> New(DateTimeOffset.UtcNow, isMonotonic);

/// <summary>
/// Creates a new ULID with the specified timestamp.
/// Creates a new <see cref="Ulid"/> with the specified timestamp.
/// </summary>
/// <param name="dateTimeOffset">The timestamp to use for the ULID.</param>
/// <param name="isMonotonic">If true, ensures the ULID is monotonically increasing.</param>
/// <returns>A new ULID instance.</returns>
/// <param name="dateTimeOffset">The timestamp to use for the <see cref="Ulid"/>.</param>
/// <param name="isMonotonic">
/// If <c>null</c> (default), the value of <see cref="DefaultIsMonotonic"/> is used to determine monotonicity.<br />
/// If <c>true</c>, ensures the <see cref="Ulid"/> is monotonically increasing.<br />
/// If <c>false</c>, generates a random <see cref="Random" /> part in <see cref="Ulid"/>.
/// </param>
/// <returns>A new <see cref="Ulid"/> instance.</returns>
[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);

/// <summary>
/// Creates a new ULID with the specified timestamp.
/// Creates a new <see cref="Ulid"/> with the specified timestamp.
/// </summary>
/// <param name="dateTimeOffset">The timestamp to use for the ULID.</param>
/// <param name="dateTimeOffset">The timestamp to use for the <see cref="Ulid"/>.</param>
/// <param name="random" >
/// 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 <see cref="Ulid"/>.
/// It must be at least 10 bytes long, as the last 10 bytes of the <see cref="Ulid"/> are derived from this span.
/// </param>
/// <returns>A new ULID instance.</returns>
/// <returns>A new <see cref="Ulid"/> instance.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Ulid New(DateTimeOffset dateTimeOffset, Span<byte> random)
=> New(dateTimeOffset.ToUnixTimeMilliseconds(), random);

/// <summary>
/// Creates a new ULID with the specified timestamp in milliseconds.
/// Creates a new <see cref="Ulid"/> with the specified timestamp in milliseconds.
/// </summary>
/// <param name="timestamp">The timestamp in milliseconds to use for the ULID.</param>
/// <param name="isMonotonic">If true, ensures the ULID is monotonically increasing.</param>
/// <returns>A new ULID instance.</returns>
/// <param name="timestamp">The timestamp in milliseconds to use for the <see cref="Ulid"/>.</param>
/// <param name="isMonotonic">
/// If <c>null</c> (default), the value of <see cref="DefaultIsMonotonic"/> is used to determine monotonicity.<br />
/// If <c>true</c>, ensures the <see cref="Ulid"/> is monotonically increasing.<br />
/// If <c>false</c>, generates a random <see cref="Random" /> part in <see cref="Ulid"/>.
/// </param>
/// <returns>A new <see cref="Ulid"/> instance.</returns>
#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;

Expand All @@ -77,7 +103,7 @@ public static Ulid New(long timestamp, bool isMonotonic = true)
var ulidBytes = new Span<byte>(Unsafe.AsPointer(ref Unsafe.AsRef(in ulid)), _ulidSize);

FillTime(ulidBytes, timestamp);
FillRandom(ulidBytes, isMonotonic);
FillRandom(ulidBytes, isMonotonic ?? DefaultIsMonotonic);
}

return ulid;
Expand All @@ -88,11 +114,11 @@ public static Ulid New(long timestamp, bool isMonotonic = true)
/// </summary>
/// <param name="timestamp">
/// 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 <see cref="Ulid"/>.
/// </param>
/// <param name="random">
/// 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 <see cref="Ulid"/>.
/// It must be at least 10 bytes long, as the last 10 bytes of the <see cref="Ulid"/> are derived from this span.
/// </param>
/// <returns>
/// A new <see cref="Ulid"/> instance composed of the given timestamp and random byte sequence.
Expand Down