diff --git a/DotNetThoughts.LocalTimeKit/DotNetThoughts.LocalTimeKit.csproj b/DotNetThoughts.LocalTimeKit/DotNetThoughts.LocalTimeKit.csproj new file mode 100644 index 0000000..baec30d --- /dev/null +++ b/DotNetThoughts.LocalTimeKit/DotNetThoughts.LocalTimeKit.csproj @@ -0,0 +1,9 @@ + + + + 1.0.0 + True + MIT + + + diff --git a/TimeKeeping/DotNetThoughts.TimeKeeping/LocalDateTime.cs b/DotNetThoughts.LocalTimeKit/LocalDateTime.cs similarity index 90% rename from TimeKeeping/DotNetThoughts.TimeKeeping/LocalDateTime.cs rename to DotNetThoughts.LocalTimeKit/LocalDateTime.cs index 8b60804..9140778 100644 --- a/TimeKeeping/DotNetThoughts.TimeKeeping/LocalDateTime.cs +++ b/DotNetThoughts.LocalTimeKit/LocalDateTime.cs @@ -1,4 +1,4 @@ -namespace DotNetThoughts.TimeKeeping; +namespace DotNetThoughts.LocalTimeKit; /// /// Unambigious representation of a local datetime @@ -13,9 +13,9 @@ public LocalDateTime(DateTimeOffset dateTimeOffset, TimeZoneInfo timeZoneInfo) public LocalDateTime(DateTime dateTime, TimeZoneInfo timeZoneInfo) { - if (!((dateTime.Kind == DateTimeKind.Local && timeZoneInfo == TimeZoneInfo.Local) - || (dateTime.Kind == DateTimeKind.Utc && timeZoneInfo == TimeZoneInfo.Utc) - || (dateTime.Kind == DateTimeKind.Unspecified && timeZoneInfo != TimeZoneInfo.Local))) + if (!(dateTime.Kind == DateTimeKind.Local && timeZoneInfo == TimeZoneInfo.Local + || dateTime.Kind == DateTimeKind.Utc && timeZoneInfo == TimeZoneInfo.Utc + || dateTime.Kind == DateTimeKind.Unspecified && timeZoneInfo != TimeZoneInfo.Local)) { throw new ArgumentException("Invalid combinations of datetime kinds and timezoneinfo"); } diff --git a/DotNetThoughts.TimeTraveler/DotNetThoughts.TimeTraveler.csproj b/DotNetThoughts.TimeTraveler/DotNetThoughts.TimeTraveler.csproj new file mode 100644 index 0000000..8f93d8a --- /dev/null +++ b/DotNetThoughts.TimeTraveler/DotNetThoughts.TimeTraveler.csproj @@ -0,0 +1,7 @@ + + + 1.0.0 + True + MIT + + diff --git a/DotNetThoughts.TimeTraveler/TimeTravelersClock.cs b/DotNetThoughts.TimeTraveler/TimeTravelersClock.cs new file mode 100644 index 0000000..ad919e6 --- /dev/null +++ b/DotNetThoughts.TimeTraveler/TimeTravelersClock.cs @@ -0,0 +1,139 @@ +namespace DotNetThoughts.TimeTraveler; + + +public class TimeTravelersClock +{ + public DateTimeOffset? FreezedAt { get; set; } + public DateTimeOffset? Frozen { get; set; } + public TimeSpan Offset { get; set; } = TimeSpan.Zero; + + public bool IsFrozen() + { + using var context = new OperationContext(); + return FreezedAt != null; + } + + public DateTimeOffset Reset() + { + using var context = new OperationContext(); + Frozen = null; + FreezedAt = null; + Offset = TimeSpan.Zero; + return UtcNow(); + } + + public DateTimeOffset Thaw() + { + using var context = new OperationContext(); + if (FreezedAt == null) + { + throw new InvalidOperationException("Cannot thaw when not frozen"); + } + Offset = Offset.Add(-RealTimeFrozen()); + FreezedAt = null; + Frozen = null; + return UtcNow(); + } + + public DateTimeOffset Freeze() + { + using var context = new OperationContext(); + FreezedAt = context.UtcNow(); + Frozen = UtcNow(); + return Frozen.Value; + } + + public DateTimeOffset Freeze(DateTimeOffset baseLine) + { + using var context = new OperationContext(); + Reset(); + SetBaseline(baseLine); + FreezedAt = context.UtcNow(); + Frozen = UtcNow(); + return Frozen.Value; + } + + public DateTimeOffset UtcNow() + { + using var context = new OperationContext(); + return Frozen ?? context.UtcNow().Add(Offset); + } + + public DateTimeOffset AdvanceDays(double days) + { + using var context = new OperationContext(); + return Advance(TimeSpan.FromDays(days)); + } + + public DateTimeOffset Advance(TimeSpan timeSpan) + { + using var context = new OperationContext(); + Offset = Offset.Add(timeSpan); + if (IsFrozen()) + { + Frozen = Frozen!.Value.Add(timeSpan); + } + return UtcNow(); + } + + public DateTimeOffset SetBaseline(DateTimeOffset baseLine) + { + if (IsFrozen()) throw new InvalidOperationException(); + using var context = new OperationContext(); + Offset = baseLine - context.UtcNow(); + return UtcNow(); + } + + public DateTimeOffset SetOffset(TimeSpan offset) + { + if (IsFrozen()) throw new InvalidOperationException(); + using var context = new OperationContext(); + Offset = offset; + + return UtcNow(); + } + + private TimeSpan RealTimeFrozen() + { + using var context = new OperationContext(); + return FreezedAt != null ? context.UtcNow() - FreezedAt.Value : TimeSpan.Zero; + } + + internal void From(TimeTravelersClock xSystemTime) + { + using var context = new OperationContext(); + Offset = xSystemTime.Offset; + FreezedAt = xSystemTime.FreezedAt; + Frozen = xSystemTime.Frozen; + } + + internal bool IsManipulated() + { + return Offset != TimeSpan.Zero || FreezedAt != null; + } + + internal class OperationContext : IDisposable + { + private static AsyncLocal _operationNow = new AsyncLocal(); + + private bool _shouldReset = false; + + public OperationContext() + { + if (_operationNow.Value == null) + { + _operationNow.Value = DateTimeOffset.UtcNow; + _shouldReset = true; + } + } + public void Dispose() + { + if (_shouldReset) + { + _operationNow.Value = null; + } + } + + public DateTimeOffset UtcNow() => _operationNow.Value!.Value; + } +} \ No newline at end of file diff --git a/DotNetThoughts.sln b/DotNetThoughts.sln index 9d39be7..5277c19 100644 --- a/DotNetThoughts.sln +++ b/DotNetThoughts.sln @@ -50,6 +50,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution ReadMe.md = ReadMe.md EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNetThoughts.LocalTimeKit", "DotNetThoughts.LocalTimeKit\DotNetThoughts.LocalTimeKit.csproj", "{03DA1023-FDF4-4EF5-8951-19806434F8CA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -100,6 +102,10 @@ Global {E0E86A0B-F4CE-4E88-99CF-C54AFC1C0C58}.Debug|Any CPU.Build.0 = Debug|Any CPU {E0E86A0B-F4CE-4E88-99CF-C54AFC1C0C58}.Release|Any CPU.ActiveCfg = Release|Any CPU {E0E86A0B-F4CE-4E88-99CF-C54AFC1C0C58}.Release|Any CPU.Build.0 = Release|Any CPU + {03DA1023-FDF4-4EF5-8951-19806434F8CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {03DA1023-FDF4-4EF5-8951-19806434F8CA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {03DA1023-FDF4-4EF5-8951-19806434F8CA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {03DA1023-FDF4-4EF5-8951-19806434F8CA}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -116,6 +122,7 @@ Global {66DCA5D2-4A06-4F1A-8DBB-36118CC01A34} = {BECD3795-EDD5-459E-BFCC-017082E1C34D} {3D9F5F06-E09D-46B4-8058-1458467E0252} = {BECD3795-EDD5-459E-BFCC-017082E1C34D} {E0E86A0B-F4CE-4E88-99CF-C54AFC1C0C58} = {BECD3795-EDD5-459E-BFCC-017082E1C34D} + {03DA1023-FDF4-4EF5-8951-19806434F8CA} = {695E363E-055E-4ED5-9613-587E02A67578} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {CAA5B6BA-B6C8-4A90-B9E6-D7634295532E} diff --git a/TimeKeeping/DotNetThoughts.TimeKeeping.App/DotNetThoughts.TimeKeeping.App.csproj b/TimeKeeping/DotNetThoughts.TimeKeeping.App/DotNetThoughts.TimeKeeping.App.csproj index 5d964d3..b6d4aab 100644 --- a/TimeKeeping/DotNetThoughts.TimeKeeping.App/DotNetThoughts.TimeKeeping.App.csproj +++ b/TimeKeeping/DotNetThoughts.TimeKeeping.App/DotNetThoughts.TimeKeeping.App.csproj @@ -5,6 +5,7 @@ + diff --git a/TimeKeeping/DotNetThoughts.TimeKeeping.App/Pages/Timer.razor b/TimeKeeping/DotNetThoughts.TimeKeeping.App/Pages/Timer.razor index 21f3c4c..29b481b 100644 --- a/TimeKeeping/DotNetThoughts.TimeKeeping.App/Pages/Timer.razor +++ b/TimeKeeping/DotNetThoughts.TimeKeeping.App/Pages/Timer.razor @@ -1,4 +1,5 @@ @page "/timer" +@using DotNetThoughts.LocalTimeKit Timing diff --git a/TimeKeeping/DotNetThoughts.TimeKeeping.App/packages.lock.json b/TimeKeeping/DotNetThoughts.TimeKeeping.App/packages.lock.json index cbd6ef3..03e7df9 100644 --- a/TimeKeeping/DotNetThoughts.TimeKeeping.App/packages.lock.json +++ b/TimeKeeping/DotNetThoughts.TimeKeeping.App/packages.lock.json @@ -231,6 +231,9 @@ "System.Text.Encodings.Web": "8.0.0" } }, + "dotnetthoughts.localtimekit": { + "type": "Project" + }, "dotnetthoughts.timekeeping": { "type": "Project" } diff --git a/TimeKeeping/DotNetThoughts.TimeKeeping.Tests/DotNetThoughts.TimeKeeping.Tests.csproj b/TimeKeeping/DotNetThoughts.TimeKeeping.Tests/DotNetThoughts.TimeKeeping.Tests.csproj index 6165ba6..2a6bfc8 100644 --- a/TimeKeeping/DotNetThoughts.TimeKeeping.Tests/DotNetThoughts.TimeKeeping.Tests.csproj +++ b/TimeKeeping/DotNetThoughts.TimeKeeping.Tests/DotNetThoughts.TimeKeeping.Tests.csproj @@ -1,6 +1,7 @@  + diff --git a/TimeKeeping/DotNetThoughts.TimeKeeping.Tests/LocalDateTimeTests.cs b/TimeKeeping/DotNetThoughts.TimeKeeping.Tests/LocalDateTimeTests.cs index 3ee5d7c..1ad93bf 100644 --- a/TimeKeeping/DotNetThoughts.TimeKeeping.Tests/LocalDateTimeTests.cs +++ b/TimeKeeping/DotNetThoughts.TimeKeeping.Tests/LocalDateTimeTests.cs @@ -1,4 +1,6 @@ -namespace DotNetThoughts.TimeKeeping.Tests; +using DotNetThoughts.LocalTimeKit; + +namespace DotNetThoughts.TimeKeeping.Tests; public class LocalDateTimeTests { diff --git a/TimeKeeping/DotNetThoughts.TimeKeeping.Tests/packages.lock.json b/TimeKeeping/DotNetThoughts.TimeKeeping.Tests/packages.lock.json index baa83d6..47d7ce7 100644 --- a/TimeKeeping/DotNetThoughts.TimeKeeping.Tests/packages.lock.json +++ b/TimeKeeping/DotNetThoughts.TimeKeeping.Tests/packages.lock.json @@ -167,6 +167,9 @@ "xunit.extensibility.core": "[2.8.1]" } }, + "dotnetthoughts.localtimekit": { + "type": "Project" + }, "dotnetthoughts.timekeeping": { "type": "Project" } diff --git a/TimeKeeping/DotNetThoughts.TimeKeeping/DotNetThoughts.TimeKeeping.csproj b/TimeKeeping/DotNetThoughts.TimeKeeping/DotNetThoughts.TimeKeeping.csproj index 69a8208..18987c9 100644 --- a/TimeKeeping/DotNetThoughts.TimeKeeping/DotNetThoughts.TimeKeeping.csproj +++ b/TimeKeeping/DotNetThoughts.TimeKeeping/DotNetThoughts.TimeKeeping.csproj @@ -1,6 +1,6 @@  - 1.1.0 + 1.2.0 True MIT diff --git a/TimeKeeping/DotNetThoughts.TimeKeeping/SystemTime.cs b/TimeKeeping/DotNetThoughts.TimeKeeping/SystemTime.cs index c5af7a0..b26e989 100644 --- a/TimeKeeping/DotNetThoughts.TimeKeeping/SystemTime.cs +++ b/TimeKeeping/DotNetThoughts.TimeKeeping/SystemTime.cs @@ -2,7 +2,6 @@ /// /// "Mockable" DateTime that you still can use statically using the ambient context anti-pattern. Let's see how it rolls. -/// Replace with an IDateTime if problems arise. /// public class SystemTime { diff --git a/TimeKeeping/DotNetThoughts.TimeKeeping/TimeTravelersClock.cs b/TimeKeeping/DotNetThoughts.TimeKeeping/TimeTravelersClock.cs index 986cead..952146c 100644 --- a/TimeKeeping/DotNetThoughts.TimeKeeping/TimeTravelersClock.cs +++ b/TimeKeeping/DotNetThoughts.TimeKeeping/TimeTravelersClock.cs @@ -1,6 +1,5 @@ namespace DotNetThoughts.TimeKeeping; - public class TimeTravelersClock { public DateTimeOffset? FreezedAt { get; set; } @@ -96,7 +95,7 @@ public DateTimeOffset SetOffset(TimeSpan offset) private TimeSpan RealTimeFrozen() { using var context = new OperationContext(); - return FreezedAt != null ? (context.UtcNow() - FreezedAt.Value) : TimeSpan.Zero; + return FreezedAt != null ? context.UtcNow() - FreezedAt.Value : TimeSpan.Zero; } internal void From(TimeTravelersClock xSystemTime)