1use super::sort::sort_key;
2use super::{
3 AutoFilter, CellRef, EngineError, FilterCriterion, Json, Ordering, RangeRef, Result, Session, Sheet, Value, Workbook, bad, bool_param, col_param,
4 current_region, data_target, edit, json,
5};
6
7pub(super) fn toggle_filter(s: &mut Session, p: &Json) -> Result<Json> {
10 let (sheet, r) = data_target(s, p, "data.filter")?;
11 crate::engine::cmd::refuse_unless_allowed(s, sheet, "Filtering", |p| p.autofilter)?;
12 let at = if sheet == s.doc()?.wb.active_sheet() { s.doc()?.selection.active } else { r.start };
14 let has = s.doc()?.wb.sheet(sheet).is_some_and(|sh| sh.autofilter.is_some());
15 let in_table = s.doc()?.wb.sheet(sheet).and_then(|sh| sh.table_at(at).map(|t| t.name.clone()));
16 if let Some(name) = in_table {
17 return edit(s, |cx| {
18 let sh = cx.sheet_mut(sheet)?;
19 if let Some(t) = sh.tables.iter_mut().find(|t| t.name == name) {
20 t.filter_button = !t.filter_button;
21 if !t.filter_button {
22 t.filter.clear();
23 }
24 }
25 Ok(Json::Null)
26 });
27 }
28 if has {
29 return edit(s, |cx| {
30 let sh = cx.sheet_mut(sheet)?;
31 let filtered: Vec<u32> = sh.rows.keys().copied().filter(|r| sh.is_row_filtered(*r)).collect();
33 sh.autofilter = None;
34 for r in filtered {
35 if let Some(info) = sh.rows.get_mut(&r) {
36 info.hidden = false;
37 }
38 }
39 cx.structural = true;
40 Ok(json!({"filter": false}))
41 });
42 }
43 install_filter(s, p)
44}
45
46fn install_filter(s: &mut Session, p: &Json) -> Result<Json> {
48 let (sheet, mut r) = data_target(s, p, "data.filter")?;
49 if p.get("range").is_none() && r.height() == 1 {
51 let d = s.doc()?;
52 let sh = d.wb.active().ok_or(EngineError::NoDocument)?;
53 let below = current_region(sh, r.start).end.row;
54 if below > r.end.row {
55 r = RangeRef::new(r.start, CellRef::new(below, r.end.col));
56 }
57 }
58 edit(s, |cx| {
59 cx.sheet_mut(sheet)?.autofilter = Some(AutoFilter { range: r, criteria: vec![] });
60 Ok(json!({"filter": true, "range": r.a1()}))
61 })
62}
63
64pub(super) fn filter_by(s: &mut Session, p: &Json) -> Result<Json> {
65 let sheet = s.doc()?.wb.active_sheet();
66 crate::engine::cmd::refuse_unless_allowed(s, sheet, "Filtering", |p| p.autofilter)?;
67 let Some(sh) = s.doc()?.wb.sheet(sheet) else { return Err(EngineError::NoDocument) };
68 let af = match &sh.autofilter {
69 Some(a) => a.clone(),
70 None => {
71 install_filter(s, &json!({}))?;
72 s.doc()?.wb.sheet(sheet).and_then(|sh| sh.autofilter.clone()).ok_or_else(|| EngineError::Other("no filter".into()))?
73 }
74 };
75 let col = match col_param(p.get("column"), af.range.start.col) {
76 Some(c) => c,
77 None if p.get("column").is_some_and(|v| v.as_u64().is_some()) => return Err(bad("data.filterBy", "column outside the filter range")),
79 None => return Err(bad("data.filterBy", "missing `column`")),
80 };
81 if col < af.range.start.col || col > af.range.end.col {
82 return Err(bad("data.filterBy", "column outside the filter range"));
83 }
84 let off = col - af.range.start.col;
85 let crit = if bool_param(p, "clear").unwrap_or(false) {
86 None
87 } else if let Some(vals) = p.get("values").and_then(Json::as_array) {
88 Some(FilterCriterion::Values {
89 values: vals.iter().map(crate::engine::cmd::edit::json_to_input).collect(),
90 blanks: bool_param(p, "blanks").unwrap_or(false),
91 })
92 } else if let Some(c) = p.get("custom") {
93 let a = (
94 c.get("op").and_then(Json::as_str).unwrap_or("=").to_string(),
95 c.get("value").map(crate::engine::cmd::edit::json_to_input).unwrap_or_default(),
96 );
97 let b = c
98 .get("op2")
99 .and_then(Json::as_str)
100 .map(|o| (o.to_string(), c.get("value2").map(crate::engine::cmd::edit::json_to_input).unwrap_or_default()));
101 Some(FilterCriterion::Custom { a, b, and: c.get("and").and_then(Json::as_bool).unwrap_or(true) })
102 } else if let Some(t) = p.get("top") {
103 Some(FilterCriterion::Top10 {
104 bottom: t.get("bottom").and_then(Json::as_bool).unwrap_or(false),
105 percent: t.get("percent").and_then(Json::as_bool).unwrap_or(false),
106 count: t.get("count").and_then(Json::as_u64).unwrap_or(10) as u32,
107 })
108 } else {
109 bool_param(p, "aboveAverage").map(FilterCriterion::AboveAverage)
110 };
111 edit(s, |cx| {
112 let sh = cx.sheet_mut(sheet)?;
113 if let Some(a) = sh.autofilter.as_mut() {
114 a.criteria.retain(|(o, _)| *o != off);
115 if let Some(c) = crit.clone() {
116 a.criteria.push((off, c));
117 }
118 }
119 apply_filter(&mut cx.wb, sheet);
120 cx.structural = true;
121 Ok(Json::Null)
122 })?;
123 let hidden = s.doc()?.wb.sheet(sheet).map(|sh| sh.rows.values().filter(|r| r.hidden).count()).unwrap_or(0);
124 Ok(json!({"hiddenRows": hidden}))
125}
126
127fn custom_ok(v: &Value, text: &str, op: &str, crit: &str) -> bool {
129 let num = crate::base::parse::parse_number_text(crit);
130 let cmp = match (v.as_f64(), num) {
131 (Some(a), Some(b)) => a.partial_cmp(&b),
132 (None, Some(_)) if matches!(op, "<" | "<=" | ">" | ">=") => None,
134 _ => Some(crate::base::compare_text(text, crit)),
135 };
136 let wild = || crate::engine::cmd::edit::wildcard(&text.to_lowercase(), &crit.to_lowercase());
137 match op {
138 "=" => {
139 if crit.contains(['*', '?']) {
140 wild()
141 } else {
142 cmp == Some(Ordering::Equal)
143 }
144 }
145 "<>" => {
146 if crit.contains(['*', '?']) {
147 !wild()
148 } else {
149 cmp != Some(Ordering::Equal)
150 }
151 }
152 ">" => cmp == Some(Ordering::Greater),
153 "<" => cmp == Some(Ordering::Less),
154 ">=" => matches!(cmp, Some(Ordering::Greater | Ordering::Equal)),
155 "<=" => matches!(cmp, Some(Ordering::Less | Ordering::Equal)),
156 "beginsWith" => text.to_lowercase().starts_with(&crit.to_lowercase()),
157 "endsWith" => text.to_lowercase().ends_with(&crit.to_lowercase()),
158 "contains" => text.to_lowercase().contains(&crit.to_lowercase()),
159 "notContains" => !text.to_lowercase().contains(&crit.to_lowercase()),
160 _ => true,
161 }
162}
163
164fn filter_blank(v: &Value) -> bool {
166 v.is_empty() || matches!(v, Value::Text(t) if t.is_empty())
167}
168
169pub(crate) fn grow_autofilter(sh: &mut Sheet, at: CellRef) {
173 if !sh.cells.get(at).is_some_and(|c| !filter_blank(&c.value) || c.formula.is_some()) {
174 return;
175 }
176 let Some(af) = sh.autofilter.as_ref() else { return };
177 let r = af.range;
178 if r.end.row.checked_add(1) != Some(at.row) || at.col < r.start.col || at.col > r.end.col || sh.tables.iter().any(|t| t.range == r) {
179 return;
180 }
181 let grown = RangeRef::new(r.start, CellRef::new(at.row, r.end.col));
182 if let Some(af) = sh.autofilter.as_mut() {
183 af.range = grown;
184 }
185}
186
187pub(crate) fn apply_filter(wb: &mut Workbook, sheet: usize) {
189 let Some(sh) = wb.sheet(sheet) else { return };
190 let Some(af) = sh.autofilter.clone() else { return };
191 let r = af.range;
192 let mut hide = Vec::new();
193 let mut col_stats: std::collections::HashMap<usize, (Option<f64>, f64)> = std::collections::HashMap::new();
195 for (i, (off, c)) in af.criteria.iter().enumerate() {
196 if matches!(c, FilterCriterion::Top10 { .. } | FilterCriterion::AboveAverage(_)) {
197 let col = r.start.col + off;
198 let mut nums: Vec<f64> = (r.start.row + 1..=r.end.row).filter_map(|row| sh.value(CellRef::new(row, col)).as_f64()).collect();
199 let avg = if nums.is_empty() { 0.0 } else { nums.iter().sum::<f64>() / nums.len() as f64 };
200 let thr = match c {
201 FilterCriterion::Top10 { bottom, percent, count } => {
202 nums.sort_by(|a, b| if *bottom { a.partial_cmp(b) } else { b.partial_cmp(a) }.unwrap_or(Ordering::Equal));
203 let k = if *percent { ((nums.len() as f64) * (*count as f64) / 100.0).ceil() as usize } else { *count as usize };
204 let k = k.clamp(1, nums.len().max(1));
205 nums.get(k - 1).copied()
206 }
207 _ => None,
208 };
209 col_stats.insert(i, (thr, avg));
210 }
211 }
212 for row in r.start.row + 1..=r.end.row {
213 let mut visible = true;
214 for (i, (off, crit)) in af.criteria.iter().enumerate() {
215 let c = CellRef::new(row, r.start.col + off);
216 let v = sh.value(c);
217 let text = crate::engine::display::cell_text(wb, sh, c);
218 let ok = match crit {
219 FilterCriterion::Values { values, blanks } => {
220 if filter_blank(&v) {
221 *blanks
222 } else {
223 values.iter().any(|x| x.eq_ignore_ascii_case(&text) || x.eq_ignore_ascii_case(&v.display()))
224 }
225 }
226 FilterCriterion::Custom { a, b, and } => {
227 let x = custom_ok(&v, &text, &a.0, &a.1);
228 match b {
229 Some(b) => {
230 let y = custom_ok(&v, &text, &b.0, &b.1);
231 if *and { x && y } else { x || y }
232 }
233 None => x,
234 }
235 }
236 FilterCriterion::Top10 { bottom, .. } => match (v.as_f64(), col_stats.get(&i)) {
237 (Some(n), Some((thr, _))) => {
238 let thr = thr.unwrap_or(n);
239 if *bottom { n <= thr } else { n >= thr }
240 }
241 _ => false,
242 },
243 FilterCriterion::AboveAverage(above) => match (v.as_f64(), col_stats.get(&i)) {
244 (Some(n), Some((_, avg))) => {
245 if *above {
246 n > *avg
247 } else {
248 n < *avg
249 }
250 }
251 _ => false,
252 },
253 FilterCriterion::FillColor(col) => wb.styles.get(sh.style_id(c)).fill.fg == *col,
254 FilterCriterion::FontColor(col) => wb.styles.get(sh.style_id(c)).font.color == *col,
255 FilterCriterion::Icon { .. } => !sh.is_row_hidden(row),
257 };
258 if !ok {
259 visible = false;
260 break;
261 }
262 }
263 hide.push((row, !visible));
264 }
265 let Some(shm) = wb.sheet_mut(sheet) else { return };
266 for (row, h) in hide {
267 if h {
268 shm.rows.entry(row).or_default().hidden = true;
269 } else if let Some(e) = shm.rows.get_mut(&row) {
270 e.hidden = false;
271 }
272 }
273}
274
275pub(super) fn clear_filter(s: &mut Session, _: &Json) -> Result<Json> {
276 let sheet = s.doc()?.wb.active_sheet();
277 crate::engine::cmd::refuse_unless_allowed(s, sheet, "Filtering", |p| p.autofilter)?;
278 edit(s, |cx| {
279 let sh = cx.sheet_mut(sheet)?;
280 if let Some(a) = sh.autofilter.as_mut() {
281 a.criteria.clear();
282 }
283 apply_filter(&mut cx.wb, sheet);
284 cx.structural = true;
285 Ok(Json::Null)
286 })
287}
288
289pub(super) fn reapply(s: &mut Session, _: &Json) -> Result<Json> {
290 let sheet = s.doc()?.wb.active_sheet();
291 crate::engine::cmd::refuse_unless_allowed(s, sheet, "Filtering", |p| p.autofilter)?;
292 edit(s, |cx| {
293 apply_filter(&mut cx.wb, sheet);
294 cx.structural = true;
295 Ok(Json::Null)
296 })
297}
298
299pub fn filter_values(wb: &Workbook, sheet: usize, col: u32) -> Vec<(String, bool)> {
301 let Some(sh) = wb.sheet(sheet) else { return vec![] };
302 let Some(af) = &sh.autofilter else { return vec![] };
303 let off = col.saturating_sub(af.range.start.col);
304 let selected: Option<&FilterCriterion> = af.criteria.iter().find(|(o, _)| *o == off).map(|(_, c)| c);
305 let mut seen = std::collections::BTreeMap::new();
306 for row in af.range.start.row + 1..=af.range.end.row.min(af.range.start.row + 1_000_000) {
307 let c = CellRef::new(row, col);
308 let v = sh.value(c);
309 let blank = filter_blank(&v);
310 let t = if blank { "(Blanks)".to_string() } else { crate::engine::display::cell_text(wb, sh, c) };
311 let on = match selected {
312 Some(FilterCriterion::Values { values, blanks }) => {
313 if blank {
314 *blanks
315 } else {
316 values.iter().any(|x| x.eq_ignore_ascii_case(&t))
317 }
318 }
319 _ => true,
320 };
321 let key = if blank { sort_key(&Value::Empty) } else { sort_key(&v) };
322 seen.entry((key, t)).or_insert(on);
323 }
324 seen.into_iter().map(|((_, t), on)| (t, on)).collect()
325}