fix(statistical): an all-blank array is 0, not three different answers - #782
Merged
Conversation
MAX, MIN, MAXA and MINA gave three different answers for an array whose
every element is blank — the shape a range over empty cells materialises
as, so `=MAX(A1:A3)` on an untouched column is the everyday form:
MAX #REF!
MIN 0
MAXA #N/A
MINA #N/A
A range-based probe with controls settles it: Google Sheets answers the
number 0 for all four. The probe carried a boring-value control in both
directions — `=MAX(Data!M4)` with M4 holding 1 returns 1, and
`=MAX(Data!M1:M4)` over a range whose only non-blank cell holds 1 also
returns 1 — so a silent harness failure could not masquerade as a
result. The same controls hold for MIN, MAXA and MINA.
MIN was already there. MAX, MAXA and MINA are brought to it by one
shared predicate, `stat_helpers::is_blank_only_array`, which is true only
when every argument is blank and at least one arrived as an array.
Deliberately narrow, in two directions:
- The decision is over the arguments as a whole, not a per-element
"had content" flag. A blank next to a date still answers #REF! from
MAX rather than a believable 0; date-only arrays are a separate,
still-unprobed defect and are untouched here.
- Requiring an array confines the rule to the range form. A bare blank
argument (`=MAXA(A1)` on an empty cell) is unprobed and keeps its
#N/A.
The predicate matches every `Value` variant explicitly instead of using
a catch-all, so a variant added later is a compile error rather than a
silent 0.
Differential against main: a probe compiled on both revisions ran 7,931
distinct input shapes — numeric, text-only, numeric text, empty string,
boolean-only, mixed, blanks, dates, zoned instants, sparklines, errors
leading and trailing, scalars, multi-argument, empty arrays, flat and
nested and 2-D arrays, and range-delivered forms through a seeded
resolver — for 31,724 evaluations per revision. 72 evaluations changed,
every one of them an all-blank input carrying an array; 31,628
evaluations outside that class were byte-identical, and no MIN
evaluation changed anywhere.
The pinning tests added with the empty-array and numberless-array work
are updated to the captured behaviour, and new tests pin both narrowing
decisions above.
Fixture rows land separately.
closes #775
The blank-only rule is broader than the one range shape originally captured, so the comments now name every shape that was probed: a single cell, a single column, a row across columns, a two-dimensional range, a range far past the used area, and a blank scalar beside a blank range in both orders. Each carried a populated control that returned its value, blankness was asserted with COUNTA/COUNTBLANK, and the answer's type was read back through a real cell as a plain number. The comments also stop implying local backing. Nothing under tests/fixtures/google_sheets/ covers a blank-only array today; the rows sit on the conformance-fixtures pipeline branch feat/stat-range-probe and land in a separate fixtures-only PR, since three of the four fail until this code exists. A reviewer working from this repo alone can now tell what is checkable here and what is not. Comments that asserted where other numberless kinds end up are narrowed to describe only what this change moves. Comments only; no behaviour change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contributor
Test Coverage by Category
✓ = 100% passing · ⚠ = known deviation · The ~79,442 total counts formula evaluations (each conformance row and each property case = 1). GitHub Checks reports 3,759 Rust test functions: 2,915 unit + 159 property functions (shown as cases above) + 685 conformance/integration. |
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 subscribe to this conversation on GitHub.
Already have an account?
Sign in.
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.
closes #775
Summary
Four functions that should agree gave three different answers for an array where every element is blank:
MAX#REF!0MIN0✓0MAXA#N/A0MINA#N/A0MINwas the only one already right. A blank-only array is what a range over empty cells materialises as, so=MAX(A1:A3)on an untouched column is the everyday form of this.Getting the evidence took fixing the probe harness first
An earlier attempt to capture this measured the harness instead of Sheets. With
M4holding1,=MAX(Data!M4)returned#REF!while=COUNTA(Data!M1:M4)returned1— impossible as real behaviour, and only visible because a control asserted a boring known value.Root cause: the evaluator resolves the setup sidecar from the path it actually reads rows from. These functions read from
common/, so the sidecar had to be attest-inputs/common/{FN}.setup.json. It had been placed undergoogle_sheets/statistical/, where it was never read — so noDatasheet was created and every reference correctly returned#REF!.(SPARKLINE's probes worked only by accident: it is a Sheets-only function with no
common/file, so its rows and sidecar happen to share a directory. The two paths coincided by luck, not by rule.)The rule generalises past one shape — and that is now captured, not assumed
The predicate is "every argument blank AND at least one arrived as an array", which is broader than the single shape first captured. At the
Valuelayer the engine cannot see range geometry, so a narrower predicate is not expressible.Rather than ship that as a judgement call, all seven shapes were probed — each with a populated control proving the range actually resolved:
A1:A1single-cell07A1:A3single-column07A1:B1multi-column07A1:B22-D07A1:A100far past the used area07A1,A1:A3scalar then range07A1:A3,A1reversed07Identical for all four functions. Blankness asserted rather than assumed (
=COUNTA(Data!A1:B100)→0,=COUNTBLANK(Data!A1:A100)→100), and the type read back through a real cell →0number, so it is a plain zero and not a date-formatted one.They live on the conformance-fixtures pipeline branch and land in a separate fixtures-only PR. Two reasons, both forced: three of the four 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 answer. The nearest in-repo rows are
=MAX({})→#REF!and the sparklineData!K1:K1rows; neither covers a blank-only array. The doc onis_blank_only_arraysays this explicitly, and each test comment notes that read from this repo alone it pins the behaviour, not the Sheets answer.Review
Valuevariants with no catch-all — a new variant is a compile error there — it fires strictly after every error path, and costs nothing on the hot path.How to test
Directly —
=MAX(A1:A3),=MIN(A1:A3),=MAXA(A1:A3),=MINA(A1:A3)over three empty cells all return0. Before:#REF!,0,#N/A,#N/A.Test plan
cargo test --workspace— 3766 passed, 89 suitescargo clippy --workspace -- -D warnings— cleancargo nextest run --workspace --profile ci— 3759/3759cargo fmt --checkfails only on files this branch never touched (mainis not fmt-clean; fmt is not a CI gate)Related
COUNTdoes not count datesMAXA/MINAdrop aZonedthatMAX/MINerror on🤖 Generated with Claude Code
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.