Skip to content

Commit

Permalink
unambigious local datetimes
Browse files Browse the repository at this point in the history
  • Loading branch information
mattiasnordqvist committed Jun 16, 2024
1 parent 6ebd541 commit 9816e7d
Show file tree
Hide file tree
Showing 2 changed files with 217 additions and 0 deletions.
121 changes: 121 additions & 0 deletions TimeKeeping/DotNetThoughts.TimeKeeping.Tests/LocalDateTimeTests.cs
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));

}
}
96 changes: 96 additions & 0 deletions TimeKeeping/DotNetThoughts.TimeKeeping/LocalDateTime.cs
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)

Check warning on line 8 in TimeKeeping/DotNetThoughts.TimeKeeping/LocalDateTime.cs

View workflow job for this annotation

GitHub Actions / create_nuget

Missing XML comment for publicly visible type or member 'LocalDateTime.LocalDateTime(DateOnly, TimeZoneInfo)'
: this(date.ToDateTime(TimeOnly.MinValue), timeZoneInfo) { }

public LocalDateTime(DateTime dateTime, TimeZoneInfo timeZoneInfo)

Check warning on line 11 in TimeKeeping/DotNetThoughts.TimeKeeping/LocalDateTime.cs

View workflow job for this annotation

GitHub Actions / create_nuget

Missing XML comment for publicly visible type or member 'LocalDateTime.LocalDateTime(DateTime, 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; }

Check warning on line 34 in TimeKeeping/DotNetThoughts.TimeKeeping/LocalDateTime.cs

View workflow job for this annotation

GitHub Actions / create_nuget

Missing XML comment for publicly visible type or member 'LocalDateTime.DateTime'
public TimeZoneInfo TimeZoneInfo { get; }

Check warning on line 35 in TimeKeeping/DotNetThoughts.TimeKeeping/LocalDateTime.cs

View workflow job for this annotation

GitHub Actions / create_nuget

Missing XML comment for publicly visible type or member 'LocalDateTime.TimeZoneInfo'

public LocalDateTime ToMidnight()

Check warning on line 37 in TimeKeeping/DotNetThoughts.TimeKeeping/LocalDateTime.cs

View workflow job for this annotation

GitHub Actions / create_nuget

Missing XML comment for publicly visible type or member 'LocalDateTime.ToMidnight()'
{
return new LocalDateTime(DateTime.Date, TimeZoneInfo);
}

public LocalDateTime ToBeginningOfYear()

Check warning on line 42 in TimeKeeping/DotNetThoughts.TimeKeeping/LocalDateTime.cs

View workflow job for this annotation

GitHub Actions / create_nuget

Missing XML comment for publicly visible type or member '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();
}

}

0 comments on commit 9816e7d

Please sign in to comment.