-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
6ebd541
commit 9816e7d
Showing
2 changed files
with
217 additions
and
0 deletions.
There are no files selected for viewing
121 changes: 121 additions & 0 deletions
121
TimeKeeping/DotNetThoughts.TimeKeeping.Tests/LocalDateTimeTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
namespace DotNetThoughts.TimeKeeping.Tests; | ||
public class LocalDateTimeTests | ||
{ | ||
|
||
[Fact] | ||
public void TestLocalDateTimeOffset() | ||
{ | ||
var t = new DateTime(2024,06,06, 0, 0, 0, DateTimeKind.Unspecified); | ||
var tz = TimeZoneInfo.FindSystemTimeZoneById("Central European Standard Time"); | ||
var ldt = new LocalDateTime(t, tz); | ||
var expectedDateTimeOffset = DateTimeOffset.Parse("2024-06-06T00:00:00+02:00"); | ||
ldt.ToDateTimeOffsetLocal().Should().Be(expectedDateTimeOffset); | ||
} | ||
|
||
[Fact] | ||
public void TestDateConstructor() | ||
{ | ||
var t = new DateTime(2024, 06, 06, 0, 0, 0, DateTimeKind.Unspecified); | ||
var d = new DateOnly(2024, 06, 06); | ||
var tz = TimeZoneInfo.FindSystemTimeZoneById("Central European Standard Time"); | ||
var ldt = new LocalDateTime(t, tz); | ||
var ldt2 = new LocalDateTime(d, tz); | ||
|
||
ldt.DateTime.Should().Be(ldt2.DateTime); | ||
} | ||
|
||
[Fact] | ||
public void TestUtcDateTimeOffset() | ||
{ | ||
var t = new DateTime(2024, 06, 06, 0, 0, 0, DateTimeKind.Unspecified); | ||
var tz = TimeZoneInfo.FindSystemTimeZoneById("Central European Standard Time"); | ||
var ldt = new LocalDateTime(t, tz); | ||
var expectedUtcDateTimeOffset = DateTimeOffset.Parse("2024-06-05T22:00:00+00:00"); | ||
ldt.ToDateTimeOffsetUtc().Should().Be(expectedUtcDateTimeOffset); | ||
} | ||
|
||
[Fact] | ||
public void InTheMiddleOfSummerTimeShift() | ||
{ | ||
var t = new DateTime(2024, 03, 31, 2, 30, 0, DateTimeKind.Unspecified); | ||
var tz = TimeZoneInfo.FindSystemTimeZoneById("Central European Standard Time"); | ||
var ldt = () => new LocalDateTime(t, tz); | ||
ldt.Should().Throw<ArgumentException>(); | ||
} | ||
|
||
[Fact] | ||
public void InTheMiddleOfWinterTimeShift() | ||
{ | ||
var t = new DateTime(2024, 10, 27, 2, 30, 0, DateTimeKind.Unspecified); | ||
var tz = TimeZoneInfo.FindSystemTimeZoneById("Central European Standard Time"); | ||
var ldt = () => new LocalDateTime(t, tz); | ||
ldt.Should().Throw<ArgumentException>(); | ||
} | ||
|
||
[Fact] | ||
public void TestLocalMidnight() | ||
{ | ||
var t = new DateTime(2024, 06, 06, 13, 2, 1, DateTimeKind.Unspecified); | ||
var tz = TimeZoneInfo.FindSystemTimeZoneById("Central European Standard Time"); | ||
var ldt = new LocalDateTime(t, tz).ToMidnight(); | ||
var expectedDateTimeOffset = DateTimeOffset.Parse("2024-06-06T00:00:00+02:00"); | ||
ldt.ToDateTimeOffsetLocal().Should().Be(expectedDateTimeOffset); | ||
} | ||
|
||
[Fact] | ||
public void LocalTimeKindaWorks() | ||
{ | ||
var nowInContextOfTest = DateTimeOffset.Now; | ||
|
||
var t = new DateTime(nowInContextOfTest.Ticks, DateTimeKind.Local); | ||
var tz = TimeZoneInfo.Local; | ||
var ldt = new LocalDateTime(t, tz); | ||
ldt.ToDateTimeOffsetLocal().Should().Be(nowInContextOfTest); | ||
} | ||
|
||
[Fact] | ||
public void LocalTimeKindaWorksWithUtc() | ||
{ | ||
var nowInContextOfTest = DateTimeOffset.Now; | ||
var utcNow = nowInContextOfTest.ToUniversalTime(); | ||
var t = new DateTime(nowInContextOfTest.Ticks, DateTimeKind.Local); | ||
var tz = TimeZoneInfo.Local; | ||
var ldt = new LocalDateTime(t, tz); | ||
ldt.ToDateTimeOffsetUtc().Should().Be(utcNow); | ||
} | ||
|
||
[Theory] | ||
[InlineData("2022-01-01T16:00:00", 1, "2022-01-01T00:00:00", "2022-01-01T00:00:00+01:00", "2021-12-31T23:00:00+00:00")] | ||
[InlineData("2022-01-01T00:00:00", 1, "2021-12-31T00:00:00", "2021-12-31T00:00:00+01:00", "2021-12-30T23:00:00+00:00")] | ||
[InlineData("2022-01-01T16:00:00", 2, "2021-12-31T00:00:00", "2021-12-31T00:00:00+01:00", "2021-12-30T23:00:00+00:00")] | ||
[InlineData("2024-04-05T16:00:00", 7, "2024-03-30T00:00:00", "2024-03-30T00:00:00+01:00", "2024-03-29T23:00:00+00:00")] | ||
[InlineData("2024-10-28T16:00:00", 3, "2024-10-26T00:00:00", "2024-10-26T00:00:00+02:00", "2024-10-25T22:00:00+00:00")] | ||
public void PastMidnightsTest(string date, int midnights, string expected, string expectedLocalOffset, string expectedUtcOffset) | ||
{ | ||
var t = DateTime.Parse(date); | ||
var tz = TimeZoneInfo.FindSystemTimeZoneById("Central European Standard Time"); | ||
var ldt = new LocalDateTime(t, tz); | ||
var past = ldt.PastMidnights(midnights); | ||
past.Should().Be(new LocalDateTime(DateTime.Parse(expected), tz)); | ||
past.ToDateTimeOffsetLocal().Should().Be(DateTimeOffset.Parse(expectedLocalOffset)); | ||
past.ToDateTimeOffsetUtc().Should().Be(DateTimeOffset.Parse(expectedUtcOffset)); | ||
} | ||
|
||
[Theory] | ||
[InlineData("2022-01-01T16:00:00", 1, "2022-01-02T00:00:00", "2022-01-02T00:00:00+01:00", "2022-01-01T23:00:00+00:00")] | ||
[InlineData("2022-01-01T00:00:00", 1, "2022-01-02T00:00:00", "2022-01-02T00:00:00+01:00", "2022-01-01T23:00:00+00:00")] | ||
[InlineData("2022-01-01T16:00:00", 2, "2022-01-03T00:00:00", "2022-01-03T00:00:00+01:00", "2022-01-02T23:00:00+00:00")] | ||
[InlineData("2024-03-30T16:00:00", 6, "2024-04-05T00:00:00", "2024-04-05T00:00:00+02:00", "2024-04-04T22:00:00+00:00")] | ||
[InlineData("2024-10-26T16:00:00", 2, "2024-10-28T00:00:00", "2024-10-28T00:00:00+01:00", "2024-10-27T23:00:00+00:00")] | ||
public void FutureMidnightsTest(string date, int midnights, string expected, string expectedLocalOffset, string expectedUtcOffset) | ||
{ | ||
var t = DateTime.Parse(date); | ||
var tz = TimeZoneInfo.FindSystemTimeZoneById("Central European Standard Time"); | ||
var ldt = new LocalDateTime(t, tz); | ||
var future = ldt.FutureMidnights(midnights); | ||
future.Should().Be(new LocalDateTime(DateTime.Parse(expected), tz)); | ||
future.ToDateTimeOffsetLocal().Should().Be(DateTimeOffset.Parse(expectedLocalOffset)); | ||
future.ToDateTimeOffsetUtc().Should().Be(DateTimeOffset.Parse(expectedUtcOffset)); | ||
|
||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
namespace DotNetThoughts.TimeKeeping; | ||
|
||
/// <summary> | ||
/// Unambigious representation of a local datetime | ||
/// </summary> | ||
public readonly record struct LocalDateTime | ||
{ | ||
public LocalDateTime(DateOnly date, TimeZoneInfo timeZoneInfo) | ||
: this(date.ToDateTime(TimeOnly.MinValue), 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))) | ||
{ | ||
throw new ArgumentException("Invalid combinations of datetime kinds and timezoneinfo"); | ||
} | ||
|
||
if (timeZoneInfo.IsInvalidTime(dateTime)) | ||
{ | ||
throw new ArgumentException("dateTime is invalid for the timezone"); | ||
} | ||
|
||
if (timeZoneInfo.IsAmbiguousTime(dateTime)) | ||
{ | ||
throw new ArgumentException("dateTime is ambiguous for the timezone"); | ||
} | ||
|
||
DateTime = dateTime; | ||
TimeZoneInfo = timeZoneInfo; | ||
} | ||
|
||
public DateTime DateTime { get; } | ||
public TimeZoneInfo TimeZoneInfo { get; } | ||
|
||
public LocalDateTime ToMidnight() | ||
{ | ||
return new LocalDateTime(DateTime.Date, TimeZoneInfo); | ||
} | ||
|
||
public LocalDateTime ToBeginningOfYear() | ||
{ | ||
return new LocalDateTime(new DateTime(DateTime.Year, 1, 1), TimeZoneInfo); | ||
} | ||
|
||
public LocalDateTime ToEndOfYear() | ||
{ | ||
return new LocalDateTime(new DateTime(DateTime.Year, 12, 31, 23, 59, 59, 999, 999), TimeZoneInfo); | ||
} | ||
|
||
public DateTimeOffset ToDateTimeOffsetUtc() | ||
{ | ||
var shouldHaveHadKindUtc = TimeZoneInfo.ConvertTimeToUtc(DateTime, TimeZoneInfo); | ||
return shouldHaveHadKindUtc; | ||
} | ||
|
||
public DateTimeOffset ToDateTimeOffsetLocal() | ||
{ | ||
return new DateTimeOffset(DateTime, TimeZoneInfo.GetUtcOffset(DateTime)); | ||
} | ||
|
||
/// <summary> | ||
/// Imagine past occurrences of midnight as a 1-indexed array where the first occurence is the last midnight experienced. | ||
/// </summary> | ||
public LocalDateTime PastMidnights(int count) | ||
{ | ||
if (count < 1) | ||
{ | ||
throw new ArgumentException("Count must be greater than 0."); | ||
} | ||
|
||
if (DateTime.TimeOfDay == TimeSpan.Zero) | ||
{ | ||
return new LocalDateTime(DateTime.AddDays(-count), TimeZoneInfo).ToMidnight(); | ||
} | ||
else | ||
{ | ||
return new LocalDateTime(DateTime.AddDays(-(count - 1)), TimeZoneInfo).ToMidnight(); | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Imagine future occurrences of midnight as a 1-indexed array where the first occurence is the next midnight experienced. | ||
/// </summary> | ||
public LocalDateTime FutureMidnights(int count) | ||
{ | ||
if (count < 1) | ||
{ | ||
throw new ArgumentException("Count must be greater than 0."); | ||
} | ||
|
||
return new LocalDateTime(DateTime.AddDays(count), TimeZoneInfo).ToMidnight(); | ||
} | ||
|
||
} |