From a38152ec45d52dabcfa78d9eb91e1b740b2a2d97 Mon Sep 17 00:00:00 2001 From: Daniel Chalmers Date: Fri, 10 Jul 2026 20:37:22 -0500 Subject: [PATCH 1/2] Fix automatic countdown being skewed by the UTC offset --- .../TimeStringFormatterTests.cs | 43 ++++++++++++++++++- DesktopClock/Utilities/TimeStringFormatter.cs | 6 ++- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/DesktopClock.Tests/TimeStringFormatterTests.cs b/DesktopClock.Tests/TimeStringFormatterTests.cs index 88ea69b..1222b7a 100644 --- a/DesktopClock.Tests/TimeStringFormatterTests.cs +++ b/DesktopClock.Tests/TimeStringFormatterTests.cs @@ -106,7 +106,48 @@ public void Format_UsesHumanizerWhenCountdownFormatMissing() " ", CultureInfo.CurrentCulture); - Assert.Equal(countdownTo.Humanize(utcDate: false, dateToCompareAgainst: nowDateTime), result); + var localNow = DateTime.SpecifyKind(nowDateTime, DateTimeKind.Local); + Assert.Equal(countdownTo.Humanize(utcDate: false, dateToCompareAgainst: localNow), result); + } + finally + { + CultureInfo.CurrentCulture = originalCulture; + CultureInfo.CurrentUICulture = originalUiCulture; + } + } + + [Theory] + [InlineData(0, "now")] + [InlineData(3, "3 hours from now")] + [InlineData(-3, "3 hours ago")] + [InlineData(72, "3 days from now")] + [InlineData(-72, "3 days ago")] + public void Format_HumanizedCountdownIsRelativeToNow(int hoursFromNow, string expected) + { + // The wall-clock values have Kind Unspecified, which Humanizer used to shift by the + // machine's UTC offset, e.g. a target of "now" read as "6 hours from now" on UTC-6. + var originalCulture = CultureInfo.CurrentCulture; + var originalUiCulture = CultureInfo.CurrentUICulture; + + try + { + var enUs = new CultureInfo("en-US"); + CultureInfo.CurrentCulture = enUs; + CultureInfo.CurrentUICulture = enUs; + + var now = new DateTimeOffset(2026, 1, 1, 12, 0, 0, TimeSpan.Zero); + var countdownTo = now.DateTime.AddHours(hoursFromNow); + + var result = TimeStringFormatter.Format( + now, + now.DateTime, + TimeZoneInfo.Utc, + countdownTo, + "HH:mm", + "", + CultureInfo.CurrentCulture); + + Assert.Equal(expected, result); } finally { diff --git a/DesktopClock/Utilities/TimeStringFormatter.cs b/DesktopClock/Utilities/TimeStringFormatter.cs index cfa6ef6..d132818 100644 --- a/DesktopClock/Utilities/TimeStringFormatter.cs +++ b/DesktopClock/Utilities/TimeStringFormatter.cs @@ -24,7 +24,11 @@ public static string Format( } else if (string.IsNullOrWhiteSpace(countdownFormat)) { - result = countdownTo.Humanize(utcDate: false, dateToCompareAgainst: nowDateTime); + // Both values are local wall-clock times, but their Kind is usually Unspecified, which Humanizer + // shifts by the UTC offset when it converts the comparison date to local time. Pinning the Kind + // makes that conversion a no-op so the countdown reads correctly in every time zone. + var localNow = DateTime.SpecifyKind(nowDateTime, DateTimeKind.Local); + result = countdownTo.Humanize(utcDate: false, dateToCompareAgainst: localNow); } else { From 57528211073c9ce25e46f4fca52c21dd05d39925 Mon Sep 17 00:00:00 2001 From: Daniel Chalmers Date: Fri, 10 Jul 2026 20:44:24 -0500 Subject: [PATCH 2/2] Condense comment --- DesktopClock/Utilities/TimeStringFormatter.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/DesktopClock/Utilities/TimeStringFormatter.cs b/DesktopClock/Utilities/TimeStringFormatter.cs index d132818..2fb63c3 100644 --- a/DesktopClock/Utilities/TimeStringFormatter.cs +++ b/DesktopClock/Utilities/TimeStringFormatter.cs @@ -24,9 +24,7 @@ public static string Format( } else if (string.IsNullOrWhiteSpace(countdownFormat)) { - // Both values are local wall-clock times, but their Kind is usually Unspecified, which Humanizer - // shifts by the UTC offset when it converts the comparison date to local time. Pinning the Kind - // makes that conversion a no-op so the countdown reads correctly in every time zone. + // Humanizer shifts Unspecified times by the UTC offset when localizing, so pin the Kind to make that conversion a no-op. var localNow = DateTime.SpecifyKind(nowDateTime, DateTimeKind.Local); result = countdownTo.Humanize(utcDate: false, dateToCompareAgainst: localNow); }