From aefaa13b6640ffe12e756d9a0ea67e4659d8f31d Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Tue, 24 Mar 2026 06:43:14 +0200 Subject: [PATCH] Fix DST offset applied incorrectly near daylight saving transitions (#2355) The Utc() method treated local time as a UTC instant when querying GetUtcOffset, causing the wrong DST offset near transitions. Use a two-pass approach matching the ES spec: estimate UTC first, then refine with the correct offset at that UTC instant. Same fix in TryParse for date strings without timezone info. Co-Authored-By: Claude Opus 4.6 (1M context) --- Jint.Tests/Runtime/DateTests.cs | 28 ++++++++++++++++++++++++++++ Jint/Native/Date/DatePrototype.cs | 8 +++++++- Jint/Runtime/DefaultTimeSystem.cs | 6 +++++- 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/Jint.Tests/Runtime/DateTests.cs b/Jint.Tests/Runtime/DateTests.cs index 6ced862c92..e590f75a7c 100644 --- a/Jint.Tests/Runtime/DateTests.cs +++ b/Jint.Tests/Runtime/DateTests.cs @@ -226,4 +226,32 @@ public void DateTimeMaxValueFlag() jsDate = new JsDate(_engine, date); Assert.Equal(DateFlags.None, jsDate._dateValue.Flags); } + + [Fact] + public void DstTransitionShouldUseCorrectOffset() + { + TimeZoneInfo nztz; + try + { + nztz = TimeZoneInfo.FindSystemTimeZoneById("New Zealand Standard Time"); + } + catch (TimeZoneNotFoundException) + { + nztz = TimeZoneInfo.FindSystemTimeZoneById("Pacific/Auckland"); + } + + var engine = new Engine(options => options.LocalTimeZone(nztz)); + + // NZDT (GMT+13) ends at 3:00 AM on the first Sunday of April 2025. + // At midnight April 6, we are still in NZDT (GMT+13). + var result1 = engine.Evaluate("new Date(2025, 3, 6, 0, 0, 0).toString()").AsString(); + Assert.Contains("GMT+1300", result1); + Assert.Contains("Apr 06 2025 00:00:00", result1); + + // NZDT (GMT+13) begins at 2:00 AM on the last Sunday of September 2025. + // At midnight Sep 28, we are still in NZST (GMT+12). + var result2 = engine.Evaluate("new Date(2025, 8, 28, 0, 0, 0).toString()").AsString(); + Assert.Contains("GMT+1200", result2); + Assert.Contains("Sep 28 2025 00:00:00", result2); + } } diff --git a/Jint/Native/Date/DatePrototype.cs b/Jint/Native/Date/DatePrototype.cs index bb4bcc2a15..cd4503a34a 100644 --- a/Jint/Native/Date/DatePrototype.cs +++ b/Jint/Native/Date/DatePrototype.cs @@ -1295,8 +1295,14 @@ private DatePresentation LocalTime(DatePresentation t) internal DatePresentation Utc(DatePresentation t) { + // t represents local time encoded as epoch milliseconds. GetUtcOffset treats + // its argument as a UTC instant, so a single-pass conversion uses the wrong + // DST offset near transitions. Use a two-pass approach matching the ES spec's + // UTC(t): first estimate UTC, then get the correct offset at that UTC instant. var offset = _timeSystem.GetUtcOffset(t.Value).TotalMilliseconds; - return t - offset; + var estimatedUtc = (t - offset).Value; + var refinedOffset = _timeSystem.GetUtcOffset(estimatedUtc).TotalMilliseconds; + return t - refinedOffset; } private static int HourFromTime(DatePresentation t) diff --git a/Jint/Runtime/DefaultTimeSystem.cs b/Jint/Runtime/DefaultTimeSystem.cs index d65af73b11..3715d65266 100644 --- a/Jint/Runtime/DefaultTimeSystem.cs +++ b/Jint/Runtime/DefaultTimeSystem.cs @@ -116,8 +116,12 @@ public virtual bool TryParse(string date, out long epochMilliseconds) if (convertToUtcAfter) { + // datePresentation is local time encoded as epoch milliseconds. + // Two-pass conversion to get correct DST offset near transitions. var offset = GetUtcOffset(datePresentation.Value).TotalMilliseconds; - datePresentation -= offset; + var estimatedUtc = (datePresentation - offset).Value; + var refinedOffset = GetUtcOffset(estimatedUtc).TotalMilliseconds; + datePresentation -= refinedOffset; } epochMilliseconds = datePresentation.Value;