Skip to content

Commit

Permalink
[5.11] Zoned date time fixes (#715)
Browse files Browse the repository at this point in the history
Fixes for ZonedDateTime.
* Introduce Ambiguous bool property, Creating ZonedDateTime with out a datetime kind, or using a datetime kind local with a named Zone:Zone.of(string), meant we could create an ambiguous date time.
* Introduce ZonedDateTime.AmbiguityReason enum flags type.
* Introduce Reason AmbiguityReason property for ZonedDateTimes, to allow users to see why a value is considered ambiguous.
* Introduce UtcSeconds long property which is the understood monotonic timestamp from unix epoch in UTC.
* Fix ZonedDateTime lazily parsing when returned from a query, now it is parsed immediately.
* Fix ZonedDateTime not supporting values outside of range of BCL Date Types
* Add EpochTicks Property, and constructor which can be used for easy interop with nodatime's Instant
  • Loading branch information
thelonelyvulpes authored Jul 26, 2023
1 parent fecc2f0 commit d024d70
Show file tree
Hide file tree
Showing 6 changed files with 677 additions and 256 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ public void ShouldDeserializeDateTimeWithOffset(
public void ShouldSerializeDateTimeWithZoneId_Windows_Istanbul()
{
var inDate = new ZonedDateTime(1978, 12, 16, 12, 35, 59, 128000987, Zone.Of("Europe/Istanbul"));
var expected = (seconds: 282652559L, nanos: 128000987L, zoneId: "Europe/Istanbul");
var expected = (seconds: 282648959, nanos: 128000987L, zoneId: "Europe/Istanbul");
var writerMachine = CreateWriterMachine();
var writer = writerMachine.Writer;
writer.Write(inDate);
Expand All @@ -140,7 +140,7 @@ public void ShouldSerializeDateTimeWithZoneId_Windows_Istanbul()
public void ShouldDeserializeDateTimeWithZoneId_Windows_Istanbul()
{
var expected = new ZonedDateTime(1978, 12, 16, 12, 35, 59, 128000987, Zone.Of("Europe/Istanbul"));
var inDate = (seconds: 282652559L, nanos: 128000987L, zoneId: "Europe/Istanbul");
var inDate = (seconds: 282648959, nanos: 128000987L, zoneId: "Europe/Istanbul");
var writerMachine = CreateWriterMachine();
var writer = writerMachine.Writer;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
using System;
using System.Collections;
using FluentAssertions;
using Neo4j.Driver.Internal;
using Xunit;

namespace Neo4j.Driver.Tests.Types
Expand Down Expand Up @@ -426,5 +427,45 @@ public void ShouldThrowWhenConversionIsNotSupported()
testAction.Should().Throw<InvalidCastException>();
}
}

[Fact]
public void ShouldCreateMinZonedDateTime()
{
var zone = new ZonedDateTime(TemporalHelpers.MinUtcForZonedDateTime, 0, Zone.Of(0));
zone.Year.Should().Be(-999_999_999);
zone.Month.Should().Be(1);
zone.Day.Should().Be(1);
zone.Hour.Should().Be(0);
zone.Minute.Should().Be(0);
zone.Second.Should().Be(0);
zone.Nanosecond.Should().Be(0);
}

[Fact]
public void ShouldCreateMinZonedDateTimeFromComponents()
{
var zone = new ZonedDateTime(-999_999_999, 1, 1, 0, 0, 0, Zone.Of(0));
zone.UtcSeconds.Should().Be(TemporalHelpers.MinUtcForZonedDateTime);
}

[Fact]
public void ShouldCreateMaxZonedDateTime()
{
var zone = new ZonedDateTime(TemporalHelpers.MaxUtcForZonedDateTime, 0, Zone.Of(0));
zone.Year.Should().Be(999_999_999);
zone.Month.Should().Be(12);
zone.Day.Should().Be(31);
zone.Hour.Should().Be(23);
zone.Minute.Should().Be(59);
zone.Second.Should().Be(59);
zone.Nanosecond.Should().Be(0);
}

[Fact]
public void ShouldCreateMaxZonedDateTimeFromComponents()
{
var zone = new ZonedDateTime(999_999_999, 12, 31, 23, 59, 59, Zone.Of(0));
zone.UtcSeconds.Should().Be(TemporalHelpers.MaxUtcForZonedDateTime);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,21 @@ namespace Neo4j.Driver.Tests.Types
{
public class ZonedDateTimeWithZoneIdTests
{
public static TheoryData<Func<ZonedDateTime>> NullZoneConstructors = new()
{
() => new ZonedDateTime(0L, 0, null),
() => new ZonedDateTime(0L, null),
() => new ZonedDateTime(DateTime.Now, null),
() => new ZonedDateTime(new LocalDateTime(DateTime.Now), null),
() => new ZonedDateTime(2020, 12, 31, 12, 0, 0, null)
};

public static TheoryData<Func<ZonedDateTime>> LocalConstructorsWithUnkownZoneIds = new()
{
() => new ZonedDateTime(DateTime.Now, "Europe/Neo4j"),
() => new ZonedDateTime(2020, 12, 31, 12, 0, 0, new ZoneId("Europe/Neo4j"))
};

[Fact]
public void ShouldCreateDateTimeWithZoneIdWithDateTimeComponents()
{
Expand Down Expand Up @@ -401,5 +416,43 @@ public void ShouldThrowWhenConversionIsNotSupported()
testAction.Should().Throw<InvalidCastException>();
}
}

[Fact]
public void ShouldSupportUnknownZoneIds()
{
var date = new ZonedDateTime(0, 0, Zone.Of("Europe/Neo4j"));
// Unknown ZoneIds should not be able to be converted to a local DateTime or DateTimeOffset
Record.Exception(() => date.ToDateTimeOffset()).Should().BeOfType<TimeZoneNotFoundException>();
Record.Exception(() => date.LocalDateTime).Should().BeOfType<TimeZoneNotFoundException>();

// But they should be able to be converted to a UTC DateTime.
date.UtcDateTime.Should().Be(new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc));
date.ToString().Should().Be("{UtcSeconds: 0, Nanoseconds: 0, Zone: [Europe/Neo4j]}");
date.UtcSeconds.Should().Be(0);
date.Zone.Should().Be(Zone.Of("Europe/Neo4j"));
date.Nanosecond.Should().Be(0);
date.Ambiguous.Should().Be(false);
}

[Theory]
[MemberData(nameof(NullZoneConstructors))]
public void ShouldThrowWithNullZoneId(Func<ZonedDateTime> ctor)
{
Record.Exception(ctor).Should().BeOfType<ArgumentNullException>();
}

[Theory]
[MemberData(nameof(LocalConstructorsWithUnkownZoneIds))]
public void ShouldThrowExceptionWhenNonMonotonicTimeProvidedAndUnknownZoneId(Func<ZonedDateTime> ctor)
{
Record.Exception(ctor).Should().BeOfType<TimeZoneNotFoundException>();
}

[Fact]
public void ShouldNotThrowExceptionWhenNonMonotonicTimeProvidedAndUnknownZoneId()
{
Record.Exception(() => new ZonedDateTime(new LocalDateTime(DateTime.Now), new ZoneId("Europe/Neo4j")))
.Should().BeNull();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ internal static class TemporalHelpers
private const long Days0000To1970 = DaysPerCycle * 5L - (30L * 365L + 7L);
private const int DaysPerCycle = 146_097;
private const int NanosecondsPerTick = 100;
internal const long DateTimeOffsetMinSeconds = -62_135_596_800;
internal const long DateTimeOffsetMaxSeconds = 253_402_300_799;
internal const long MinUtcForZonedDateTime = -31557014135596800;
internal const long MaxUtcForZonedDateTime = 31556889832780799;

public static long ToNanoOfDay(this IHasTimeComponents time)
{
Expand Down Expand Up @@ -110,7 +114,7 @@ public static LocalDateTime EpochSecondsAndNanoToDateTime(long epochSeconds, int
{
var epochDay = FloorDiv(epochSeconds, SecondsPerDay);
var secondsOfDay = FloorMod(epochSeconds, SecondsPerDay);
var nanoOfDay = secondsOfDay * NanosPerSecond + nano;
var nanoOfDay = secondsOfDay * NanosPerSecond + nano;

ComponentsOfEpochDays(epochDay, out var year, out var month, out var day);
ComponentsOfNanoOfDay(nanoOfDay, out var hour, out var minute, out var second, out var nanosecond);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,14 @@ public void Serialize(BoltProtocolVersion _, PackStreamWriter writer, object val
{
case ZoneId zone:
writer.WriteStructHeader(StructSize, StructTypeWithId);
writer.WriteLong(TemporalHelpers.UtcEpochSeconds(dateTime));
writer.WriteLong(dateTime.UtcSeconds);
writer.WriteInt(dateTime.Nanosecond);
writer.WriteString(zone.Id);
break;

case ZoneOffset zone:
writer.WriteStructHeader(StructSize, StructTypeWithOffset);
writer.WriteLong(TemporalHelpers.UtcEpochSeconds(dateTime));
writer.WriteLong(dateTime.UtcSeconds);
writer.WriteInt(dateTime.Nanosecond);
writer.WriteInt(zone.OffsetSeconds);
break;
Expand Down
Loading

0 comments on commit d024d70

Please sign in to comment.