diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Internal/IO/ValueSerializers/Temporal/UtcZonedDateTimeSerializerTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Internal/IO/ValueSerializers/Temporal/UtcZonedDateTimeSerializerTests.cs index 818d8635c..a357b85f0 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests/Internal/IO/ValueSerializers/Temporal/UtcZonedDateTimeSerializerTests.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Internal/IO/ValueSerializers/Temporal/UtcZonedDateTimeSerializerTests.cs @@ -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); @@ -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; diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Types/ZonedDateTimeWithOffsetTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Types/ZonedDateTimeWithOffsetTests.cs index 86f5b42ba..08685593d 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests/Types/ZonedDateTimeWithOffsetTests.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Types/ZonedDateTimeWithOffsetTests.cs @@ -18,6 +18,7 @@ using System; using System.Collections; using FluentAssertions; +using Neo4j.Driver.Internal; using Xunit; namespace Neo4j.Driver.Tests.Types @@ -426,5 +427,45 @@ public void ShouldThrowWhenConversionIsNotSupported() testAction.Should().Throw(); } } + + [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); + } } } diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Types/ZonedDateTimeWithZoneIdTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Types/ZonedDateTimeWithZoneIdTests.cs index bbe72a7a7..f0a757d67 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests/Types/ZonedDateTimeWithZoneIdTests.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Types/ZonedDateTimeWithZoneIdTests.cs @@ -24,6 +24,21 @@ namespace Neo4j.Driver.Tests.Types { public class ZonedDateTimeWithZoneIdTests { + public static TheoryData> 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> LocalConstructorsWithUnkownZoneIds = new() + { + () => new ZonedDateTime(DateTime.Now, "Europe/Neo4j"), + () => new ZonedDateTime(2020, 12, 31, 12, 0, 0, new ZoneId("Europe/Neo4j")) + }; + [Fact] public void ShouldCreateDateTimeWithZoneIdWithDateTimeComponents() { @@ -401,5 +416,43 @@ public void ShouldThrowWhenConversionIsNotSupported() testAction.Should().Throw(); } } + + [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(); + Record.Exception(() => date.LocalDateTime).Should().BeOfType(); + + // 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 ctor) + { + Record.Exception(ctor).Should().BeOfType(); + } + + [Theory] + [MemberData(nameof(LocalConstructorsWithUnkownZoneIds))] + public void ShouldThrowExceptionWhenNonMonotonicTimeProvidedAndUnknownZoneId(Func ctor) + { + Record.Exception(ctor).Should().BeOfType(); + } + + [Fact] + public void ShouldNotThrowExceptionWhenNonMonotonicTimeProvidedAndUnknownZoneId() + { + Record.Exception(() => new ZonedDateTime(new LocalDateTime(DateTime.Now), new ZoneId("Europe/Neo4j"))) + .Should().BeNull(); + } } } diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/Helpers/TemporalHelpers.cs b/Neo4j.Driver/Neo4j.Driver/Internal/Helpers/TemporalHelpers.cs index 20a04537d..d40f738b8 100644 --- a/Neo4j.Driver/Neo4j.Driver/Internal/Helpers/TemporalHelpers.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Helpers/TemporalHelpers.cs @@ -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) { @@ -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); diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/IO/ValueSerializers/Temporal/UtcZonedDateTimeSerializer.cs b/Neo4j.Driver/Neo4j.Driver/Internal/IO/ValueSerializers/Temporal/UtcZonedDateTimeSerializer.cs index ae500dc0f..40bed4f1e 100644 --- a/Neo4j.Driver/Neo4j.Driver/Internal/IO/ValueSerializers/Temporal/UtcZonedDateTimeSerializer.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/IO/ValueSerializers/Temporal/UtcZonedDateTimeSerializer.cs @@ -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; diff --git a/Neo4j.Driver/Neo4j.Driver/Types/ZonedDateTime.cs b/Neo4j.Driver/Neo4j.Driver/Types/ZonedDateTime.cs index 08cc1b0c7..d32285b02 100644 --- a/Neo4j.Driver/Neo4j.Driver/Types/ZonedDateTime.cs +++ b/Neo4j.Driver/Neo4j.Driver/Types/ZonedDateTime.cs @@ -29,103 +29,334 @@ public sealed class ZonedDateTime : TemporalValue, IComparable, IHasDateTimeComponents { + /// + /// Used by the driver to explain the cause of a 's + /// being true. + /// + [Flags] + public enum AmbiguityReason + { + /// No ambiguity. + None, + + /// The lookup of the offset will be completed with a local time which may result an ambiguous value. + ZoneIdLookUpWithLocalTime, + + /// The datetime kind is unspecified, it will be treated as local time. + UnspecifiedDateTimeKind, + + /// + /// The lookup of the offset will be completed with a value truncated to the range of BCL date/time types meaning + /// that any rules that may apply outside of the BCL date/time type range are not applied. + /// + RuleLookupTruncatedToClrRange + } + /// Default comparer for values. public static readonly IComparer Comparer = new TemporalValueComparer(); - private readonly object _lazyLock; + /// Used for storing offset in seconds. + private readonly int? _offsetSeconds; - private int _day; - private int _hour; - private int _minute; - private int _month; + /// + /// Create a new instance of using delta from unix epoch (1970-1-1 00:00:00.00 UTC).
+ /// Allows handling values in range for neo4j and outside of the range of BCL date/time types (, + /// ). + /// + /// When is outside of BCL date/time types range (-62_135_596_800, 253_402_300_799) + /// and is a the instance will be marked as + /// as does not support BCL date/time type ranges. + /// + ///
+ /// Seconds from unix epoch (1970-1-1 00:00:00.00 UTC). + /// Nanoseconds of the second. + /// Zone for offsetting utc to local. + public ZonedDateTime(long utcSeconds, int nanos, Zone zone) + { + if (utcSeconds is > TemporalHelpers.MaxUtcForZonedDateTime or < TemporalHelpers.MinUtcForZonedDateTime) + { + throw new ArgumentOutOfRangeException(nameof(utcSeconds)); + } - private int? _offset; - private int _second; - private long? _utcSeconds; + UtcSeconds = utcSeconds; + Nanosecond = nanos; + Zone = zone ?? throw new ArgumentNullException(nameof(zone)); - private int _year; + if (zone is ZoneOffset zo) + { + _offsetSeconds = zo.OffsetSeconds; + var local = TemporalHelpers.EpochSecondsAndNanoToDateTime(utcSeconds + zo.OffsetSeconds, Nanosecond); + Year = local.Year; + Month = local.Month; + Day = local.Day; + Hour = local.Hour; + Minute = local.Minute; + Second = local.Second; + } + else + { + if (utcSeconds is < TemporalHelpers.DateTimeOffsetMinSeconds + or > TemporalHelpers.DateTimeOffsetMaxSeconds) + { + SetAmbiguous(AmbiguityReason.RuleLookupTruncatedToClrRange); + } + + var utc = TemporalHelpers.EpochSecondsAndNanoToDateTime(utcSeconds, Nanosecond); + try + { + var offset = zone.OffsetSecondsAt(ClrFriendly(utc)); + _offsetSeconds = offset; + var local = TemporalHelpers.EpochSecondsAndNanoToDateTime(utcSeconds + offset, Nanosecond); + Year = local.Year; + Month = local.Month; + Day = local.Day; + Hour = local.Hour; + Minute = local.Minute; + Second = local.Second; + } + catch (TimeZoneNotFoundException) + { + UnknownZoneInfo = true; + } + } + } - /// Used to lazily evaluate zones offset. - /// - /// - /// - internal ZonedDateTime(long utcSeconds, int nanos, Zone zone) + /// + /// Create a new instance of using delta from unix epoch (1970-1-1 00:00:00.00 UTC) in ticks. + ///
Allows handling values in range for neo4j and outside of the range of BCL date types (, + /// ). + /// + /// When is outside of BCL date ranges (-621_355_968_000_000_000, + /// 2_534_023_009_990_000_000) and is a the + /// instance will be marked as . + /// + ///
+ /// ticks from unix epoch (1970-1-1 00:00:00.00 UTC). + /// Zone for offsetting utc to local. + public ZonedDateTime(long ticks, Zone zone) : this( + ticks / TimeSpan.TicksPerSecond, + (int)(ticks % TimeSpan.TicksPerSecond * 100), + zone) { - _lazyLock = new object(); - _utcSeconds = utcSeconds; - Nanosecond = nanos; - Zone = zone; } - /// Initializes a new instance of from given value. + /// Initializes a new instance of from given the value. /// public ZonedDateTime(DateTimeOffset dateTimeOffset) - : this(dateTimeOffset.DateTime, dateTimeOffset.Offset) { + UtcSeconds = dateTimeOffset.ToUnixTimeSeconds(); + Nanosecond = TemporalHelpers.ExtractNanosecondFromTicks(dateTimeOffset.UtcTicks); + Zone = Zone.Of((int)dateTimeOffset.Offset.TotalSeconds); + + _offsetSeconds = (int)dateTimeOffset.Offset.TotalSeconds; + + Year = dateTimeOffset.Year; + Month = dateTimeOffset.Month; + Day = dateTimeOffset.Day; + Hour = dateTimeOffset.Hour; + Minute = dateTimeOffset.Minute; + Second = dateTimeOffset.Second; } /// Initializes a new instance of from given value. - /// - /// + /// Date time instance, If is the instance will set as true. + /// TimeSpan to offset datetime by, will be converted into . + /// + /// When is Utc, this instance's date time components (...) + /// will be offset by . + /// + /// + /// When is Local, this instance's date time components (...) + /// will be the same as . + /// + /// + /// When is , this instance's date time components + /// (...) will be the same as + /// but will be true. + /// public ZonedDateTime(DateTime dateTime, TimeSpan offset) - : this(dateTime, (int)offset.TotalSeconds) { + Zone = Zone.Of((int)offset.TotalSeconds); + Nanosecond = TemporalHelpers.ExtractNanosecondFromTicks(dateTime.Ticks); + _offsetSeconds = (int)offset.TotalSeconds; + + if (dateTime.Kind == DateTimeKind.Utc) + { + var dto = new DateTimeOffset(dateTime); + UtcSeconds = dto.ToUnixTimeSeconds(); + var local = dto.AddSeconds(_offsetSeconds.Value); + Year = local.Year; + Month = local.Month; + Day = local.Day; + Hour = local.Hour; + Minute = local.Minute; + Second = local.Second; + } + else + { + if (dateTime.Kind == DateTimeKind.Unspecified) + { + SetAmbiguous(AmbiguityReason.UnspecifiedDateTimeKind); + } + + var dto = new DateTimeOffset( + new DateTime( + dateTime.AddSeconds(-_offsetSeconds.Value).Ticks, + DateTimeKind.Utc)); + + UtcSeconds = dto.ToUnixTimeSeconds(); + Year = dateTime.Year; + Month = dateTime.Month; + Day = dateTime.Day; + Hour = dateTime.Hour; + Minute = dateTime.Minute; + Second = dateTime.Second; + } } /// Initializes a new instance of from given value. - /// - /// + /// Date time instance, If is the instance will set as true. + /// Seconds to offset datetime by, will be converted into . + /// + /// When is Utc, this instance's date time components (...) + /// will be offset by . + /// + /// + /// When is Local, this instance's date time components (...) + /// will be the same as . + /// + /// + /// When is , this instance's date time components + /// (...) will be the same as + /// but will be true. + /// public ZonedDateTime(DateTime dateTime, int offsetSeconds) - : this( - dateTime.Year, - dateTime.Month, - dateTime.Day, - dateTime.Hour, - dateTime.Minute, - dateTime.Second, - TemporalHelpers.ExtractNanosecondFromTicks(dateTime.Ticks), - Zone.Of(offsetSeconds)) { + Zone = Zone.Of(offsetSeconds); + Nanosecond = TemporalHelpers.ExtractNanosecondFromTicks(dateTime.Ticks); + _offsetSeconds = offsetSeconds; + + if (dateTime.Kind == DateTimeKind.Utc) + { + var dto = new DateTimeOffset(dateTime); + UtcSeconds = dto.ToUnixTimeSeconds(); + var local = dto.AddSeconds(offsetSeconds); + + Year = local.Year; + Month = local.Month; + Day = local.Day; + Hour = local.Hour; + Minute = local.Minute; + Second = local.Second; + } + else if (dateTime.Kind == DateTimeKind.Local) + { + var dto = new DateTimeOffset(new DateTime(dateTime.AddSeconds(-offsetSeconds).Ticks, DateTimeKind.Utc)); + UtcSeconds = dto.ToUnixTimeSeconds(); + Year = dateTime.Year; + Month = dateTime.Month; + Day = dateTime.Day; + Hour = dateTime.Hour; + Minute = dateTime.Minute; + Second = dateTime.Second; + } + else + { + SetAmbiguous(AmbiguityReason.UnspecifiedDateTimeKind); + var dto = new DateTimeOffset(new DateTime(dateTime.AddSeconds(-offsetSeconds).Ticks, DateTimeKind.Utc)); + UtcSeconds = dto.ToUnixTimeSeconds(); + Year = dateTime.Year; + Month = dateTime.Month; + Day = dateTime.Day; + Hour = dateTime.Hour; + Minute = dateTime.Minute; + Second = dateTime.Second; + } } /// Initializes a new instance of from given value. - /// - /// + /// Date time instance, should be local or utc. + /// + /// Zone name, if zone name is not known by the operating system and 's + /// is not , the driver can not correctly set + /// which is required for server versions 5+, 4.4.12+, 4.3.19+.
+ /// if 's is , + /// the will be caught, unless one of the local values (..) is accessed and can be sent to the server. + /// + /// if zone name is not known by the operating system and 's + /// is not , the driver can not correctly set + /// which is required for server versions 5+, 4.4.12+, 4.3.19+. public ZonedDateTime(DateTime dateTime, string zoneId) - : this( - dateTime.Year, - dateTime.Month, - dateTime.Day, - dateTime.Hour, - dateTime.Minute, - dateTime.Second, - TemporalHelpers.ExtractNanosecondFromTicks(dateTime.Ticks), - Zone.Of(zoneId)) { + Zone = zoneId != null ? Zone.Of(zoneId) : throw new ArgumentNullException(nameof(zoneId)); + Nanosecond = TemporalHelpers.ExtractNanosecondFromTicks(dateTime.Ticks); + + if (dateTime.Kind == DateTimeKind.Utc) + { + var dto = new DateTimeOffset(dateTime); + UtcSeconds = dto.ToUnixTimeSeconds(); + try + { + var local = dto.ToOffset(LookupOffsetAt(dto.UtcDateTime)); + _offsetSeconds = (int)local.Offset.TotalSeconds; + Year = local.Year; + Month = local.Month; + Day = local.Day; + Hour = local.Hour; + Minute = local.Minute; + Second = local.Second; + } + catch (TimeZoneNotFoundException) + { + UnknownZoneInfo = true; + } + } + else + { + SetAmbiguous( + dateTime.Kind == DateTimeKind.Unspecified + ? AmbiguityReason.UnspecifiedDateTimeKind | AmbiguityReason.ZoneIdLookUpWithLocalTime + : AmbiguityReason.ZoneIdLookUpWithLocalTime); + + var dto = new DateTimeOffset(dateTime, LookupOffsetAt(dateTime)); + UtcSeconds = dto.ToUnixTimeSeconds(); + _offsetSeconds = (int)dto.Offset.TotalSeconds; + var local = dateTime; + Year = local.Year; + Month = local.Month; + Day = local.Day; + Hour = local.Hour; + Minute = local.Minute; + Second = local.Second; + } } - /// Initializes a new instance of from individual date time component values - /// - /// - /// - /// - /// - /// - /// + /// Initializes a new instance of from individual local date time component values. + /// Local year value. + /// Local month value. + /// Local day value. + /// Local hour value. + /// Local minute value. + /// Local second value. + /// Zone of this date time, will be used to calculate from local values. + /// if zone name is not known by the operating system. + [Obsolete("Deprecated, This constructor does not support a kind, so not known if utc or local.")] public ZonedDateTime(int year, int month, int day, int hour, int minute, int second, Zone zone) : this(year, month, day, hour, minute, second, 0, zone) { } - /// Initializes a new instance of from individual date time component values - /// - /// - /// - /// - /// - /// - /// - /// + /// Initializes a new instance of from individual local date time component values. + /// Local year value. + /// Local month value. + /// Local day value. + /// Local hour value. + /// Local minute value. + /// Local second value. + /// Nanoseconds of the second. + /// Zone of this date time, will be used to calculate from local values. + /// if zone name is not known by the operating system. + [Obsolete("Deprecated, This constructor does not support a kind, so not known if utc or local.")] public ZonedDateTime( int year, int month, @@ -136,96 +367,191 @@ public ZonedDateTime( int nanosecond, Zone zone) { - Throw.ArgumentOutOfRangeException.IfValueNotBetween( - year, - TemporalHelpers.MinYear, - TemporalHelpers.MaxYear, - nameof(year)); - - Throw.ArgumentOutOfRangeException.IfValueNotBetween( - month, - TemporalHelpers.MinMonth, - TemporalHelpers.MaxMonth, - nameof(month)); + ValidateComponents(year, month, day, hour, minute, second, nanosecond); - Throw.ArgumentOutOfRangeException.IfValueNotBetween( - day, - TemporalHelpers.MinDay, - TemporalHelpers.MaxDayOfMonth(year, month), - nameof(day)); + zone = zone ?? throw new ArgumentNullException(nameof(zone)); - Throw.ArgumentOutOfRangeException.IfValueNotBetween( - hour, - TemporalHelpers.MinHour, - TemporalHelpers.MaxHour, - nameof(hour)); - - Throw.ArgumentOutOfRangeException.IfValueNotBetween( - minute, - TemporalHelpers.MinMinute, - TemporalHelpers.MaxMinute, - nameof(minute)); + SetAmbiguous(AmbiguityReason.UnspecifiedDateTimeKind); - Throw.ArgumentOutOfRangeException.IfValueNotBetween( - second, - TemporalHelpers.MinSecond, - TemporalHelpers.MaxSecond, - nameof(second)); + Zone = zone; + Nanosecond = nanosecond; - Throw.ArgumentOutOfRangeException.IfValueNotBetween( - nanosecond, - TemporalHelpers.MinNanosecond, - TemporalHelpers.MaxNanosecond, - nameof(nanosecond)); + Year = year; + Month = month; + Day = day; + Hour = hour; + Minute = minute; + Second = second; - _year = year; - _month = month; - _day = day; - _hour = hour; - _minute = minute; - _second = second; + if (zone is ZoneOffset zo) + { + _offsetSeconds = zo.OffsetSeconds; + var epoch = new LocalDateTime(year, month, day, hour, minute, second, nanosecond).ToEpochSeconds(); + UtcSeconds = epoch - _offsetSeconds.Value; + } + else + { + if (Year is > 9999 or < 1) + { + SetAmbiguous( + AmbiguityReason.UnspecifiedDateTimeKind | + AmbiguityReason.ZoneIdLookUpWithLocalTime | + AmbiguityReason.RuleLookupTruncatedToClrRange); + } + else + { + SetAmbiguous(AmbiguityReason.UnspecifiedDateTimeKind | AmbiguityReason.ZoneIdLookUpWithLocalTime); + } - Nanosecond = nanosecond; - Zone = zone ?? throw new ArgumentNullException(nameof(zone)); + var local = new LocalDateTime(year, month, day, hour, minute, second, nanosecond); + var offset = LookupOffsetAt(ClrFriendly(local)); + _offsetSeconds = (int)offset.TotalSeconds; + UtcSeconds = local.ToEpochSeconds() - _offsetSeconds.Value; + } } + /// + /// Deprecated Constructor for internal use only. + /// internal ZonedDateTime(IHasDateTimeComponents dateTime, Zone zone) - : this( - dateTime.Year, - dateTime.Month, - dateTime.Day, - dateTime.Hour, - dateTime.Minute, - dateTime.Second, - dateTime.Nanosecond, - zone) { + zone = zone ?? throw new ArgumentNullException(nameof(zone)); + + SetAmbiguous(AmbiguityReason.UnspecifiedDateTimeKind); + + Zone = zone; + Nanosecond = dateTime.Nanosecond; + + Year = dateTime.Year; + Month = dateTime.Month; + Day = dateTime.Day; + Hour = dateTime.Hour; + Minute = dateTime.Minute; + Second = dateTime.Second; + ValidateComponents(Year, Month, Day, Hour, Minute, Second, Nanosecond); + + if (zone is ZoneOffset zo) + { + _offsetSeconds = zo.OffsetSeconds; + var epoch = new LocalDateTime(Year, Month, Day, Hour, Minute, Second, Nanosecond).ToEpochSeconds(); + UtcSeconds = epoch - _offsetSeconds.Value; + } + else + { + try + { + if (Year is > 9999 or < 1) + { + SetAmbiguous( + AmbiguityReason.UnspecifiedDateTimeKind | + AmbiguityReason.ZoneIdLookUpWithLocalTime | + AmbiguityReason.RuleLookupTruncatedToClrRange); + } + else + { + SetAmbiguous(AmbiguityReason.UnspecifiedDateTimeKind | AmbiguityReason.ZoneIdLookUpWithLocalTime); + } + + var local = new LocalDateTime(Year, Month, Day, Hour, Minute, Second, Nanosecond); + var offset = LookupOffsetAt(ClrFriendly(local)); + _offsetSeconds = (int)offset.TotalSeconds; + UtcSeconds = local.ToEpochSeconds() - _offsetSeconds.Value; + } + catch (TimeZoneNotFoundException) + { + UnknownZoneInfo = true; + } + } } + /// + /// The is not recognized by the driver, or operating system.
+ /// Attempting to call or will raise an . + ///
+ public bool UnknownZoneInfo { get; set; } + + /// + /// Gets why this instance is has set as true. + /// + public AmbiguityReason Reason { get; private set; } = AmbiguityReason.None; + + /// + /// Gets if this instance is has a possible ambiguity.
+ /// If the date specified is near a daylight saving it could have been misinterpreted.
+ /// The most common cause for this will be because the used to construct this instance did not have a specified as . + /// In order to reliably look up the offsets at a given time in a timezone we require a monotonic datetime.
+ ///
+ public bool Ambiguous { get; private set; } + + /// + /// Gets the number of seconds since the Unix Epoch (00:00:00 UTC, Thursday, 1 January 1970).
Introduced in + /// 4.4.1 a fix to a long standing issue of not having a monotonic datetime used on construction or transmission. + ///
+ public long UtcSeconds { get; } + + /// + /// Gets the number of Ticks from the Unix Epoch (00:00:00 UTC, Thursday, 1 January 1970). This will truncate + /// to closest tick. + /// + public long EpochTicks => + UtcSeconds * TimeSpan.TicksPerSecond + TemporalHelpers.ExtractTicksFromNanosecond(Nanosecond); /// The time zone that this instance represents. public Zone Zone { get; } - /// Gets a value that represents the date and time of this instance. + /// Gets a value that represents the local date and time of this instance. + /// If is not known locally. /// If the value cannot be represented with DateTime /// If a truncation occurs during conversion - private DateTime DateTime + public DateTime LocalDateTime { get { + if (UnknownZoneInfo) + { + throw new TimeZoneNotFoundException(); + } + TemporalHelpers.AssertNoTruncation(this, nameof(DateTime)); TemporalHelpers.AssertNoOverflow(this, nameof(DateTime)); - return new DateTime(Year, Month, Day, Hour, Minute, Second).AddTicks( - TemporalHelpers.ExtractTicksFromNanosecond(Nanosecond)); + return new DateTime(Year, Month, Day, Hour, Minute, Second, DateTimeKind.Local) + .AddTicks(TemporalHelpers.ExtractTicksFromNanosecond(TruncatedNanos())); + } + } + + /// Gets a the UTC value that represents the date and time of this instance. + /// If the value cannot be represented with DateTime + /// If a truncation occurs during conversion + public DateTime UtcDateTime + { + get + { + DateTimeOffset dto; + try + { + dto = DateTimeOffset.FromUnixTimeSeconds(UtcSeconds); + } + catch (ArgumentOutOfRangeException) + { + throw new ValueOverflowException( + $"The value of {nameof(UtcSeconds)} is too large or small to be represented by {nameof(DateTime)}"); + } + + TemporalHelpers.AssertNoTruncation(this, nameof(DateTime)); + + dto = dto.AddTicks(TemporalHelpers.ExtractTicksFromNanosecond(Nanosecond)); + + return dto.UtcDateTime; } } /// - /// Returns the offset from UTC of this instance at the time it represents. if year is more than 9999, offset will - /// be taken from year 9999. + /// Gets the offset from UTC in seconds.
+ /// if is of type this will return .
+ /// otherwise is of type and this will return the UTC offset at the exact point of time of . ///
- public int OffsetSeconds => _offset ?? - Zone.OffsetSecondsAt(new DateTime(Year > 9999 ? 9999 : Year, Month, Day, Hour, Minute, Second)); + /// If is not known locally. + public int OffsetSeconds => _offsetSeconds ?? LookupOffsetFromZone(); /// Gets a value that represents the offset of this instance. private TimeSpan Offset => TimeSpan.FromSeconds(OffsetSeconds); @@ -249,12 +575,12 @@ public int CompareTo(object obj) return 0; } - if (!(obj is ZonedDateTime)) + if (obj is not ZonedDateTime time) { throw new ArgumentException($"Object must be of type {nameof(ZonedDateTime)}"); } - return CompareTo((ZonedDateTime)obj); + return CompareTo(time); } /// @@ -275,15 +601,8 @@ public int CompareTo(ZonedDateTime other) return 1; } - var thisEpochSeconds = this.ToEpochSeconds() - OffsetSeconds; - var otherEpochSeconds = other.ToEpochSeconds() - other.OffsetSeconds; - var epochComparison = thisEpochSeconds.CompareTo(otherEpochSeconds); - if (epochComparison != 0) - { - return epochComparison; - } - - return Nanosecond.CompareTo(other.Nanosecond); + var epochComparison = UtcSeconds.CompareTo(other.UtcSeconds); + return epochComparison != 0 ? epochComparison : Nanosecond.CompareTo(other.Nanosecond); } /// @@ -307,117 +626,130 @@ public bool Equals(ZonedDateTime other) return true; } - return Year == other.Year && - Month == other.Month && - Day == other.Day && - Hour == other.Hour && - Second == other.Second && - Nanosecond == other.Nanosecond && - Equals(Zone, other.Zone); + return GetHashCode() == other.GetHashCode(); } - /// Gets the year component of this instance. - public int Year - { - get - { - EvaluateUtcValue(); - return _year; - } - } + /// Gets the year component of this instance in the Locale of . + public int Year { get; } - /// Gets the month component of this instance. - public int Month - { - get - { - EvaluateUtcValue(); - return _month; - } - } + /// Gets the month component of this instance in the Locale of . + public int Month { get; } + + /// Gets the day of month component of this instance in the Locale of . + public int Day { get; } + + /// Gets the hour component of this instance in the Locale of . + public int Hour { get; } + + /// Gets the minute component of this instance in the Locale of . + public int Minute { get; } + + /// Gets the second component of this instance in the Locale of . + public int Second { get; } + + /// Gets the nanosecond component of this instance. + public int Nanosecond { get; } - /// Gets the day of month component of this instance. - public int Day + private static void ValidateComponents( + int year, + int month, + int day, + int hour, + int minute, + int second, + int nanosecond) { - get - { - EvaluateUtcValue(); - return _day; - } + Throw.ArgumentOutOfRangeException.IfValueNotBetween( + year, + TemporalHelpers.MinYear, + TemporalHelpers.MaxYear, + nameof(year)); + + Throw.ArgumentOutOfRangeException.IfValueNotBetween( + month, + TemporalHelpers.MinMonth, + TemporalHelpers.MaxMonth, + nameof(month)); + + Throw.ArgumentOutOfRangeException.IfValueNotBetween( + day, + TemporalHelpers.MinDay, + TemporalHelpers.MaxDayOfMonth(year, month), + nameof(day)); + + Throw.ArgumentOutOfRangeException.IfValueNotBetween( + hour, + TemporalHelpers.MinHour, + TemporalHelpers.MaxHour, + nameof(hour)); + + Throw.ArgumentOutOfRangeException.IfValueNotBetween( + minute, + TemporalHelpers.MinMinute, + TemporalHelpers.MaxMinute, + nameof(minute)); + + Throw.ArgumentOutOfRangeException.IfValueNotBetween( + second, + TemporalHelpers.MinSecond, + TemporalHelpers.MaxSecond, + nameof(second)); + + Throw.ArgumentOutOfRangeException.IfValueNotBetween( + nanosecond, + TemporalHelpers.MinNanosecond, + TemporalHelpers.MaxNanosecond, + nameof(nanosecond)); } - /// Gets the hour component of this instance. - public int Hour + private void SetAmbiguous(AmbiguityReason reason) { - get - { - EvaluateUtcValue(); - return _hour; - } + Ambiguous = true; + Reason = reason; } - /// Gets the minute component of this instance. - public int Minute + private DateTime ClrFriendly(LocalDateTime local) { - get - { - EvaluateUtcValue(); - return _minute; - } + var abs = Math.Abs(local.Year); + // we can only offset years that are in the CLR range, so we need to offset the year + var year = abs % 4 == 0 && (abs % 100 != 0 || abs % 400 == 0) + ? local.Year < 1 + ? 4 // first leap year in CLR type range + : 9996 // last leap year in CLR type range + : local.Year < 1 + ? 2 // first year that can safely be offset in CLR range, non-leap year. + : 9998; // last year that can safely be offset in CLR range, non-leap year. + + return new DateTime( + year, + local.Month, + local.Day, + local.Hour, + local.Minute, + local.Second).AddTicks(TruncatedNanos()); } - /// Gets the second component of this instance. - public int Second + private int TruncatedNanos() { - get - { - EvaluateUtcValue(); - return _second; - } + return Nanosecond / 1_000_000 * 1_000_000; } - /// Gets the nanosecond component of this instance. - public int Nanosecond { get; } - - private void EvaluateUtcValue() + private int LookupOffsetFromZone() { - if (!_utcSeconds.HasValue) + if (Zone is ZoneOffset zo) { - return; + return zo.OffsetSeconds; } - LocalDateTime local; - - lock (_lazyLock) - { - if (!_utcSeconds.HasValue) - { - return; - } - - local = TemporalHelpers.EpochSecondsAndNanoToDateTime(_utcSeconds.Value, Nanosecond); - - var time = new DateTime( - local.Year > 9999 ? 9999 : local.Year, - local.Month, - local.Day, - local.Hour, - local.Minute, - local.Second); - - _offset = Zone is ZoneOffset zo ? zo.OffsetSeconds : Zone.OffsetSecondsAt(time); - - local = TemporalHelpers.EpochSecondsAndNanoToDateTime(_utcSeconds.Value + _offset.Value, Nanosecond); + var utc = DateTimeOffset.FromUnixTimeSeconds(UtcSeconds) + .AddTicks(TemporalHelpers.ExtractTicksFromNanosecond(Nanosecond)); - _utcSeconds = null; - } + return Zone.OffsetSecondsAt(utc.UtcDateTime); + } - _year = local.Year; - _month = local.Month; - _day = local.Day; - _hour = local.Hour; - _minute = local.Minute; - _second = local.Second; + private TimeSpan LookupOffsetAt(DateTime dateTime) + { + return TimeSpan.FromSeconds(Zone.OffsetSecondsAt(dateTime)); } /// Converts this instance to an equivalent value @@ -426,14 +758,18 @@ private void EvaluateUtcValue() /// If a truncation occurs during conversion public DateTimeOffset ToDateTimeOffset() { - // we first get DateTime instance to force Truncation / Overflow checks - var dateTime = DateTime; - var offset = Offset; + if (UnknownZoneInfo) + { + throw new TimeZoneNotFoundException(); + } + + TemporalHelpers.AssertNoTruncation(this, nameof(DateTimeOffset)); + TemporalHelpers.AssertNoOverflow(this, nameof(DateTimeOffset)); - TemporalHelpers.AssertNoTruncation(offset, nameof(DateTimeOffset)); - TemporalHelpers.AssertNoOverflow(offset, nameof(DateTimeOffset)); + var dto = DateTimeOffset.FromUnixTimeSeconds(UtcSeconds) + .AddTicks(TemporalHelpers.ExtractTicksFromNanosecond(Nanosecond)); - return new DateTimeOffset(dateTime, offset); + return dto.ToOffset(Offset); } /// Returns a value indicating whether this instance is equal to a specified object. @@ -444,16 +780,6 @@ public DateTimeOffset ToDateTimeOffset() /// public override bool Equals(object obj) { - if (obj is null) - { - return false; - } - - if (ReferenceEquals(this, obj)) - { - return true; - } - return obj is ZonedDateTime dateTime && Equals(dateTime); } @@ -463,13 +789,9 @@ public override int GetHashCode() { unchecked { - var hashCode = Year; - hashCode = (hashCode * 397) ^ Month; - hashCode = (hashCode * 397) ^ Day; - hashCode = (hashCode * 397) ^ Hour; - hashCode = (hashCode * 397) ^ Second; - hashCode = (hashCode * 397) ^ Nanosecond; - hashCode = (hashCode * 397) ^ (Zone != null ? Zone.GetHashCode() : 0); + var hashCode = Nanosecond; + hashCode = (hashCode * 397) ^ Zone.GetHashCode(); + hashCode = (hashCode * 397) ^ UtcSeconds.GetHashCode(); return hashCode; } } @@ -478,13 +800,14 @@ public override int GetHashCode() /// String representation of this Point. public override string ToString() { - if (_utcSeconds.HasValue) + if (UnknownZoneInfo) { - return $"Lazy-ZonedDateTime{{EpochSeconds:{_utcSeconds.Value} Nanos:{Nanosecond} Zone:{Zone}}}"; + return @$"{{UtcSeconds: {UtcSeconds}, Nanoseconds: {Nanosecond}, Zone: {Zone}}}"; } - return - $"{TemporalHelpers.ToIsoDateString(Year, Month, Day)}T{TemporalHelpers.ToIsoTimeString(Hour, Minute, Second, Nanosecond)}{Zone}"; + var isoDate = TemporalHelpers.ToIsoDateString(Year, Month, Day); + var isoTime = TemporalHelpers.ToIsoTimeString(Hour, Minute, Second, Nanosecond); + return $"{isoDate}T{isoTime}{Zone}"; } ///