Skip to content

Commit d024d70

Browse files
[5.11] Zoned date time fixes (#715)
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
1 parent fecc2f0 commit d024d70

File tree

6 files changed

+677
-256
lines changed

6 files changed

+677
-256
lines changed

Neo4j.Driver/Neo4j.Driver.Tests/Internal/IO/ValueSerializers/Temporal/UtcZonedDateTimeSerializerTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ public void ShouldDeserializeDateTimeWithOffset(
120120
public void ShouldSerializeDateTimeWithZoneId_Windows_Istanbul()
121121
{
122122
var inDate = new ZonedDateTime(1978, 12, 16, 12, 35, 59, 128000987, Zone.Of("Europe/Istanbul"));
123-
var expected = (seconds: 282652559L, nanos: 128000987L, zoneId: "Europe/Istanbul");
123+
var expected = (seconds: 282648959, nanos: 128000987L, zoneId: "Europe/Istanbul");
124124
var writerMachine = CreateWriterMachine();
125125
var writer = writerMachine.Writer;
126126
writer.Write(inDate);
@@ -140,7 +140,7 @@ public void ShouldSerializeDateTimeWithZoneId_Windows_Istanbul()
140140
public void ShouldDeserializeDateTimeWithZoneId_Windows_Istanbul()
141141
{
142142
var expected = new ZonedDateTime(1978, 12, 16, 12, 35, 59, 128000987, Zone.Of("Europe/Istanbul"));
143-
var inDate = (seconds: 282652559L, nanos: 128000987L, zoneId: "Europe/Istanbul");
143+
var inDate = (seconds: 282648959, nanos: 128000987L, zoneId: "Europe/Istanbul");
144144
var writerMachine = CreateWriterMachine();
145145
var writer = writerMachine.Writer;
146146

Neo4j.Driver/Neo4j.Driver.Tests/Types/ZonedDateTimeWithOffsetTests.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
using System;
1919
using System.Collections;
2020
using FluentAssertions;
21+
using Neo4j.Driver.Internal;
2122
using Xunit;
2223

2324
namespace Neo4j.Driver.Tests.Types
@@ -426,5 +427,45 @@ public void ShouldThrowWhenConversionIsNotSupported()
426427
testAction.Should().Throw<InvalidCastException>();
427428
}
428429
}
430+
431+
[Fact]
432+
public void ShouldCreateMinZonedDateTime()
433+
{
434+
var zone = new ZonedDateTime(TemporalHelpers.MinUtcForZonedDateTime, 0, Zone.Of(0));
435+
zone.Year.Should().Be(-999_999_999);
436+
zone.Month.Should().Be(1);
437+
zone.Day.Should().Be(1);
438+
zone.Hour.Should().Be(0);
439+
zone.Minute.Should().Be(0);
440+
zone.Second.Should().Be(0);
441+
zone.Nanosecond.Should().Be(0);
442+
}
443+
444+
[Fact]
445+
public void ShouldCreateMinZonedDateTimeFromComponents()
446+
{
447+
var zone = new ZonedDateTime(-999_999_999, 1, 1, 0, 0, 0, Zone.Of(0));
448+
zone.UtcSeconds.Should().Be(TemporalHelpers.MinUtcForZonedDateTime);
449+
}
450+
451+
[Fact]
452+
public void ShouldCreateMaxZonedDateTime()
453+
{
454+
var zone = new ZonedDateTime(TemporalHelpers.MaxUtcForZonedDateTime, 0, Zone.Of(0));
455+
zone.Year.Should().Be(999_999_999);
456+
zone.Month.Should().Be(12);
457+
zone.Day.Should().Be(31);
458+
zone.Hour.Should().Be(23);
459+
zone.Minute.Should().Be(59);
460+
zone.Second.Should().Be(59);
461+
zone.Nanosecond.Should().Be(0);
462+
}
463+
464+
[Fact]
465+
public void ShouldCreateMaxZonedDateTimeFromComponents()
466+
{
467+
var zone = new ZonedDateTime(999_999_999, 12, 31, 23, 59, 59, Zone.Of(0));
468+
zone.UtcSeconds.Should().Be(TemporalHelpers.MaxUtcForZonedDateTime);
469+
}
429470
}
430471
}

Neo4j.Driver/Neo4j.Driver.Tests/Types/ZonedDateTimeWithZoneIdTests.cs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,21 @@ namespace Neo4j.Driver.Tests.Types
2424
{
2525
public class ZonedDateTimeWithZoneIdTests
2626
{
27+
public static TheoryData<Func<ZonedDateTime>> NullZoneConstructors = new()
28+
{
29+
() => new ZonedDateTime(0L, 0, null),
30+
() => new ZonedDateTime(0L, null),
31+
() => new ZonedDateTime(DateTime.Now, null),
32+
() => new ZonedDateTime(new LocalDateTime(DateTime.Now), null),
33+
() => new ZonedDateTime(2020, 12, 31, 12, 0, 0, null)
34+
};
35+
36+
public static TheoryData<Func<ZonedDateTime>> LocalConstructorsWithUnkownZoneIds = new()
37+
{
38+
() => new ZonedDateTime(DateTime.Now, "Europe/Neo4j"),
39+
() => new ZonedDateTime(2020, 12, 31, 12, 0, 0, new ZoneId("Europe/Neo4j"))
40+
};
41+
2742
[Fact]
2843
public void ShouldCreateDateTimeWithZoneIdWithDateTimeComponents()
2944
{
@@ -401,5 +416,43 @@ public void ShouldThrowWhenConversionIsNotSupported()
401416
testAction.Should().Throw<InvalidCastException>();
402417
}
403418
}
419+
420+
[Fact]
421+
public void ShouldSupportUnknownZoneIds()
422+
{
423+
var date = new ZonedDateTime(0, 0, Zone.Of("Europe/Neo4j"));
424+
// Unknown ZoneIds should not be able to be converted to a local DateTime or DateTimeOffset
425+
Record.Exception(() => date.ToDateTimeOffset()).Should().BeOfType<TimeZoneNotFoundException>();
426+
Record.Exception(() => date.LocalDateTime).Should().BeOfType<TimeZoneNotFoundException>();
427+
428+
// But they should be able to be converted to a UTC DateTime.
429+
date.UtcDateTime.Should().Be(new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc));
430+
date.ToString().Should().Be("{UtcSeconds: 0, Nanoseconds: 0, Zone: [Europe/Neo4j]}");
431+
date.UtcSeconds.Should().Be(0);
432+
date.Zone.Should().Be(Zone.Of("Europe/Neo4j"));
433+
date.Nanosecond.Should().Be(0);
434+
date.Ambiguous.Should().Be(false);
435+
}
436+
437+
[Theory]
438+
[MemberData(nameof(NullZoneConstructors))]
439+
public void ShouldThrowWithNullZoneId(Func<ZonedDateTime> ctor)
440+
{
441+
Record.Exception(ctor).Should().BeOfType<ArgumentNullException>();
442+
}
443+
444+
[Theory]
445+
[MemberData(nameof(LocalConstructorsWithUnkownZoneIds))]
446+
public void ShouldThrowExceptionWhenNonMonotonicTimeProvidedAndUnknownZoneId(Func<ZonedDateTime> ctor)
447+
{
448+
Record.Exception(ctor).Should().BeOfType<TimeZoneNotFoundException>();
449+
}
450+
451+
[Fact]
452+
public void ShouldNotThrowExceptionWhenNonMonotonicTimeProvidedAndUnknownZoneId()
453+
{
454+
Record.Exception(() => new ZonedDateTime(new LocalDateTime(DateTime.Now), new ZoneId("Europe/Neo4j")))
455+
.Should().BeNull();
456+
}
404457
}
405458
}

Neo4j.Driver/Neo4j.Driver/Internal/Helpers/TemporalHelpers.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ internal static class TemporalHelpers
5555
private const long Days0000To1970 = DaysPerCycle * 5L - (30L * 365L + 7L);
5656
private const int DaysPerCycle = 146_097;
5757
private const int NanosecondsPerTick = 100;
58+
internal const long DateTimeOffsetMinSeconds = -62_135_596_800;
59+
internal const long DateTimeOffsetMaxSeconds = 253_402_300_799;
60+
internal const long MinUtcForZonedDateTime = -31557014135596800;
61+
internal const long MaxUtcForZonedDateTime = 31556889832780799;
5862

5963
public static long ToNanoOfDay(this IHasTimeComponents time)
6064
{
@@ -110,7 +114,7 @@ public static LocalDateTime EpochSecondsAndNanoToDateTime(long epochSeconds, int
110114
{
111115
var epochDay = FloorDiv(epochSeconds, SecondsPerDay);
112116
var secondsOfDay = FloorMod(epochSeconds, SecondsPerDay);
113-
var nanoOfDay = secondsOfDay * NanosPerSecond + nano;
117+
var nanoOfDay = secondsOfDay * NanosPerSecond + nano;
114118

115119
ComponentsOfEpochDays(epochDay, out var year, out var month, out var day);
116120
ComponentsOfNanoOfDay(nanoOfDay, out var hour, out var minute, out var second, out var nanosecond);

Neo4j.Driver/Neo4j.Driver/Internal/IO/ValueSerializers/Temporal/UtcZonedDateTimeSerializer.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,14 +58,14 @@ public void Serialize(BoltProtocolVersion _, PackStreamWriter writer, object val
5858
{
5959
case ZoneId zone:
6060
writer.WriteStructHeader(StructSize, StructTypeWithId);
61-
writer.WriteLong(TemporalHelpers.UtcEpochSeconds(dateTime));
61+
writer.WriteLong(dateTime.UtcSeconds);
6262
writer.WriteInt(dateTime.Nanosecond);
6363
writer.WriteString(zone.Id);
6464
break;
6565

6666
case ZoneOffset zone:
6767
writer.WriteStructHeader(StructSize, StructTypeWithOffset);
68-
writer.WriteLong(TemporalHelpers.UtcEpochSeconds(dateTime));
68+
writer.WriteLong(dateTime.UtcSeconds);
6969
writer.WriteInt(dateTime.Nanosecond);
7070
writer.WriteInt(zone.OffsetSeconds);
7171
break;

0 commit comments

Comments
 (0)