From 86447236c00a6dece6d91d6a8cb59621aeca9415 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 16 Jun 2026 14:40:53 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20add=20DateTimeOffset=20interop=20?= =?UTF-8?q?=E2=80=94=20constructor,=20ToDateTimeOffsetUtc(),=20and=20impli?= =?UTF-8?q?cit=20operators?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DateTimeOffset is widely used in modern .NET (ASP.NET Core, EF Core, System.Text.Json) and its absence from DateTimeNano means users must convert manually before passing to APIs that require it. Changes: - DateTimeNano(DateTimeOffset): constructs from any offset; converts to UTC before computing NanosecondsSinceEpoch. - ToDateTimeOffsetUtc(): returns the UTC instant as DateTimeOffset with zero offset. Sub-microsecond nanoseconds are noted in the XML doc as truncated. - implicit operator DateTimeOffset(DateTimeNano): lossless for DateTime-precision timestamps; nanosecond-only sub-microsecond part noted. - implicit operator DateTimeNano(DateTimeOffset): always normalises to UTC, mirroring the existing DateTime implicit operator behaviour. 6 new tests added (46 total, was 40). All pass on net8.0, net9.0, net10.0. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- DateTimeNano.Tests/DateTimeNanoTests.cs | 55 +++++++++++++++++++++++++ DateTimeNano/DateTimeNano.cs | 23 +++++++++++ 2 files changed, 78 insertions(+) diff --git a/DateTimeNano.Tests/DateTimeNanoTests.cs b/DateTimeNano.Tests/DateTimeNanoTests.cs index 7f79241..db3d17d 100644 --- a/DateTimeNano.Tests/DateTimeNanoTests.cs +++ b/DateTimeNano.Tests/DateTimeNanoTests.cs @@ -334,5 +334,60 @@ public void Now_ShouldReturnRecentUtcTime() Assert.That(now.ToDateTimeUtc(), Is.GreaterThanOrEqualTo(before)); Assert.That(now.ToDateTimeUtc(), Is.LessThanOrEqualTo(after)); } + + // ── DateTimeOffset interop ──────────────────────────────────────────────── + + [Test] + public void Constructor_DateTimeOffset_UtcOffset_ShouldStoreCorrectNanoseconds() + { + var dto = new DateTimeOffset(2025, 2, 10, 20, 27, 12, 123, TimeSpan.Zero); + var nano = new Seerstone.DateTimeNano(dto); + Assert.That(nano.NanosecondsSinceEpoch, Is.EqualTo(1_739_219_232_123_000_000UL)); + } + + [Test] + public void Constructor_DateTimeOffset_NonUtcOffset_ShouldConvertToUtc() + { + // +05:30 (IST) — same instant as 15:00 UTC + var dto = new DateTimeOffset(2025, 2, 10, 20, 30, 0, TimeSpan.FromHours(5.5)); + var expected = new Seerstone.DateTimeNano(dto.UtcDateTime); + var actual = new Seerstone.DateTimeNano(dto); + Assert.That(actual.NanosecondsSinceEpoch, Is.EqualTo(expected.NanosecondsSinceEpoch)); + } + + [Test] + public void ToDateTimeOffsetUtc_ShouldReturnUtcOffset() + { + var nano = new Seerstone.DateTimeNano(1_739_219_232_123_000_000UL); + var dto = nano.ToDateTimeOffsetUtc(); + Assert.That(dto.Offset, Is.EqualTo(TimeSpan.Zero)); + Assert.That(dto.UtcDateTime, Is.EqualTo(new DateTime(2025, 2, 10, 20, 27, 12, 123, DateTimeKind.Utc))); + } + + [Test] + public void ImplicitOperator_DateTimeNanoToDateTimeOffset_ShouldBeUtc() + { + var nano = new Seerstone.DateTimeNano(1_739_219_232_123_000_000UL); + DateTimeOffset dto = nano; + Assert.That(dto.Offset, Is.EqualTo(TimeSpan.Zero)); + Assert.That(dto.UtcDateTime, Is.EqualTo(nano.ToDateTimeUtc())); + } + + [Test] + public void ImplicitOperator_DateTimeOffsetToDateTimeNano_RoundTrips() + { + var original = new DateTimeOffset(2025, 2, 10, 20, 27, 12, 123, TimeSpan.Zero); + Seerstone.DateTimeNano nano = original; + DateTimeOffset roundTripped = nano; + Assert.That(roundTripped, Is.EqualTo(original)); + } + + [Test] + public void ImplicitOperator_NonUtcDateTimeOffset_ShouldNormalisedToUtc() + { + var dto = new DateTimeOffset(2025, 2, 10, 21, 27, 12, 123, TimeSpan.FromHours(1)); + Seerstone.DateTimeNano nano = dto; + Assert.That(nano.ToDateTimeUtc(), Is.EqualTo(dto.UtcDateTime)); + } } } diff --git a/DateTimeNano/DateTimeNano.cs b/DateTimeNano/DateTimeNano.cs index 911dea1..54116a0 100644 --- a/DateTimeNano/DateTimeNano.cs +++ b/DateTimeNano/DateTimeNano.cs @@ -380,5 +380,28 @@ public override string ToString() /// Implicitly converts a to a . public static implicit operator DateTimeNano(DateTime d) => new DateTimeNano(d); + + /// + /// Create a from a . + /// The offset is converted to UTC before the nanosecond value is computed, + /// so the stored timestamp always represents the UTC instant. + /// + /// The source . + public DateTimeNano(DateTimeOffset datetimeOffset) + : this(datetimeOffset.UtcDateTime) { } + + /// + /// Returns the UTC date and time as a with a zero UTC offset. + /// Sub-microsecond nanoseconds are not representable in + /// and are truncated; access them via . + /// + /// A in UTC. + public DateTimeOffset ToDateTimeOffsetUtc() => new DateTimeOffset(DateTime, TimeSpan.Zero); + + /// Implicitly converts a to a UTC . + public static implicit operator DateTimeOffset(DateTimeNano d) => d.ToDateTimeOffsetUtc(); + + /// Implicitly converts a to a . + public static implicit operator DateTimeNano(DateTimeOffset d) => new DateTimeNano(d); } }