fix(util): parseDate rejects calendar-invalid dates (Feb 30, Feb 29 non-leap, Apr 31, ...)#178
Closed
sroussey wants to merge 1 commit into
Closed
fix(util): parseDate rejects calendar-invalid dates (Feb 30, Feb 29 non-leap, Apr 31, ...)#178sroussey wants to merge 1 commit into
sroussey wants to merge 1 commit into
Conversation
Previously parseDate accepted range-valid but calendar-invalid inputs like
"2025-02-30", "2025-02-29" (non-leap), and "2025-04-31". The returned
{year, month, day} then flowed into downstream `new Date(...)` calls,
which silently rolled forward (Feb 30 -> Mar 2), shifting point-in-time
semantics of ChangeLog / spac_history / offering-history change_date
fields and defeating as_of guards.
Add a UTC-Date probe-back-check after the existing range check: if
`new Date(Date.UTC(y, m-1, d))` doesn't round-trip to the same y/m/d,
throw `Invalid calendar date: <input>` instead of returning a lie.
Tests cover Feb 30 (any year), Feb 29 leap-year rules (2024/2000 accept;
2025/2023/1900 reject), the impossible 31st of 30-day months (Apr/Jun/
Sep/Nov), out-of-range month/day, and a regression fence for all five
supported input shapes on a real date.
Contributor
Author
|
Integrated into #176. The Generated by Claude Code |
sroussey
pushed a commit
that referenced
this pull request
Jul 2, 2026
Correctness: - parseDate: restore the literal year via setUTCFullYear before the calendar probe. Date.UTC remaps years 0-99 to 1900-1999, so a valid 4-digit year like '0099-01-01' was wrongly rejected as an invalid calendar date. Regression test added; Feb-30-style rollover detection is unaffected. Resilience regression from #178's stricter parseDate throw: - FetchQuarterlyIndexTask / FetchQuarterlyFormIdxTask: wrap the per-row secDate() in try/catch so a single calendar-invalid EDGAR 'Date Filed' row is skipped+warned instead of aborting the whole quarter's batch (parseDate now throws where it previously rolled forward). Cleanup: - XbrlFactRepo.clearForAccession delegates to replaceForAccession(acc, [], { intentionalClear: true }), removing a byte-identical duplicate delete loop. Skipped (documented in review): the 4-way junction-repo copy-paste (would need a shared CanonicalJunctionRepo base — larger refactor beyond this diff); the CLI --date throw (operator fail-fast is acceptable); wiring clearForAccession into xbrlEnrichment (pre-existing behavior #177 deliberately left unchanged); and 5 concurrency observations that are pre-existing #175 reap-path limitations or documented single-process scope, not wave-2 regressions.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
src/util/parseDate.tsalready range-checks month (1-12) and day (1-31), but thatstill admits calendar-invalid inputs —
2025-02-30,2025-02-29in a non-leapyear,
2025-04-31, and the equivalents for June / September / November. Thereturned
{ year, month, day }string is subsequently handed tonew Date(...)in the storage/history layer, which silently rolls forward(
2025-02-30→ March 2), corrupting the point-in-time semantics ofChangeLog/spac_history/ offering-historychange_daterows and defeatingthe
as_ofguards.Fix
After the existing range check, probe the calendar via a UTC
Dateand rejectany input the calendar refuses to preserve verbatim:
The return shape is unchanged. Real dates flow through untouched.
Tests
Added to
src/util/parseDate.test.ts:divisible-by-100-but-not-400 rule) and accepted in 2024 and 2000 (the
divisible-by-400 rule).
Invalid date formatfrom the existing range check (regression fence — newprobe must not swallow the pre-existing error message).
yyyy-MM-dd,yyyy/MM/dd,MM/dd/yyyy,MM-dd-yyyy,yyyyMMdd) still accept awell-formed real date (
2024-02-29).Verification
bun test src/util/parseDate.test.ts— 20 pass, 0 fail (7 new tests added).bun test— no new failures introduced. The 7 remaining failures arepre-existing network timeouts on
FetchQuarterlyIndexTask/FetchDailyIndexTask(verified by stashing the change and reproducing).npx tsc --noEmit— clean.Generated by Claude Code