1use std::collections::{HashMap, HashSet};
10
11use crate::base::{CellRef, RangeRef, Value};
12use crate::calc::Calc;
13use crate::model::{CalcMode, Cell, DefinedName, Sheet, StyleId, Workbook};
14use serde::{Deserialize, Serialize};
15use serde_json::{Value as Json, json};
16
17use super::{CommandSpec, DocState, EngineError, Result, Session, bad, bool_param, cmd, edit, f64_param, has_doc, split_sheet, str_param};
18
19mod advanced_filter;
20mod consolidate;
21mod scenarios;
22
23use advanced_filter::advanced_filter;
24#[cfg(test)]
25pub(crate) use advanced_filter::criterion_matches;
26use consolidate::consolidate;
27#[cfg(test)]
28pub(crate) use scenarios::{SCENARIO_NAME, load_scenarios};
29use scenarios::{scenario_add, scenario_delete, scenario_list, scenario_show, scenario_summary};
30
31pub fn specs() -> Vec<CommandSpec> {
32 vec![
33 cmd!(
34 "data.goalSeek",
35 "Goal Seek…",
36 ["Data", "Forecast", "What-If Analysis"],
37 None,
38 "{set: \"B5\" (formula cell), to: number, changing: \"B2\" (constant cell)} → {value, result, iterations, converged}. Secant search with bisection once the answer is bracketed; stops within Maximum Change (0.001) or after Maximum Iterations (100) from File › Options › Formulas. Writes the found value into `changing`.",
39 has_doc,
40 goal_seek
41 ),
42 cmd!(
43 query "data.scenarioManager",
44 "Scenario Manager…",
45 ["Data", "Forecast", "What-If Analysis"],
46 None,
47 "{sheet?} → {scenarios: [{name, sheet, changing, values, comment}]} (scenarios of the active sheet; `sheet: \"*\"` lists all)",
48 has_doc,
49 scenario_list
50 ),
51 cmd!(
52 "data.scenarioAdd",
53 "Add Scenario",
54 [],
55 None,
56 "{name, changing: \"B2:B4\" or \"B2,B4\" (≤32 cells, active sheet), values?: [..] (default: the cells' current values), comment?, replace?: bool}",
57 has_doc,
58 scenario_add
59 ),
60 cmd!("data.scenarioShow", "Show Scenario", [], None, "{name} writes the scenario's values into its changing cells", has_doc, scenario_show),
61 cmd!("data.scenarioDelete", "Delete Scenario", [], None, "{name}", has_doc, scenario_delete),
62 cmd!(
63 "data.scenarioSummary",
64 "Scenario Summary",
65 [],
66 None,
67 "{resultCells?: \"B10\" or \"B10:B12,D4\"} inserts a \"Scenario Summary\" sheet: changing cells and result cells for the current values and each scenario",
68 has_doc,
69 scenario_summary
70 ),
71 cmd!(
72 "data.dataTable",
73 "Data Table…",
74 ["Data", "Forecast", "What-If Analysis"],
75 None,
76 "{range: \"D2:E10\", rowInput?: \"B1\", columnInput?: \"B2\"} one-variable (values down the first column with columnInput, or across the top row with rowInput; formulas in the other edge) or two-variable (formula in the top-left corner). Computes every result by substitution and writes static values (a snapshot, not a live {=TABLE()}); ≤100,000 cells.",
77 has_doc,
78 data_table
79 ),
80 cmd!(
81 "data.advancedFilter",
82 "Advanced…",
83 ["Data", "Sort & Filter"],
84 None,
85 "{range? (list with headers; default current region), criteria?: \"H1:I3\" (omitted/blank matches all rows; header row of field names; rows OR'ed, columns AND'ed; \">100\", \"=abc\", \"a*\", \"<>\"; blank-headed formula cells are computed criteria for the first data row), copyTo?: \"K1\" (else filter in place by hiding rows), unique?: bool}",
86 has_doc,
87 advanced_filter
88 ),
89 cmd!(
90 "data.consolidate",
91 "Consolidate…",
92 ["Data", "Data Tools"],
93 None,
94 "{sources: [\"Sheet2!A1:C10\", ..], destination?: \"A1\" (default active cell), function?: sum|count|countNums|average|max|min|product, topRow?: bool, leftColumn?: bool (consolidate by labels; else by position)}",
95 has_doc,
96 consolidate
97 ),
98 ]
99}
100
101fn sheet_named(wb: &Workbook, name: Option<String>, default: usize, cmd: &str) -> Result<usize> {
105 match name {
106 Some(n) => wb.sheet_index(&n).ok_or_else(|| bad(cmd, format!("no sheet named `{n}`"))),
107 None => Ok(default),
108 }
109}
110
111fn cell_arg(s: &Session, p: &Json, key: &str, cmd: &str) -> Result<(usize, CellRef)> {
113 let d = s.doc()?;
114 let t = str_param(p, key).ok_or_else(|| bad(cmd, format!("missing `{key}`")))?;
115 let (sh, body) = split_sheet(t);
116 let si = sheet_named(&d.wb, sh, d.wb.active_sheet(), cmd)?;
117 let c = CellRef::parse(body.trim()).ok_or_else(|| bad(cmd, format!("`{key}` is not a cell: `{t}`")))?;
118 Ok((si, c))
119}
120
121pub(crate) fn parse_range(wb: &Workbook, t: &str, cmd: &str) -> Result<(usize, RangeRef)> {
124 let (sh, body) = split_sheet(t);
125 let si = sheet_named(wb, sh, wb.active_sheet(), cmd)?;
126 let r = RangeRef::parse(body.trim()).ok_or_else(|| bad(cmd, format!("not a range: `{t}`")))?;
127 Ok((si, trim_to_used(wb, si, r)))
128}
129
130fn trim_to_used(wb: &Workbook, si: usize, r: RangeRef) -> RangeRef {
131 if !(r.is_full_cols() || r.is_full_rows()) {
132 return r;
133 }
134 match wb.sheet(si).and_then(|s| s.used_range()) {
135 Some(u) => r.intersection(&RangeRef::new(CellRef::new(0, 0), u.end)).unwrap_or(RangeRef::cell(r.start)),
136 None => RangeRef::cell(r.start),
137 }
138}
139
140fn range_arg(s: &Session, p: &Json, key: &str, cmd: &str) -> Result<(usize, RangeRef)> {
141 let d = s.doc()?;
142 let t = str_param(p, key).ok_or_else(|| bad(cmd, format!("missing `{key}`")))?;
143 parse_range(&d.wb, t, cmd)
144}
145
146fn cell_list(wb: &Workbook, t: &str, cmd: &str, cap: usize) -> Result<(usize, Vec<CellRef>)> {
148 let mut sheet = None;
149 let mut out: Vec<CellRef> = Vec::new();
150 for part in t.split(',').map(str::trim).filter(|x| !x.is_empty()) {
151 let (si, r) = parse_range(wb, part, cmd)?;
152 if *sheet.get_or_insert(si) != si {
153 return Err(bad(cmd, "all cells must be on one sheet"));
154 }
155 if r.count() > cap as u64 {
156 return Err(bad(cmd, format!("too many cells (at most {cap})")));
157 }
158 for c in r.iter() {
159 if !out.contains(&c) {
160 out.push(c);
161 }
162 }
163 if out.len() > cap {
164 return Err(bad(cmd, format!("too many cells (at most {cap})")));
165 }
166 }
167 Ok((sheet.unwrap_or(wb.active_sheet()), out))
168}
169
170fn json_value(v: &Json, wb: &Workbook) -> Value {
172 match v {
173 Json::Null => Value::Empty,
174 Json::Bool(b) => Value::Bool(*b),
175 Json::Number(n) => n.as_f64().map(Value::number).unwrap_or_default(),
176 Json::String(s) if s.is_empty() => Value::Empty,
177 Json::String(s) => crate::base::parse::parse_input(s, wb.date_system).value,
178 other => Value::text(other.to_string()),
179 }
180}
181
182fn value_json(v: &Value) -> Json {
183 match v {
184 Value::Empty => Json::Null,
185 Value::Number(n) => json!(n),
186 Value::Bool(b) => json!(b),
187 Value::Text(t) => json!(t.as_ref()),
188 Value::Error(e) => json!(e.as_str()),
189 Value::Array(a) => value_json(&a.data.first().cloned().unwrap_or_default()),
190 }
191}
192
193pub(crate) struct Trial {
197 base: Workbook,
198 calc: Calc,
199}
200
201impl Trial {
202 pub(crate) fn new(d: &DocState) -> Trial {
203 let mut base = (*d.wb).clone();
204 base.calc.mode = CalcMode::Automatic;
206 Trial { base, calc: d.calc.clone() }
207 }
208
209 pub(crate) fn run(&mut self, sets: &[(usize, CellRef, Value)], reads: &[(usize, CellRef)]) -> Vec<Value> {
211 let mut wb = self.base.clone();
212 let mut keys = Vec::with_capacity(sets.len());
213 for (si, c, v) in sets {
214 if let Some(sh) = wb.sheet_mut(*si) {
215 let style = sh.style_id(*c);
216 sh.set_cell(*c, Cell { value: v.clone(), formula: None, style, py: None });
217 keys.push((*si, *c));
218 }
219 }
220 self.calc.cells_changed(&mut wb, &keys);
221 reads.iter().map(|(si, c)| wb.sheet(*si).map(|s| s.value(*c)).unwrap_or_default()).collect()
222 }
223}
224
225#[derive(Clone, Copy, Debug, PartialEq)]
228pub(crate) struct Seek {
229 pub x: f64,
230 pub fx: f64,
231 pub iterations: u32,
232 pub converged: bool,
233}
234
235pub(crate) fn seek(x0: f64, f: &mut dyn FnMut(f64) -> f64, max_iter: u32, tol: f64) -> Seek {
239 let tol = if tol.is_finite() && tol > 0.0 { tol } else { 0.001 };
240 let max_iter = max_iter.clamp(1, 10_000);
241 let x0 = if x0.is_finite() { x0 } else { 0.0 };
242 let f0 = f(x0);
243 let mut best = (x0, f0);
244 let consider = |best: &mut (f64, f64), x: f64, fx: f64| {
245 if fx.is_finite() && (!best.1.is_finite() || fx.abs() < best.1.abs()) {
246 *best = (x, fx);
247 }
248 };
249 let done = |best: &(f64, f64), it: u32| Seek { x: best.0, fx: best.1, iterations: it, converged: best.1.is_finite() && best.1.abs() <= tol };
250 if f0.is_finite() && f0.abs() <= tol {
251 return done(&best, 0);
252 }
253 let step0 = if x0 == 0.0 { 0.01 } else { x0.abs() * 0.01 };
254 let (mut xa, mut fa) = (x0, f0);
255 let mut xb = x0 + step0;
256 let mut fb = f(xb);
257 let mut it = 1u32;
258 consider(&mut best, xb, fb);
259 let mut bracket: Option<(f64, f64, f64, f64)> = None;
261 let mut slow = 0u32;
262 while it < max_iter {
263 if fb.is_finite() && fb.abs() <= tol {
264 break;
265 }
266 if bracket.is_none() && fa.is_finite() && fb.is_finite() && (fa < 0.0) != (fb < 0.0) {
267 bracket = Some((xa, fa, xb, fb));
268 }
269 let secant = if fa.is_finite() && fb.is_finite() && fb != fa { xb - fb * (xb - xa) / (fb - fa) } else { f64::NAN };
270 let next = match bracket {
271 Some((lo, _, hi, _)) => {
272 let (a, b) = (lo.min(hi), lo.max(hi));
273 if slow >= 2 || !(secant > a && secant < b) { (lo + hi) / 2.0 } else { secant }
274 }
275 None => {
276 let span = (xb - xa).abs().max(f64::MIN_POSITIVE);
277 if !fb.is_finite() {
278 (xa + xb) / 2.0
280 } else if !secant.is_finite() {
281 xb + 2.0 * (xb - xa)
282 } else if (secant - xb).abs() > 100.0 * span + 1.0 {
283 xb + (secant - xb).signum() * (100.0 * span + 1.0)
284 } else {
285 secant
286 }
287 }
288 };
289 if !next.is_finite() {
290 break;
291 }
292 let fnext = f(next);
293 it += 1;
294 consider(&mut best, next, fnext);
295 if let Some((lo, flo, hi, fhi)) = bracket {
296 let width = (hi - lo).abs();
297 if fnext.is_finite() {
298 bracket = Some(if (fnext < 0.0) == (flo < 0.0) { (next, fnext, hi, fhi) } else { (lo, flo, next, fnext) });
299 } else {
300 bracket = Some(if flo.abs() < fhi.abs() { (lo, flo, next, fhi) } else { (next, flo, hi, fhi) });
302 }
303 if let Some((l, _, h, _)) = bracket {
304 let nw = (h - l).abs();
305 slow = if nw > width * 0.5 { slow + 1 } else { 0 };
306 if nw <= f64::EPSILON * (1.0 + l.abs().max(h.abs())) {
307 break;
308 }
309 }
310 }
311 xa = xb;
312 fa = fb;
313 xb = next;
314 fb = fnext;
315 }
316 done(&best, it)
317}
318
319fn goal_seek(s: &mut Session, p: &Json) -> Result<Json> {
320 const CMD: &str = "data.goalSeek";
321 let (ssi, set) = cell_arg(s, p, "set", CMD)?;
322 let (csi, chg) = cell_arg(s, p, "changing", CMD)?;
323 let target = f64_param(p, "to")
324 .or_else(|| str_param(p, "to").and_then(crate::base::parse::parse_number_text))
325 .filter(|v| v.is_finite())
326 .ok_or_else(|| bad(CMD, "`to` must be a number"))?;
327 let d = s.doc()?;
328 let has_formula = |si: usize, c: CellRef| d.wb.sheet(si).and_then(|sh| sh.cell(c)).is_some_and(|c| c.formula.is_some());
329 if !has_formula(ssi, set) {
330 return Err(EngineError::Other("Cell must contain a formula.".into()));
331 }
332 if has_formula(csi, chg) || d.wb.sheet(csi).and_then(|sh| sh.cell(chg)).is_some_and(|c| c.py.is_some()) {
334 return Err(EngineError::Other("Cell must contain a value.".into()));
335 }
336 let x0 = d.wb.sheet(csi).map(|sh| sh.value(chg)).and_then(|v| v.as_f64()).unwrap_or(0.0);
337 let (max_iter, tol) = (d.wb.calc.max_iterations, d.wb.calc.max_change);
338 let mut trial = Trial::new(d);
339 let mut f = |x: f64| -> f64 {
340 trial.run(&[(csi, chg, Value::number(x))], &[(ssi, set)]).first().and_then(Value::as_f64).map(|v| v - target).unwrap_or(f64::NAN)
341 };
342 let r = seek(x0, &mut f, max_iter, tol);
343 edit(s, |cx| {
344 let sh = cx.sheet_mut(csi)?;
345 let style = sh.style_id(chg);
346 sh.set_cell(chg, Cell { value: Value::number(r.x), formula: None, style, py: None });
347 cx.touch(csi, chg);
348 Ok(())
349 })?;
350 let result = s.doc()?.wb.sheet(ssi).map(|sh| sh.value(set)).unwrap_or_default();
351 Ok(json!({"value": r.x, "result": value_json(&result), "iterations": r.iterations, "converged": r.converged}))
352}
353
354const MAX_TABLE_CELLS: u64 = 100_000;
357
358fn data_table(s: &mut Session, p: &Json) -> Result<Json> {
359 const CMD: &str = "data.dataTable";
360 let (si, r) = range_arg(s, p, "range", CMD)?;
361 let row_in = if p.get("rowInput").and_then(Json::as_str).is_some_and(|t| !t.is_empty()) { Some(cell_arg(s, p, "rowInput", CMD)?) } else { None };
362 let col_in =
363 if p.get("columnInput").and_then(Json::as_str).is_some_and(|t| !t.is_empty()) { Some(cell_arg(s, p, "columnInput", CMD)?) } else { None };
364 if row_in.is_none() && col_in.is_none() {
365 return Err(bad(CMD, "give `rowInput`, `columnInput` or both"));
366 }
367 if r.height() < 2 || r.width() < 2 {
368 return Err(bad(CMD, "the table needs at least two rows and two columns"));
369 }
370 let body = RangeRef::new(CellRef::new(r.start.row + 1, r.start.col + 1), r.end);
371 if body.count() > MAX_TABLE_CELLS {
372 return Err(bad(CMD, format!("the table body is too large ({MAX_TABLE_CELLS} cells max)")));
373 }
374 for input in [row_in, col_in].into_iter().flatten() {
375 if input.0 == si && r.contains(input.1) {
376 return Err(EngineError::Other("Input cell reference is not valid.".into()));
377 }
378 }
379 let d = s.doc()?;
380 let sh = d.wb.sheet(si).ok_or(EngineError::NoDocument)?;
381 let (r0, c0) = (r.start.row, r.start.col);
382 let mut trial = Trial::new(d);
383 let mut out: Vec<(CellRef, Value)> = Vec::with_capacity(body.count() as usize);
384 let mode = match (row_in, col_in) {
385 (None, Some((isi, ic))) => {
386 let reads: Vec<(usize, CellRef)> = (c0 + 1..=r.end.col).map(|c| (si, CellRef::new(r0, c))).collect();
387 for row in r0 + 1..=r.end.row {
388 let v = sh.value(CellRef::new(row, c0));
389 let vals = trial.run(&[(isi, ic, v)], &reads);
390 for (k, val) in vals.into_iter().enumerate() {
391 out.push((CellRef::new(row, c0 + 1 + k as u32), val));
392 }
393 }
394 "column"
395 }
396 (Some((isi, ic)), None) => {
397 let reads: Vec<(usize, CellRef)> = (r0 + 1..=r.end.row).map(|row| (si, CellRef::new(row, c0))).collect();
398 for col in c0 + 1..=r.end.col {
399 let v = sh.value(CellRef::new(r0, col));
400 let vals = trial.run(&[(isi, ic, v)], &reads);
401 for (k, val) in vals.into_iter().enumerate() {
402 out.push((CellRef::new(r0 + 1 + k as u32, col), val));
403 }
404 }
405 "row"
406 }
407 (Some((rsi, rc)), Some((csi, cc))) => {
408 let reads = [(si, CellRef::new(r0, c0))];
409 for row in r0 + 1..=r.end.row {
410 let cv = sh.value(CellRef::new(row, c0));
411 for col in c0 + 1..=r.end.col {
412 let rv = sh.value(CellRef::new(r0, col));
413 let vals = trial.run(&[(rsi, rc, rv), (csi, cc, cv.clone())], &reads);
414 out.push((CellRef::new(row, col), vals.into_iter().next().unwrap_or_default()));
415 }
416 }
417 "twoVariable"
418 }
419 (None, None) => return Err(bad(CMD, "give `rowInput`, `columnInput` or both")),
420 };
421 let n = out.len();
422 edit(s, |cx| {
423 for (c, v) in out {
424 let sh = cx.sheet_mut(si)?;
425 let style = sh.style_id(c);
426 sh.set_cell(c, Cell { value: v, formula: None, style, py: None });
427 cx.touch(si, c);
428 }
429 Ok(())
430 })?;
431 Ok(json!({"range": body.a1(), "cells": n, "mode": mode, "static": true}))
432}
433
434#[cfg(test)]
435mod tests;