Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for creating ID from timestamps #14

Merged
merged 4 commits into from
Feb 28, 2024
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -348,3 +348,5 @@ MigrationBackup/

# Ionide (cross platform F# VS Code tools) working folder
.ionide/

.DS_Store
31 changes: 29 additions & 2 deletions src/FlakeId.Tests/IdTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FlakeId.Extensions;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace FlakeId.Tests;
Expand All @@ -17,6 +18,23 @@ public void Id_Create()
Assert.IsTrue(id > 1);
}

[TestMethod]
public void Id_CreateFromTimeStamp()
{
DateTimeOffset now = DateTimeOffset.UtcNow;
long timeStamp = now.ToUnixTimeMilliseconds();
Id id = Id.Create(timeStamp);

Assert.AreEqual(timeStamp, id.ToUnixTimeMilliseconds());
}

[TestMethod]
public void Id_CreateFromTimeStamp_RecentTimeStamp()
{
Assert.ThrowsException<ArgumentException>(
() => Id.Create(new DateTimeOffset(2010, 1, 1, 0, 0, 0, 0, TimeSpan.Zero).ToUnixTimeMilliseconds()));
}

[TestMethod]
public void Id_CreateManyFast()
{
Expand All @@ -31,7 +49,7 @@ public void Id_CreateManyFast()
[TestMethod]
public async Task Id_CreateManyDelayed()
{
List<Id> ids = new();
List<Id> ids = [];

for (int i = 0; i < 100; i++)
{
Expand Down Expand Up @@ -60,7 +78,7 @@ public void Id_Sortable()
{
// The sequence in which Ids are generated should be equal to a set of sorted Ids.
Id[] ids = Enumerable.Range(0, 1000).Select(_ => Id.Create()).ToArray();
Id[] sorted = ids.OrderBy(i => i).ToArray();
Id[] sorted = [.. ids.OrderBy(i => i)];

Assert.IsTrue(ids.SequenceEqual(sorted));
}
Expand All @@ -72,4 +90,13 @@ public void Id_ToString()

Assert.AreEqual(id.ToString(), id.ToString());
}

[TestMethod]
public void Id_ContainsTimeZoneComponent()
{
DateTimeOffset timeStamp = new(2020, 1, 1, 0, 0, 0, TimeSpan.FromHours(7));
Id id = Id.Create(timeStamp.ToUnixTimeMilliseconds());

Assert.AreEqual(timeStamp.ToUnixTimeMilliseconds(), id.ToUnixTimeMilliseconds());
}
}
13 changes: 13 additions & 0 deletions src/FlakeId/Extensions/IdExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,18 @@ public static string ToStringIdentifier(this Id id)

return Convert.ToBase64String(Encoding.UTF8.GetBytes(identifier));
}

/// <summary>
/// Returns an ID that is valid for the specified timestamp.
/// Note that consecutive calls with the same timestamp will yield different IDs, as the other components of the ID will still differ.
/// In other words, the time component of the ID is guaranteed to be equal to the specified timestamp.
/// </summary>
/// <param name="id"></param>
/// <param name="timeStamp"></param>
/// <returns></returns>
public static Id FromDateTimeOffset(this Id id, DateTimeOffset timeStamp)
{
return Id.Create(timeStamp.ToUnixTimeMilliseconds());
}
}
}
41 changes: 34 additions & 7 deletions src/FlakeId/Id.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,33 @@ public static Id Create()
return id;
}

/// <summary>
/// Creates a new ID based on the provided timestamp in milliseconds.
/// When using this overload, make sure you take the timezone of the provided timestamp into consideration.
/// </summary>
/// <param name="timeStampMs"></param>
/// <returns></returns>
/// <exception cref="ArgumentOutOfRangeException">Timestamps can not be negative</exception>
public static Id Create(long timeStampMs)
{
if (timeStampMs < 0)
{
throw new ArgumentOutOfRangeException(nameof(timeStampMs));
}

Id id = new Id();
long relativeTimeStamp = timeStampMs - MonotonicTimer.Epoch.ToUnixTimeMilliseconds();

if (relativeTimeStamp < 0)
{
throw new ArgumentException("Specified timestamp would result in a negative ID (it's before instance epoch)");
}

id.CreateInternal(relativeTimeStamp);

return id;
}

/// <summary>
/// Attempts to parse an ID from the specified <see cref="long" /> value. This method will return false if the
/// specified value doesn't match the shape of a snowflake ID.
Expand Down Expand Up @@ -110,9 +137,9 @@ public static Id Parse(long value)
return id;
}

private void CreateInternal()
private void CreateInternal(long timeStampMs = 0)
{
long milliseconds = MonotonicTimer.ElapsedMilliseconds;
long milliseconds = timeStampMs == 0 ? MonotonicTimer.ElapsedMilliseconds : timeStampMs;
long timestamp = milliseconds & TimestampMask;
int threadId = Thread.CurrentThread.ManagedThreadId & ThreadIdMask;
int processId = s_processId ??= Process.GetCurrentProcess().Id & ProcessIdMask;
Expand All @@ -130,20 +157,20 @@ private void CreateInternal()
}
}

public override string ToString() => _value.ToString();
public override readonly string ToString() => _value.ToString();

public static implicit operator long(Id id) => id._value;

public static bool operator ==(Id left, Id right) => left._value == right._value;

public static bool operator !=(Id left, Id right) => !(left == right);

public int CompareTo(Id other) => _value.CompareTo(other._value);
public readonly int CompareTo(Id other) => _value.CompareTo(other._value);

public bool Equals(Id other) => _value == other._value;
public readonly bool Equals(Id other) => _value == other._value;

public override bool Equals(object obj) => obj is Id other && Equals(other);
public override readonly bool Equals(object obj) => obj is Id other && Equals(other);

public override int GetHashCode() => _value.GetHashCode();
public override readonly int GetHashCode() => _value.GetHashCode();
}
}