Skip to main content

ravenrs_sheet/calc/eval/
special.rs

1use super::{Area, BinOp, Ev, Expr, Reference, SheetSel, Value, Workbook};
2use crate::base::{CellRef, MAX_COLS, MAX_ROWS, RangeRef};
3
4/// SUMIF and AVERAGEIF with a sum range: Excel sizes the sum range like the criteria range.
5pub(super) fn resizes_sum_range(name: &str, args: usize) -> bool {
6    args == 3 && matches!(name, "SUMIF" | "AVERAGEIF")
7}
8
9/// Height and width of a single-area reference or an array argument.
10pub(super) fn ev_shape(ev: &Ev) -> Option<(u32, u32)> {
11    match ev {
12        Ev::R(areas) => match areas.as_slice() {
13            [a] => Some((a.range.height(), a.range.width())),
14            _ => None,
15        },
16        Ev::V(Value::Array(a)) => Some((u32::try_from(a.rows).unwrap_or(u32::MAX), u32::try_from(a.cols).unwrap_or(u32::MAX))),
17        Ev::V(_) => Some((1, 1)),
18        Ev::L(_) => None,
19    }
20}
21
22/// The range SUMIF/AVERAGEIF add up: from the sum range's top-left cell, as many rows and columns
23/// as the criteria range (`height` × `width`), cut off at the sheet's edge.
24pub fn resized_sum_range(sum: RangeRef, height: u32, width: u32) -> RangeRef {
25    let row = sum.start.row.saturating_add(height.saturating_sub(1)).min(MAX_ROWS - 1);
26    let col = sum.start.col.saturating_add(width.saturating_sub(1)).min(MAX_COLS - 1);
27    RangeRef::new(sum.start, CellRef::new(row, col))
28}
29
30/// Collects the static references of an expression with their sheets resolved; `dynamic` is
31/// set when the formula uses INDIRECT/OFFSET/names/tables whose targets can change.
32pub fn precedents(wb: &Workbook, sheet: usize, e: &Expr) -> (Vec<Area>, bool) {
33    let mut out = Vec::new();
34    let mut dynamic = false;
35    let push = |out: &mut Vec<Area>, r: &Reference, range: RangeRef| match &r.sheet {
36        SheetSel::Current => out.push(Area { sheet, range }),
37        SheetSel::Named(n) => {
38            if let Some(i) = wb.sheet_index(n) {
39                out.push(Area { sheet: i, range });
40            }
41        }
42        SheetSel::Span(a, b) => {
43            if let (Some(i), Some(j)) = (wb.sheet_index(a), wb.sheet_index(b)) {
44                for s in i.min(j)..=i.max(j) {
45                    out.push(Area { sheet: s, range });
46                }
47            }
48        }
49    };
50    e.walk(&mut |x| match x {
51        Expr::Ref(r) => push(&mut out, r, r.range()),
52        // The cells SUMIF/AVERAGEIF really read from the sum range (see `resized_sum_range`); when
53        // the criteria range's size isn't known statically, recalculate the formula every time.
54        Expr::Call(n, args) if resizes_sum_range(n, args.len()) => match (args.first(), args.get(2)) {
55            (Some(Expr::Ref(c)), Some(Expr::Ref(s))) => {
56                let (crit, sum) = (c.range(), s.range());
57                push(&mut out, s, resized_sum_range(sum, crit.height(), crit.width()));
58            }
59            (_, Some(Expr::Number(_) | Expr::Text(_) | Expr::Bool(_) | Expr::Error(_) | Expr::Array(_) | Expr::Missing)) => {}
60            _ => dynamic = true,
61        },
62        Expr::Call(n, _)
63            if matches!(n.as_str(), "INDIRECT" | "OFFSET" | "NOW" | "TODAY" | "RAND" | "RANDBETWEEN" | "RANDARRAY" | "CELL" | "INFO") =>
64        {
65            dynamic = true
66        }
67        Expr::Name(_) | Expr::Struct(_) => dynamic = true,
68        Expr::Call(n, _) if crate::functions::lookup(n).is_none() && !is_special(n) => dynamic = true,
69        // A range ending in a function's result (`A1:INDEX(B1:B9,n)`) covers the cells between
70        // the references inside it, not only those references.
71        Expr::Binary(BinOp::Range, a, b) if !matches!((&**a, &**b), (Expr::Ref(_), Expr::Ref(_))) => {
72            let mut inner = precedents(wb, sheet, a).0;
73            inner.extend(precedents(wb, sheet, b).0);
74            let mut boxes: Vec<Area> = Vec::new();
75            for area in inner {
76                match boxes.iter_mut().find(|x| x.sheet == area.sheet) {
77                    Some(x) => x.range = x.range.union(&area.range),
78                    None => boxes.push(area),
79                }
80            }
81            out.extend(boxes);
82        }
83        _ => {}
84    });
85    (out, dynamic)
86}
87
88fn is_special(n: &str) -> bool {
89    matches!(
90        n,
91        "IF" | "IFS"
92            | "IFERROR"
93            | "IFNA"
94            | "CHOOSE"
95            | "SWITCH"
96            | "ROW"
97            | "COLUMN"
98            | "ROWS"
99            | "COLUMNS"
100            | "AREAS"
101            | "ISREF"
102            | "ISFORMULA"
103            | "FORMULATEXT"
104            | "SHEET"
105            | "SHEETS"
106            | "OFFSET"
107            | "INDIRECT"
108            | "INDEX"
109            | "CELL"
110            | "TRIMRANGE"
111            | "SUBTOTAL"
112            | "AGGREGATE"
113            | "TEXT"
114            | "LET"
115            | "LAMBDA"
116            | "ISOMITTED"
117            | "MAP"
118            | "REDUCE"
119            | "SCAN"
120            | "BYROW"
121            | "BYCOL"
122            | "MAKEARRAY"
123    )
124}
125
126/// Names of functions the evaluator implements itself (merged into the function list for UI).
127pub const SPECIAL_FUNCTIONS: &[&str] = &[
128    "IF",
129    "IFS",
130    "IFERROR",
131    "IFNA",
132    "CHOOSE",
133    "SWITCH",
134    "ROW",
135    "COLUMN",
136    "ROWS",
137    "COLUMNS",
138    "AREAS",
139    "ISREF",
140    "ISFORMULA",
141    "FORMULATEXT",
142    "SHEET",
143    "SHEETS",
144    "OFFSET",
145    "INDIRECT",
146    "INDEX",
147    "CELL",
148    "TRIMRANGE",
149    "SUBTOTAL",
150    "AGGREGATE",
151    "TEXT",
152    "LET",
153    "LAMBDA",
154    "ISOMITTED",
155    "MAP",
156    "REDUCE",
157    "SCAN",
158    "BYROW",
159    "BYCOL",
160    "MAKEARRAY",
161];
162
163pub fn is_known_function(n: &str) -> bool {
164    is_special(&n.to_ascii_uppercase()) || crate::functions::lookup(n).is_some()
165}