fix: align Date::Language::str2time with Date::Parse behavior#110
Conversation
Greptile SummaryThis PR fixes two real behavioral divergences between
Confidence Score: 4/5Safe to merge after addressing the missing -1 / overflow guards in the eval blocks. One P1 finding: the eval blocks don't replicate Date::Parse's -1 sentinel and negative-result overflow checks, leaving a behavioral gap on older Time::Local versions and 32-bit platforms. All other findings are P2. lib/Date/Language.pm — the timegm/timelocal eval paths (lines 107–121)
|
| Filename | Overview |
|---|---|
| lib/Date/Language.pm | Year-inference and eval-wrapping fixes are correct, but the new eval blocks are missing the -1 sentinel and negative-result overflow guards present in the Date::Parse reference implementation. |
| t/lang-str2time.t | New test file covering year inference, invalid input handling, and a German round-trip; has an unused POSIX qw(mktime) import and skips the future-day test on days 28–31. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[str2time input] --> B[strptime parse]
B --> C{result empty?}
C -- yes --> D[return undef]
C -- no --> E[fill defaults: hh/mm/ss/month/day]
E --> F{year defined?}
F -- no --> G{month > cur_month OR same month and day > cur_day?}
G -- yes --> H[year = last year]
G -- no --> I[year = current year]
F -- yes --> J[use parsed year]
H --> K
I --> K
J --> K[Validate ranges: month<=11, day 1-31, hh<=23, mm<=59, ss<=59]
K -- invalid --> D
K -- valid --> L{zone defined?}
L -- yes --> M[eval timegm / catch exception]
L -- no --> N[eval timelocal / catch exception]
M --> O{result defined?}
N --> O
O -- no --> D
O -- yes --> P[result -= zone if zoned]
P --> Q[return result]
Reviews (1): Last reviewed commit: "fix: align Date::Language::str2time with..." | Re-trigger Greptile
| if (defined $zone) { | ||
| $result = eval { | ||
| local $SIG{__DIE__} = sub {}; # match Date::Parse behavior | ||
| timegm($ss,$mm,$hh,$day,$month,$year); | ||
| }; | ||
| return undef unless defined $result; | ||
| $result -= $zone; | ||
| } | ||
| else { | ||
| $result = eval { | ||
| local $SIG{__DIE__} = sub {}; # match Date::Parse behavior | ||
| timelocal($ss,$mm,$hh,$day,$month,$year); | ||
| }; | ||
| return undef unless defined $result; | ||
| } |
There was a problem hiding this comment.
Missing
-1 sentinel and overflow guards from Date::Parse
The PR claims parity with Date::Parse::str2time, but the eval blocks are missing two checks that exist in the reference implementation.
- On older
Time::Localversions (pre-1.23, ~pre-2011),timegm/timelocalreturns-1to signal error instead of dying. The eval won't catch that, so-1would be returned as a valid epoch to callers. - Integer overflow on 32-bit systems can cause
timegm/timelocalto return a large negative value;Date::Parserejects$result < 0 && $year >= 1970.
The corresponding guard in Date::Parse::str2time (lines 302–308 / 317–323):
return undef
if !defined $result
or $result == -1
&& join("",$ss,$mm,$hh,$day,$month,$year)
ne "595923311169";
return undef if $result < 0 && $year >= 1970;Without these, the two code paths diverge for dates on or near epoch −1, and for overflow inputs on 32-bit platforms.
| use Test::More; | ||
| use Date::Language; | ||
| use Date::Parse; | ||
| use POSIX qw(mktime); |
| return undef | ||
| unless(defined $month && $month <= 11 && $day >= 1 && $day <= 31 | ||
| && $hh <= 23 && $mm <= 59 && $ss <= 59); |
There was a problem hiding this comment.
Redundant
defined $month guard
$month is unconditionally assigned from $lt[4] on lines 89–90 when it was previously undefined, so by line 101 it is always defined. The defined $month check is dead code. Date::Parse::str2time omits it for the same reason.
| return undef | |
| unless(defined $month && $month <= 11 && $day >= 1 && $day <= 31 | |
| && $hh <= 23 && $mm <= 59 && $ss <= 59); | |
| return undef | |
| unless($month <= 11 && $day >= 1 && $day <= 31 | |
| && $hh <= 23 && $mm <= 59 && $ss <= 59); |
|
@Koan-Bot rebase |
Two bugs in Date::Language::str2time compared to Date::Parse::str2time: 1. Year inference missed day comparison: when parsing a date without a year where the month matches the current month, only the month was compared. This meant "15 Apr" parsed on April 12 would resolve to the current year (future date) instead of the previous year, unlike Date::Parse which correctly treats same-month future days as referring to last year. 2. No error protection: timegm/timelocal could die on invalid input (e.g. day=32), while Date::Parse wraps these calls in eval and returns undef. Added input validation and eval guards to match. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
All three review comments addressed: - **P1 — Missing `-1` sentinel and overflow guards**: Added the `-1` sentinel check and `$result < 0` overflow guard to both the `timegm` and `timelocal` eval blocks, matching `Date::Parse::str2time` exactly (including the `localtime(-1)` approach for the `timelocal` path and the different year threshold: `>= 1970` for `timegm`, `>= 1971` for `timelocal`). - **P2 — Unused `POSIX qw(mktime)` import**: Removed the unused import from `t/lang-str2time.t`. - **P2 — Redundant `defined $month` guard**: Removed `defined $month &&` from the validation check since `$month` is always defined at that point.
Rebase with requested adjustmentsBranch Changes applied
StatsActions performed
CI statusCI will be checked asynchronously. Automated by Kōan |
cf2c050 to
72f7046
Compare
What
Fixes two behavioral differences between
Date::Language::str2timeandDate::Parse::str2time.Why
Date::Language::str2timeandDate::Parse::str2timeshould behave consistently for the same inputs. Two discrepancies caused surprising results:Year inference bug: parsing "15 Apr" on April 12 gave 2026 (Date::Language) vs 2025 (Date::Parse). Date::Language only compared months, missing the day-of-month check for same-month future dates.
Fatal error on invalid input:
Date::Language::str2time("32 Jun 2024")died withDay '32' out of range, whileDate::Parse::str2timereturnsundef. The timegm/timelocal calls were unprotected.How
Testing
New
t/lang-str2time.tcovers:Full test suite passes (28 test files).
🤖 Generated with Claude Code
Quality Report
Changes: 2 files changed, 137 insertions(+), 4 deletions(-)
Code scan: clean
Tests: skipped
Branch hygiene: clean
Generated by Kōan post-mission quality pipeline