Skip to content

fix(statistical): MAX/MIN/MAXA/MINA compare dates instead of ignoring them - #783

Merged
hhimanshu merged 2 commits into
mainfrom
fix/776-dates-in-aggregates
Jul 28, 2026
Merged

fix(statistical): MAX/MIN/MAXA/MINA compare dates instead of ignoring them#783
hhimanshu merged 2 commits into
mainfrom
fix/776-dates-in-aggregates

Conversation

@hhimanshu

@hhimanshu hhimanshu commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

closes #776

Summary

All four functions folded only Value::Number, so dates fell through entirely:

=MIN(<a date column>)    truecalc: 0        Sheets: the earliest date
=MAX(<a date column>)    truecalc: #REF!    Sheets: the latest date

=MIN(A1:A10) over a date column returning a silent 0 is the exact hazard #771 was filed about — a believable wrong answer rather than a visible error. It has been there the whole time.

What Sheets actually does

Captured, all four functions agreeing:

=MAX(Data!N1:N3)   44362      date-only column, latest serial
=MIN(Data!N1:N3)   43526      earliest
=MIN(Data!P1:P3)   5          mixed: a plain number beats every date
=Data!Q1           44362 date  ← typing, read back through a real cell
=Data!Q2           5     date  ← still date-typed even when a number wins

No special casing — dates compare as bare serials, so a plain 5 beats every date for MIN. But the result is date-typed on both the date-only and the mixed range, including where the number won and would render as 1900-01-04.

⚠️ Capturing the typing needed a workaround, and one control had to fail

The GAS harness wraps every formula as INDEX((<formula>),1,1), which strips the automatic date format. A control asserting =DATE(2020,1,1) — which must be a date in Sheets — came back typed number.

So no direct probe row can answer "date or bare serial". It would have silently reported serial for everything, and this fix would have been built on it. The date cases are therefore evaluated into real cells (Data!Q1, Data!Q2) and read back through a reference, which preserves the format.

Consequence, stated in all four doc blocks: values are captured for every form; typing is captured only for the range forms. Literal-form typing is extrapolated and labelled as such, in the comments and in both integration tests.

⚠️ npm surface change

crates/wasm/src/lib.rs maps Value::Date to EvalResult::Date, so =MAX(dateColumn) goes from {type:"number"} to {type:"date"}. value encoding unchanged. Noted in crates/wasm/README.md beside the existing surface-change note, so a consumer sees it where they'd look.

The rebase onto #775 found four test failures a clean merge had hidden

This branch conflicts with #775 by design. The fold bodies auto-merged; the conflicts were all in doc comments. But four tests then failed — and the test files auto-merged without conflicting at all.

#775 pinned an invariant using a Date companion, with the note "if that ever moves it has to move deliberately, not as fallout from this rule." It moved. And there is no drop-in replacement: after this change, no variant reaches MAX's numberless #REF! through a populated array — text and booleans set array_had_content, dates now contribute a number, sparklines set their own flag, and a Zoned is intercepted before the loop. The assertion was retargeted to the date, renamed, and the reason recorded. The invariant is still pinned and still discriminating: the answer is the date, not the blank-only 0.

Fixes went in via --fixup/--autosquash, so commit 1 is green standalone (2922 lib tests at HEAD~1) — no bisect hazard.

Verified after the rebase, not assumed

The compile guard survived — all 8 sites. Re-added a probe variant to Value; every function still fails to compile at both its top-level loop and its recursive fold:

max/mod.rs:81, :193     maxa/mod.rs:56, :130
min/mod.rs:78, :143     mina/mod.rs:57, :131

count/mod.rs is still not flagged — #780's catch-all remains live, as filed.

#775's behaviour intact. stat_helpers.rs byte-identical to main; all four still return 0 for blank-only arrays across three shapes.

A 327-row differential captured before the rebase and re-run after: the diff is exactly 9 rows, all of them #775's blank-only corrections. Every date row, every typing probe, and every neighbour row (COUNT, SUM, AVERAGE) byte-identical.

Review

Two follow-ups this exposed

⚠️ The captured rows are not in this repo yet

They come off the conformance-fixtures pipeline in a separate fixtures-only PR, for two forced reasons: they fail until this code exists, and CI rejects any PR touching both fixture TSVs and code. A reviewer working from this repo alone cannot verify the Sheets answers.

How to test

cargo test -p truecalc-core statistical::max
cargo test --workspace

Directly — over a column of dates, =MIN returns the earliest and =MAX the latest, both date-typed. Before: 0 and #REF!.

Test plan

  • cargo test --workspace — 3775 passed, 89 suites
  • cargo clippy --workspace -- -D warnings — clean
  • cargo nextest run --workspace --profile ci — conformance 10521/10527, same 6 pre-existing failures
  • Compile guard re-verified at all 8 sites after the rebase
  • Commit 1 green standalone
  • No fixture TSV, no value_completeness.rs
  • CI green

Related

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

MAX and MIN folded only `Value::Number`, so a `Value::Date` element was
skipped entirely. A date-only array reached the end with no result:
`=MAX({DATE(2020,1,1),DATE(2021,1,1)})` was #REF! and `=MIN(...)` a
silent 0 that renders as a date in 1899. MAXA/MINA answered #N/A on the
same input — three different answers across four functions. The
everyday form of this is `=MAX(A1:A10)` over a column of dates.

Dates now fold in as their bare serial in every position (direct
argument, array element, nested/range-materialized array), so all four
functions agree: MAX/MAXA take the largest serial, MIN/MINA the
smallest, with no special casing — a small plain number beats every date
for MIN and MINA on a mixed range. The result is date-typed whenever a
date took part, including when a plain number won the comparison.

Captured in Google Sheets over a date-only column, a date/number column,
and array literals of both shapes; the typing was read back through the
cell holding the result. The fixture rows land separately.

Two things deliberately left alone, both unprobed and both unchanged by
this commit: an all-blank range (MAX #REF!, MIN 0, MAXA/MINA #N/A), and
a zone-aware value beside a date in MAXA/MINA, which do not consult the
zoned path MAX/MIN use.

The remaining `_ =>` catch-alls in the four folds are replaced by
explicit variant arms, so a `Value` variant added later is a compile
error at each fold rather than a silent skip.

closes #776
Review follow-up on the MAX/MIN/MAXA/MINA date change. The behaviour is
unchanged — this corrects what the comments claim about the evidence
behind it, and refreshes two summaries the change had made stale.

The earlier wording said the typing was captured "over a date-only
column, a date/number column, and array literals of both shapes". That
overstates the read-back. The capture types the result only through the
cell holding it for the two *range* forms; the array-literal and
direct-argument rows come back reporting `number` purely because the
harness reads them through an `INDEX(...,1,1)` wrapper that drops the
cell's date format. Values are captured for every form, typing for the
range forms alone, and the literal-form typing is extrapolated. All four
doc blocks now say so, and the two integration tests that assert
`ISDATE(MAX({DATE(...),DATE(...)}))` carry the same marker instead of
reading as probed.

Stale one-liners fixed: MAX/MIN's header said "Array elements: Numbers
only", and MAXA/MINA's in-array comment omitted dates, both contradicting
the paragraphs beneath them.

Two known consequences are now cross-referenced from all four doc blocks
rather than living only in a hand-off note:

- COUNT does not count dates, so `=COUNT(MAX(<date range>))` flips 1 to
  0. Pre-existing in COUNT — its fold still uses a `_ => {}` catch-all,
  which a probe variant added to `Value` confirms is not flagged on
  either side of this change — and this change makes it reachable (#780).
- MAXA/MINA silently drop a Zoned beside a Date while MAX/MIN error on
  the same input, because MAXA/MINA never consult `zoned_extreme`.
  Unprobed in both directions (#781).

The npm surface change is stated where a consumer sees it: the
@truecalc/core README now notes that MAX/MIN/MAXA/MINA return
`{type:'date'}` where they returned `{type:'number'}`, with the same
serial-number `value` encoding.

refs #776
@hhimanshu hhimanshu self-assigned this Jul 28, 2026
@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Test Coverage by Category

Category Unit Tests Google Sheets Conformance Property Cases Total
Array 42 547/547 ✓ 1,000 (2×500) 1,589
Database 35 182/182 ✓ 3,500 (7×500) 3,717
Date 369 418/418 ✓ 2,500 (5×500) 3,287
Engineering 245 886/888 ⚠ 5,500 (11×500) 6,633
Filter 11 80/80 ✓ 4,500 (9×500) 4,591
Financial 149 1,208/1,208 ✓ 2,000 (4×500) 3,357
Info 0 256/256 ✓ 4,500 (9×500) 4,756
Logical 121 263/263 ✓ 3,500 (7×500) 3,884
Lookup 69 392/392 ✓ 1,000 (2×500) 1,461
Math 536 2,006/2,006 ✓ 8,000 (16×500) 10,542
Operator 87 250/250 ✓ 7,500 (15×500) 7,837
Parser 83 92/92 ✓ 4,000 (8×500) 4,175
Query 37 37
Statistical 508 3,156/3,156 ✓ 5,000 (10×500) 8,664
Text 298 729/733 ⚠ 4,000 (8×500) 5,031
Timezone 47 47
Volatile 0 3,500 (7×500) 3,500
Web 29 56/56 ✓ 6,000 (12×500) 6,085
Total 2,922 10,521/10,527 66,000 (132×500) ~79,449

✓ = 100% passing · ⚠ = known deviation · The ~79,449 total counts formula evaluations (each conformance row and each property case = 1). GitHub Checks reports 3,768 Rust test functions: 2,922 unit + 159 property functions (shown as cases above) + 687 conformance/integration.

@hhimanshu
hhimanshu merged commit 8315bc7 into main Jul 28, 2026
9 checks passed
@hhimanshu
hhimanshu deleted the fix/776-dates-in-aggregates branch July 28, 2026 23:42
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 28, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(statistical): MAX/MIN ignore dates inside arrays — a date column returns #REF! (MAX) or 0 (MIN) instead of the extreme date

1 participant