diff --git a/.gitignore b/.gitignore index dfcfd56..39ae32e 100644 --- a/.gitignore +++ b/.gitignore @@ -348,3 +348,5 @@ MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ + +.DS_Store \ No newline at end of file diff --git a/src/FlakeId.Tests/IdTests.cs b/src/FlakeId.Tests/IdTests.cs index b8c00ba..23a2d60 100644 --- a/src/FlakeId.Tests/IdTests.cs +++ b/src/FlakeId.Tests/IdTests.cs @@ -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; @@ -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( + () => Id.Create(new DateTimeOffset(2010, 1, 1, 0, 0, 0, 0, TimeSpan.Zero).ToUnixTimeMilliseconds())); + } + [TestMethod] public void Id_CreateManyFast() { @@ -31,7 +49,7 @@ public void Id_CreateManyFast() [TestMethod] public async Task Id_CreateManyDelayed() { - List ids = new(); + List ids = []; for (int i = 0; i < 100; i++) { @@ -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)); } @@ -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()); + } } diff --git a/src/FlakeId/Extensions/IdExtensions.cs b/src/FlakeId/Extensions/IdExtensions.cs index 28b4692..54c830b 100644 --- a/src/FlakeId/Extensions/IdExtensions.cs +++ b/src/FlakeId/Extensions/IdExtensions.cs @@ -56,5 +56,18 @@ public static string ToStringIdentifier(this Id id) return Convert.ToBase64String(Encoding.UTF8.GetBytes(identifier)); } + + /// + /// 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. + /// + /// + /// + /// + public static Id FromDateTimeOffset(this Id id, DateTimeOffset timeStamp) + { + return Id.Create(timeStamp.ToUnixTimeMilliseconds()); + } } } diff --git a/src/FlakeId/Id.cs b/src/FlakeId/Id.cs index 460c019..ede2957 100644 --- a/src/FlakeId/Id.cs +++ b/src/FlakeId/Id.cs @@ -70,6 +70,33 @@ public static Id Create() return id; } + /// + /// 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. + /// + /// + /// + /// Timestamps can not be negative + 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; + } + /// /// Attempts to parse an ID from the specified value. This method will return false if the /// specified value doesn't match the shape of a snowflake ID. @@ -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; @@ -130,7 +157,7 @@ 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; @@ -138,12 +165,12 @@ private void CreateInternal() 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(); } }