From 7bea3d1d6a939432a9d684cd0baa60f30c6396b5 Mon Sep 17 00:00:00 2001 From: Hector Villarreal Date: Tue, 3 Nov 2026 00:06:14 -0600 Subject: [PATCH] #1448: PivotTable ranks are dense; Count Numbers is captioned "Count of" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Show Values As › Rank Largest to Smallest counted every larger sibling, so ties skipped ranks: 5, 4, 3, 3, 4 ranked 1, 2, 4, 4, 2 where Excel gives 1, 2, 3, 3, 2. It now counts the distinct larger values. A new Count Numbers value field was captioned "Count Numbers of Units"; Excel captions it "Count of Units", as GridCraft's XLSX reader already names a value field saved without a name. Co-Authored-By: Claude Opus 5.5 --- crates/engine/src/cmd/pivot/tests.rs | 16 ++++++++++++++++ crates/engine/src/pivot/compute.rs | 22 +++++++++++++--------- crates/engine/src/pivot/items.rs | 5 +++-- 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/crates/engine/src/cmd/pivot/tests.rs b/crates/engine/src/cmd/pivot/tests.rs index 4e4f6870c..1733a2664 100644 --- a/crates/engine/src/cmd/pivot/tests.rs +++ b/crates/engine/src/cmd/pivot/tests.rs @@ -1013,3 +1013,19 @@ fn detail_cells_are_the_cells_show_details_takes() { let pt = &d.wb.active().unwrap().pivots[0]; assert!(crate::pivot::detail_cells(&d.wb, pt, d.wb.active_sheet()).unwrap_err().contains("out of date")); } + +#[test] +fn rank_ties_are_dense_and_count_numbers_is_captioned_count_of() { + // #1448 items 10 and 12: Excel ranks 5, 4, 3, 3, 4 as 1, 2, 3, 3, 2 (GridCraft gave 1, 2, 4, + // 4, 2), and captions a Count Numbers field "Count of Val". + let mut s = Session::new(); + s.new_workbook(); + let rows = json!([["Key", "Val"], ["A", 5], ["B", 4], ["C", 3], ["D", 3], ["E", 4]]); + s.execute("range.setValues", json!({"range": "A1", "values": rows})).unwrap(); + s.execute("insert.pivotTable", json!({"source": "Sheet1!$A$1:$B$6", "destination": "Sheet1!D3", "rows": ["Key"], "values": ["Val"]})).unwrap(); + s.execute("pivot.valueSettings", json!({"index": 0, "showAs": "rank"})).unwrap(); + let ranks: Vec = ["E4", "E5", "E6", "E7", "E8"].iter().map(|a| val(&s, a)).collect(); + assert_eq!(ranks, [1.0, 2.0, 3.0, 3.0, 2.0].map(Value::Number)); + s.execute("pivot.valueSettings", json!({"index": 0, "showAs": "normal", "func": "countNumbers"})).unwrap(); + assert_eq!(text(&s, "E3"), "Count of Val"); +} diff --git a/crates/engine/src/pivot/compute.rs b/crates/engine/src/pivot/compute.rs index 9ebf44737..0ffa8cae5 100644 --- a/crates/engine/src/pivot/compute.rs +++ b/crates/engine/src/pivot/compute.rs @@ -355,15 +355,19 @@ fn ranked_value(raw: Value, lookup: &ValueLookup<'_>, context: &ShowContext<'_>) let Some((_, parent)) = context.row_path.split_last() else { return Value::Empty }; let Some(me) = raw.as_f64() else { return raw }; let siblings = context.siblings.get(parent).cloned().unwrap_or_default(); - let mut rank = 1u64; - for sibling in siblings { - let mut path = parent.to_vec(); - path.push(sibling); - if number(&lookup.get(&path, context.col_path, context.value_index)).is_some_and(|value| value > me) { - rank += 1; - } - } - Value::number(rank as f64) + // Excel's ranks are dense: ties share a rank and the next value takes the next one + // (5, 4, 3, 3, 4 rank 1, 2, 3, 3, 2), so count the distinct larger values. + let mut larger: Vec = siblings + .into_iter() + .filter_map(|sibling| { + let mut path = parent.to_vec(); + path.push(sibling); + number(&lookup.get(&path, context.col_path, context.value_index)).filter(|value| *value > me) + }) + .collect(); + larger.sort_by(f64::total_cmp); + larger.dedup(); + Value::number(larger.len() as f64 + 1.0) } fn show_value(definition: &PivotValue, raw: Value, lookup: &ValueLookup<'_>, context: &ShowContext<'_>) -> Value { diff --git a/crates/engine/src/pivot/items.rs b/crates/engine/src/pivot/items.rs index 1a39c59d9..79b74da1a 100644 --- a/crates/engine/src/pivot/items.rs +++ b/crates/engine/src/pivot/items.rs @@ -197,7 +197,8 @@ impl Acc { } } -/// Excel's caption verb for a function ("Sum of Sales"). +/// Excel's caption verb for a function ("Sum of Sales"). Count Numbers is captioned "Count of +/// Sales", as Excel names it and as the XLSX reader names a value field saved without a name. pub fn func_caption(f: PivotFunc) -> &'static str { match f { PivotFunc::Sum => "Sum", @@ -206,7 +207,7 @@ pub fn func_caption(f: PivotFunc) -> &'static str { PivotFunc::Max => "Max", PivotFunc::Min => "Min", PivotFunc::Product => "Product", - PivotFunc::CountNumbers => "Count Numbers", + PivotFunc::CountNumbers => "Count", PivotFunc::StdDev => "StdDev", PivotFunc::StdDevP => "StdDevp", PivotFunc::Var => "Var",