Skip to content

fix: preserve column orientation for vertical ranges - #763

Merged
hhimanshu merged 2 commits into
mainfrom
fix/724-vertical-range-spill
Jul 23, 2026
Merged

fix: preserve column orientation for vertical ranges#763
hhimanshu merged 2 commits into
mainfrom
fix/724-vertical-range-spill

Conversation

@hhimanshu

@hhimanshu hhimanshu commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Elementwise operations over a vertical range (e.g. =A1:A3*2 where A1:A3 is a 1-column, 3-row range) now spill down (an Nx1 column) instead of sideways (a 1xN row), matching Google Sheets.

resolve_range now materializes a single-column, multi-row range as a nested Nx1 array (one extra Array level per cell) instead of a flat row. Making that shape change safe required updating every function that flattens a Value::Array argument by hand to fully recurse into nested arrays, not just unwrap one level:

  • AVERAGE, MAX, MIN, and the SUMIF/criterion helper (flatten_to_vec) — the 4 functions caught by pinned conformance fixtures.
  • LARGE, SMALL, MODE, MODE.MULT, the PERCENTILE/PERCENTILE.INC/PERCENTILE.EXC/QUARTILE/QUARTILE.INC/QUARTILE.EXC/PERCENTRANK/PERCENTRANK.INC/PERCENTRANK.EXC family (they share one helper module), RANK, RANK.AVG, RANK.EQ, GEOMEAN, HARMEAN, TRIMMEAN, MAXA, MINA — these had no fixture coverage over a plain vertical range, so they silently regressed to #NUM!/#N/A until fixed here.

No existing conformance fixtures were touched.

How to test

From the core repo root:

cargo test -p truecalc-workbook --test vertical_range_spill_tests

Or manually, in a workbook: set A1:A3 to 1, 2, 3, then in B1 enter =A1:A3*2 — it should spill down into B1:B3 as 2, 4, 6 (not sideways into B1:D1). Then try =SMALL(A1:A3,1), =GEOMEAN(A1:A3), =RANK(A1,A1:A3), =MAXA(A1:A3) over the same vertical range — each should return its real value instead of #NUM!/#N/A.

Review

Independent review: 1 finding fixed. The review flagged that the initial fix only patched the 4 functions caught by pinned fixtures (AVERAGE, MAX, MIN, criterion::flatten_to_vec) and left ~17 other statistical functions with a one-level-only array unwrap, which the reviewer confirmed regressed (SMALL, GEOMEAN, RANK, MAXA all returned #NUM!/#N/A over a plain vertical range on the pre-fix branch). All of those functions' local flatten helpers now recurse into nested arrays, and regression tests were added pinning the previously-broken cases.

Test plan

  • cargo build --workspace
  • cargo clippy --workspace -- -D warnings — clean
  • cargo test --workspace — 3613 passed
  • cargo test -p truecalc-workbook --test vertical_range_spill_tests — 2 passed (new regression coverage for SMALL/LARGE/GEOMEAN/RANK/MAXA/MINA/MODE/PERCENTRANK over a vertical range)

closes #724


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.

resolve_range materialized every range as a fully flat, row-major
Value::Array, so a vertical (single-column, multi-row) range lost its
orientation before core's evaluator ever saw it. An elementwise op like
=A1:A3*2 then spilled as a 1xN row instead of the Nx1 column Google
Sheets produces.

resolve_range now materializes a vertical range as core's standard
Nx1 nested-array shape (a row per element, one column each) instead of
a flat array — but only for that shape; single-row and 2-D ranges are
unchanged.

Nesting a range this way exposed that AVERAGE, MAX, MIN, and the
SUMIF/COUNTIF-family criterion flattener only unwrapped one level of
array nesting, so a vertical range's aggregation broke (AVERAGE ->
#DIV/0!, MAX -> #REF!, SUMIF -> 0). Made those four fully recursive so
they walk arbitrarily nested arrays, matching how SUM/COUNT/COUNTA and
the financial-function flatteners already behave.

closes #724
The vertical-range nesting fix (previous commit) only patched AVERAGE,
MAX, MIN, and criterion::flatten_to_vec — the functions caught by
pinned fixtures. Every other statistical function that flattens its
array argument by hand (LARGE, SMALL, MODE, MODE.MULT, the PERCENTILE/
QUARTILE/PERCENTRANK family via their shared helper, RANK/RANK.AVG/
RANK.EQ, GEOMEAN, HARMEAN, TRIMMEAN, MAXA, MINA) only unwrapped one
level of Value::Array, so a vertical range's new Nx1 nested shape
silently vanished into "no recognized values" and these returned
#NUM!/#N/A instead of the real answer.

Make each of those local flatten helpers recurse into nested arrays,
matching the recursive pattern already used by AVERAGE/MAX/MIN.
Behavior for non-nested inputs (scalars, flat arrays, array literals)
is unchanged.

Extends the vertical_range_spill_tests.rs regression coverage to pin
SMALL/LARGE/GEOMEAN/RANK/MAXA/MINA/MODE/PERCENTRANK over a vertical
range.
@hhimanshu hhimanshu self-assigned this Jul 22, 2026
@github-actions

github-actions Bot commented Jul 22, 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
Statistical 483 3,156/3,156 ✓ 5,000 (10×500) 8,639
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,835 10,521/10,527 66,000 (132×500) ~79,362

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

@hhimanshu
hhimanshu merged commit 04be3c6 into main Jul 23, 2026
8 checks passed
@hhimanshu
hhimanshu deleted the fix/724-vertical-range-spill branch July 23, 2026 00:39
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 23, 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.

Elementwise ops over a vertical range return a 1xN row instead of an Nx1 column (range half of #707)

1 participant