mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-29 21:26:48 -06:00
Compute the Top 10 filter threshold once per column, not once per row (#661)
apply_filter cloned and sorted the column's numbers for every row it tested, O(n^2 log n). The threshold is now computed once per criterion with the other column stats. Adds a test for top/bottom items and percent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ut3hr3a13szpQdQ7gbYbfc
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
3926d21aa9
commit
bf9a202222
@@ -570,14 +570,23 @@ pub(crate) fn apply_filter(wb: &mut Workbook, sheet: usize) {
|
||||
let Some(af) = sh.autofilter.clone() else { return };
|
||||
let r = af.range;
|
||||
let mut hide = Vec::new();
|
||||
// Precompute per-column numbers for top10/average.
|
||||
let mut col_stats: std::collections::HashMap<u32, (Vec<f64>, f64)> = std::collections::HashMap::new();
|
||||
// Precompute per-column top10 threshold and average once, not per row.
|
||||
let mut col_stats: std::collections::HashMap<u32, (Option<f64>, f64)> = std::collections::HashMap::new();
|
||||
for (off, c) in &af.criteria {
|
||||
if matches!(c, FilterCriterion::Top10 { .. } | FilterCriterion::AboveAverage(_)) {
|
||||
let col = r.start.col + off;
|
||||
let nums: Vec<f64> = (r.start.row + 1..=r.end.row).filter_map(|row| sh.value(CellRef::new(row, col)).as_f64()).collect();
|
||||
let mut nums: Vec<f64> = (r.start.row + 1..=r.end.row).filter_map(|row| sh.value(CellRef::new(row, col)).as_f64()).collect();
|
||||
let avg = if nums.is_empty() { 0.0 } else { nums.iter().sum::<f64>() / nums.len() as f64 };
|
||||
col_stats.insert(*off, (nums, avg));
|
||||
let thr = match c {
|
||||
FilterCriterion::Top10 { bottom, percent, count } => {
|
||||
nums.sort_by(|a, b| if *bottom { a.partial_cmp(b) } else { b.partial_cmp(a) }.unwrap_or(Ordering::Equal));
|
||||
let k = if *percent { ((nums.len() as f64) * (*count as f64) / 100.0).ceil() as usize } else { *count as usize };
|
||||
let k = k.clamp(1, nums.len().max(1));
|
||||
nums.get(k - 1).copied()
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
col_stats.insert(*off, (thr, avg));
|
||||
}
|
||||
}
|
||||
for row in r.start.row + 1..=r.end.row {
|
||||
@@ -604,13 +613,9 @@ pub(crate) fn apply_filter(wb: &mut Workbook, sheet: usize) {
|
||||
None => x,
|
||||
}
|
||||
}
|
||||
FilterCriterion::Top10 { bottom, percent, count } => match (v.as_f64(), col_stats.get(off)) {
|
||||
(Some(n), Some((nums, _))) => {
|
||||
let mut sorted = nums.clone();
|
||||
sorted.sort_by(|a, b| if *bottom { a.partial_cmp(b) } else { b.partial_cmp(a) }.unwrap_or(Ordering::Equal));
|
||||
let k = if *percent { ((sorted.len() as f64) * (*count as f64) / 100.0).ceil() as usize } else { *count as usize };
|
||||
let k = k.clamp(1, sorted.len().max(1));
|
||||
let thr = sorted.get(k - 1).copied().unwrap_or(n);
|
||||
FilterCriterion::Top10 { bottom, .. } => match (v.as_f64(), col_stats.get(off)) {
|
||||
(Some(n), Some((thr, _))) => {
|
||||
let thr = thr.unwrap_or(n);
|
||||
if *bottom { n <= thr } else { n >= thr }
|
||||
}
|
||||
_ => false,
|
||||
|
||||
@@ -805,6 +805,24 @@ fn sort_and_filter() {
|
||||
assert_eq!(r["hiddenRows"], 1);
|
||||
}
|
||||
|
||||
/// Top 10 keeps the top or bottom N items or percent, ties at the threshold included.
|
||||
#[test]
|
||||
fn filter_top10_items_and_percent() {
|
||||
let mut s = s();
|
||||
s.execute("range.setValues", json!({"range": "A1", "values": [["Score"], [5], [1], [9], [3], [9], [7], ["x"]]})).unwrap();
|
||||
s.execute("selection.set", json!({"cell": "A1"})).unwrap();
|
||||
s.execute("data.filter", json!({})).unwrap();
|
||||
// Top 2 items: 9, 9 (the "x" text row is hidden too).
|
||||
let r = s.execute("data.filterBy", json!({"column": "A", "top": {"count": 2}})).unwrap();
|
||||
assert_eq!(r["hiddenRows"], 5);
|
||||
// Bottom 2 items: 1, 3.
|
||||
let r = s.execute("data.filterBy", json!({"column": "A", "top": {"count": 2, "bottom": true}})).unwrap();
|
||||
assert_eq!(r["hiddenRows"], 5);
|
||||
// Top 50 percent of 6 numbers = 3 items; the threshold 7 keeps 9, 9, 7.
|
||||
let r = s.execute("data.filterBy", json!({"column": "A", "top": {"count": 50, "percent": true}})).unwrap();
|
||||
assert_eq!(r["hiddenRows"], 4);
|
||||
}
|
||||
|
||||
/// Turning the filter on with only the header row selected filters the data below it (#201).
|
||||
#[test]
|
||||
fn filter_on_a_selected_header_row_covers_the_data_below() {
|
||||
|
||||
Reference in New Issue
Block a user