From 80467bb2cf69b0c7fc16949c94bb73e8558a2a83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C5=8Dan?= Date: Mon, 11 May 2026 06:07:09 -0600 Subject: [PATCH] fix: time2str stores tz-adjusted epoch in $me->[9] instead of real epoch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When time2str is called with a timezone, $time is modified by tz_offset() before gmtime(), but then the modified value was stored in $me->[9]. Format codes that read $me->[9] (format_G for GPS week, format_z for numeric offset DST detection) got the wrong timestamp. strftime already stored $epoch (real epoch) correctly — this aligns time2str with the same behavior. The bug manifests at GPS week boundaries: time2str("%G", $t, "EST") returned week N-1 instead of N when the timezone offset pushed the adjusted time across the 604800-second boundary. Co-Authored-By: Claude Opus 4.6 --- lib/Date/Format/Generic.pm | 2 +- t/time2str-tz-epoch.t | 43 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 t/time2str-tz-epoch.t diff --git a/lib/Date/Format/Generic.pm b/lib/Date/Format/Generic.pm index b3b8bdf..8eca006 100644 --- a/lib/Date/Format/Generic.pm +++ b/lib/Date/Format/Generic.pm @@ -92,7 +92,7 @@ sub time2str { @$me = localtime($time); } - $me->[9] = $time; + $me->[9] = $epoch; _subs($me,$fmt); } diff --git a/t/time2str-tz-epoch.t b/t/time2str-tz-epoch.t new file mode 100644 index 0000000..64146d0 --- /dev/null +++ b/t/time2str-tz-epoch.t @@ -0,0 +1,43 @@ +use strict; +use warnings; +use Test::More; +use Date::Format qw(time2str); +use Time::Local qw(timegm); + +# Regression: time2str stored tz-adjusted epoch in $me->[9] instead of +# the real epoch. This caused format_G (GPS week) to give wrong results +# when a non-UTC timezone was specified and the epoch was near a GPS week +# boundary. format_z was also affected (DST lookup used wrong time). + +my $gps_epoch = 315964800; # midnight UTC, Jan 6, 1980 + +# GPS week 1 starts exactly 604800s after GPS epoch +my $week1_start = $gps_epoch + 604800; # midnight UTC, Jan 13, 1980 + +# Without timezone: both should agree on the week boundary +is(time2str("%G", $week1_start, "UTC"), "1", + "week 1 start with UTC"); + +# With EST (-5h): the real epoch hasn't changed, so GPS week must be the same. +# Before the fix, $me->[9] was epoch+offset, making format_G compute +# int((epoch-18000 - gps_epoch) / 604800) = 0 instead of 1. +is(time2str("%G", $week1_start, "EST"), "1", + "week 1 start with EST — GPS week must match UTC"); + +# Edge case: one second before week 1 +is(time2str("%G", $week1_start - 1, "EST"), "0", + "one second before week 1, EST — still week 0"); + +# A date well within a week should be unaffected by timezone +my $mid_week = $gps_epoch + 604800 * 100 + 43200; # week 100, noon UTC +is(time2str("%G", $mid_week, "UTC"), "100", "mid-week UTC"); +is(time2str("%G", $mid_week, "EST"), "100", "mid-week EST"); +is(time2str("%G", $mid_week, "JST"), "100", "mid-week JST (+9)"); + +# format_s (%s) should always return the real epoch regardless of timezone +is(time2str("%s", $week1_start, "UTC"), "$week1_start", + "%s with UTC returns real epoch"); +is(time2str("%s", $week1_start, "EST"), "$week1_start", + "%s with EST returns real epoch (not adjusted)"); + +done_testing;