Skip to content

fix(parser,transpiler): #66 - computed-endpoint ranges, YEARFRAC 30/360, array-criteria SUMIFS, no silent partial parses#67

Merged
ebootheee merged 3 commits into
mainfrom
fix/structure-fidelity-66
Jun 10, 2026
Merged

fix(parser,transpiler): #66 - computed-endpoint ranges, YEARFRAC 30/360, array-criteria SUMIFS, no silent partial parses#67
ebootheee merged 3 commits into
mainfrom
fix/structure-fidelity-66

Conversation

@ebootheee

Copy link
Copy Markdown
Owner

Fixes the three structure-fidelity classes behind #66 (the 383k residual divergent cells on 4 A-1 sheets after v0.3.0). Full mechanism write-ups in the commit message; headlines:

  • Class B — ref:OFFSET(...) computed-endpoint ranges (Technology, 284k): the parser had no rule for a bare : token — it stopped at the colon and silently returned the partial AST, so SUM(CF14:OFFSET(...))*(X<Y) lost both the dynamic window AND the trailing factor. New Expr::DynRange + runtime _dynRange/_offsetAddr. The same formula exposed the empty-argument bug (OFFSET(x,,n)'s comma was eaten, misaligning later args). parse_formula now refuses partial parses (trailing tokens → parse-error), and parse-error cells emit NaN, not 0, in both emitters — a formula we can't fully parse is now detectably unusable, never a plausible number.
  • Class A — YEARFRAC default basis = US-NASD 30/360 (Lease Am + PP&E, ~91k): was (b−a)/365.25; month-aligned spans are now exact, so MOD(YEARFRAC(...),x)=0 anniversary gates and YEARFRAC*12+1 month counts behave like Excel. Bases 0–4 implemented.
  • Class C — SUMIFS with a range criteria value (Debt array formulas, 6.7k): SUM(SUMIFS(vals, cats, $EK$973:$EK$977)) now yields one sum per criteria element (Excel array semantics).
  • Tokenizer hardening: A1:MAX(...) no longer folds MAX into a column-range endpoint.

Negative control: test-structure-fidelity.mjs runs every class through the real parser → engine; RED on the pre-fix binary with exactly the predicted wrong values (8 failures: truncated 10s, misaligned 0, 365.25 drift values), 10/10 GREEN post-fix. cargo test 29/29, full npm test green. The A-1 rebuild + all-17-sheet warm-GT sweep is running; results will be posted to #66.

🤖 Generated with Claude Code

ebootheee and others added 3 commits June 9, 2026 20:37
…60, array-criteria SUMIFS, no silent partial parses

Three structure-fidelity classes found by the v0.3.0 release-day warm-GT
sweep (383k residual divergent cells on 4 A-1 sheets):

B. `ref:OFFSET(...)` computed-endpoint ranges (Technology, 284k cells).
   The parser had no rule for a bare Colon token: it stopped AT the colon
   and returned the partial AST — SUM(CF14:OFFSET(CF14,,-($F$12-2)))
   became SUM(CF14) and the trailing *(...) factor vanished. New
   Expr::DynRange parsed by a colon-loop over primaries; transpiles to
   _dynRange(ctx, corners) with _offsetAddr() corner math (OFFSET h/w
   extend to the far corner). Unsupported endpoints (INDIRECT/INDEX/...)
   emit an honest NaN sentinel. Same formula exposed the EMPTY-argument
   bug: parse_primary's fallback ATE the comma in OFFSET(x,,n) and
   misaligned every later argument — list terminators now return an
   empty-arg 0 without consuming. parse_formula now returns None when
   tokens remain (a partial parse can never again pass as a formula);
   parse-error cells emit NaN, not 0, in both emitters. Tokenizer: a
   range endpoint followed by '(' is a function call, never a ref
   (A1:MAX(...) previously folded into a column range).

A. YEARFRAC default basis is US-NASD 30/360, was (b-a)/365.25 (Lease
   Amortization + Owned Asset PP&E, ~91k cells). Month-aligned spans are
   now EXACT (1, 0.5) — the model gates on MOD(YEARFRAC(...),x)=0 and
   counts months as YEARFRAC*12+1. _yearfrac implements bases 0-4
   (NASD Feb/31st rules, actual/actual, A/360, A/365, Euro 30/360).

C. SUMIFS with a RANGE criteria value (Debt array formulas, 6.7k cells):
   SUM(SUMIFS(vals, cats, $EK$973:$EK$977)) now yields one sum per
   criteria element (Excel array semantics); previously the array
   criteria matched nothing -> 0.

test-structure-fidelity.mjs (npm test): all classes through the REAL
parser -> engine, negative-controlled — RED on the pre-fix binary with
exactly the predicted truncation/misalignment/365.25 values (8 failures),
10/10 GREEN post-fix. cargo test 29/29; full npm test green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… textbook NASD

Excel runs the d2=31 rule BEFORE the last-day-of-Feb adjustment (testing
d1 after the 31-rule only) and has NO both-Feb rule, so a Feb-end start
keeps a day-31 end: YEARFRAC(Feb28-2023, May31-2023) = 91/360 where SIA
NASD gives 90/360. Hand-verified against the real A-1 PP&E row 92
(GT 465.9667 = 468 - (91/360)*12 + 1) and Lease Amortization row 608
(GT 43.0333 = (1261/360)*12 + 1 over Feb29-2024 -> Aug31-2027). Two new
discriminating cases in test-structure-fidelity.mjs, RED on textbook
NASD with the predicted 0.25/1 values, 12/12 GREEN after.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… the no-both-Feb hypothesis

The a1-66b sweep residual concentrated on every-12th columns (Feb-to-Feb
anniversary spans): GT holds EXACT integers there (PP&E!BA92 = 457 needs
YEARFRAC(Feb28-2023, Feb29-2024) = 1.0, not 359/360). So Excel applies
the textbook both-Feb d2=30 rule; what differs from SIA NASD is only the
ORDER: the d2=31 rule tests d1 BEFORE the February adjustment (keeping
the 91/360 case verified earlier). Rule order now: both-Feb d2, d1=31,
d2=31-with-d1=30 (pre-Feb), Feb d1 last. All prior hand-verified cases
still hold (91/360, 1261-day chain); new discriminating cases for
both-Feb and leap-to-nonleap Feb pairs, RED pre-fix (359/360, 718/360),
13/13 GREEN after. The earlier G6 expectation had validated the
implementation against its own hypothesis instead of Excel - the real
model ground truth is the only oracle that counts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ebootheee ebootheee merged commit 77b2959 into main Jun 10, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant