From 4b6153285040591d626cfd284d25bc25ca598338 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C5=8Dan?= Date: Wed, 6 May 2026 05:29:54 -0600 Subject: [PATCH] fix: align Date::Language::str2time year handling with Date::Parse MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Date::Language::str2time was missing two pieces of year normalization that Date::Parse::str2time applies: 1. Century restoration — when strptime returns a $century value (from 4-digit year input), the year must be restored via $year += 1900. Without this, Language.pm relied on Time::Local treating small values as years-since-1900, which works but is fragile. 2. Two-digit year POSIX normalization — Date::Parse uses a fixed threshold (69-99 → 1900s, 00-68 → 2000s) matching POSIX strptime conventions. Language.pm delegated this to Time::Local's sliding window, which on Time::Local >= 1.28 centers on the current year, producing different results for years 69-76 (e.g., "1 Jun 69" resolves to 2069 via Language but 1969 via Parse). Also adds explicit fractional-second handling ($frac separation and re-addition) to match Date::Parse::str2time's approach, instead of relying on Time::Local accepting float seconds. Co-Authored-By: Claude Opus 4.6 --- lib/Date/Language.pm | 15 ++++++++++-- t/lang-str2time.t | 56 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/lib/Date/Language.pm b/lib/Date/Language.pm index 17570f4..4af31b6 100644 --- a/lib/Date/Language.pm +++ b/lib/Date/Language.pm @@ -79,13 +79,16 @@ sub str2time return undef unless @t; - my($ss,$mm,$hh,$day,$month,$year,$zone) = @t; + my($ss,$mm,$hh,$day,$month,$year,$zone,$century) = @t; my @lt = localtime(time); $hh ||= 0; $mm ||= 0; $ss ||= 0; + my $frac = $ss - int($ss); + $ss = int $ss; + $month = $lt[4] unless(defined $month); @@ -98,6 +101,14 @@ sub str2time $year = $is_future ? ($lt[5] - 1) : $lt[5]; } + # Restore 4-digit year when century was present in the input + $year += 1900 if defined $century; + + # Normalize two-digit years using POSIX convention (match Date::Parse behavior). + # Without this, Time::Local's own sliding window produces different results than + # Date::Parse::str2time for the same input string. + $year += ($year >= 69 ? 1900 : 2000) if $year < 100; + return undef unless($month <= 11 && $day >= 1 && $day <= 31 && $hh <= 23 && $mm <= 59 && $ss <= 59); @@ -132,7 +143,7 @@ sub str2time return undef if $result < 0 && $year >= 1971; } - return $result; + return $result + $frac; } 1; diff --git a/t/lang-str2time.t b/t/lang-str2time.t index 658ed48..d7cc7d3 100644 --- a/t/lang-str2time.t +++ b/t/lang-str2time.t @@ -105,4 +105,60 @@ my $lang = Date::Language->new('English'); is($parsed, $t, "German round-trip ctime/str2time"); } +# --- Two-digit year normalization: must match Date::Parse --- +# Without explicit normalization, Date::Language relies on Time::Local's +# sliding window which diverges from Date::Parse's fixed POSIX threshold. + +{ + for my $y (69, 70, 75, 95, 99) { + my $date = "1 Jun $y GMT"; + my $dp = str2time($date); + my $dl = $lang->str2time($date); + ok(defined $dp, "Date::Parse parses '1 Jun $y GMT'"); + ok(defined $dl, "Date::Language parses '1 Jun $y GMT'"); + if (defined $dp && defined $dl) { + my $dp_year = (gmtime($dp))[5] + 1900; + my $dl_year = (gmtime($dl))[5] + 1900; + is($dl_year, $dp_year, + "two-digit year $y: Language ($dl_year) matches Parse ($dp_year)"); + } + } + + # Also test years in the 2000s range + for my $y (0, 1, 26, 50, 68) { + my $date = "15 Mar $y GMT"; + my $dp = str2time($date); + my $dl = $lang->str2time($date); + ok(defined $dp, "Date::Parse parses '15 Mar $y GMT'"); + ok(defined $dl, "Date::Language parses '15 Mar $y GMT'"); + if (defined $dp && defined $dl) { + my $dp_year = (gmtime($dp))[5] + 1900; + my $dl_year = (gmtime($dl))[5] + 1900; + is($dl_year, $dp_year, + "two-digit year $y: Language ($dl_year) matches Parse ($dp_year)"); + } + } +} + +# --- Four-digit year with century: must survive round-trip --- + +{ + my $date = "15 Mar 2024 10:30:00 GMT"; + my $dp = str2time($date); + my $dl = $lang->str2time($date); + ok(defined $dp && defined $dl, "4-digit year parses in both"); + is($dl, $dp, "4-digit year: Language matches Parse exactly"); +} + +# --- Fractional seconds preserved --- + +{ + my $date = "2024-01-15T12:34:56.789 GMT"; + my $result = $lang->str2time($date); + ok(defined $result, "fractional seconds date parses"); + my $frac = $result - int($result); + ok($frac > 0.78 && $frac < 0.80, + "fractional seconds preserved (got $frac, expected ~0.789)"); +} + done_testing;