Skip to main content

improv_engine/
reduce.rs

1//! The reductions: what an aggregation (`SUM(x OVER T)`, `CORREL(x, y OVER
2//! T)`, `NPV(rate, cf OVER T)`) computes from one group's rows.
3//!
4//! The dataflow hands a group over as [`Rows`]: every row's position along
5//! each reduced category (so rows arrive in item order: the first row is the
6//! first item that has a cell) and its values, one per reduced measure in
7//! argument order. Parameters (`rate`, `k`, `guess`) have one value per group.
8//! The scalar evaluator never reduces (a scalar formula has no categories).
9//!
10//! Families add their reductions in `reduce/<family>.rs`, dispatched by id
11//! range like `builtins`: P4 statistics own 1000–1099 and 1100–1149, P5
12//! financial 1150–1199, P6 lookups 1200–1249.
13
14use crate::builtins::{self, err, text_of};
15use crate::CellValue;
16use improv_core_model::FuncId;
17
18mod analysis;
19mod array;
20mod financial;
21mod lookup;
22mod statistical;
23
24/// One group's rows, in item order.
25#[derive(Debug, Clone, PartialEq)]
26pub struct Rows {
27    /// Per row, the position of its item along each reduced category
28    /// (0-based, in the category's item order).
29    pub pos: Vec<Vec<u32>>,
30    /// Per row, the values of the reduced measures (argument order).
31    pub vals: Vec<Vec<CellValue>>,
32    /// How many cells the reduced categories could hold (the product of
33    /// their item counts), for `COUNTBLANK`.
34    pub capacity: usize,
35}
36
37impl Rows {
38    /// The `i`-th reduced measure's values, in item order.
39    pub fn col(&self, i: usize) -> impl Iterator<Item = &CellValue> + '_ {
40        self.vals.iter().filter_map(move |r| r.get(i))
41    }
42
43    /// The first error among the values (in item order), if any.
44    pub fn first_error(&self) -> Option<CellValue> {
45        self.vals
46            .iter()
47            .flatten()
48            .find(|v| matches!(v, CellValue::Err(_)))
49            .cloned()
50    }
51
52    /// The number of rows.
53    pub fn len(&self) -> usize {
54        self.vals.len()
55    }
56
57    /// Whether there are no rows.
58    pub fn is_empty(&self) -> bool {
59        self.vals.is_empty()
60    }
61}
62
63/// Reduce one group: aggregation `func` over `rows` with `params`.
64pub fn reduce(func: FuncId, rows: &Rows, params: &[CellValue]) -> CellValue {
65    if (1200..=1249).contains(&func.0) {
66        // A lookup checks its own parameters: XLOOKUP's if_not_found may be
67        // an error (it is read only when nothing matches).
68        return lookup::reduce(func.0, rows, params);
69    }
70    if let Some(bad) = params.iter().find(|v| matches!(v, CellValue::Err(_))) {
71        return bad.clone();
72    }
73    match func.0 {
74        1..=9 => core(func.0, rows),
75        906..=909 => ordered(func.0, rows, params),
76        1000..=1149 => statistical::reduce(func.0, rows, params),
77        1150..=1199 => financial::reduce(func.0, rows, params),
78        1850..=1999 => analysis::reduce(func.0, rows, params),
79        _ => CellValue::Err(err::NAME),
80    }
81}
82
83/// One result per row (a [`improv_core_model::functions::Kind::PerItem`]
84/// function such as `RANK`): the `i`-th result belongs to the `i`-th row.
85pub fn per_item(func: FuncId, rows: &Rows, params: &[CellValue]) -> Vec<CellValue> {
86    if let Some(bad) = params.iter().find(|v| matches!(v, CellValue::Err(_))) {
87        return vec![bad.clone(); rows.len()];
88    }
89    // Each family adds its per-item reductions here by id (RANK, TREND, …).
90    match func.0 {
91        1000..=1149 => statistical::per_item(func.0, rows, params),
92        1850..=1999 => analysis::per_item(func.0, rows, params),
93        _ => vec![CellValue::Err(err::NAME); rows.len()],
94    }
95}
96
97/// A list of results (an [`improv_core_model::functions::Kind::Array`]
98/// function such as `SORT`), laid along the `INTO` category's items in order.
99pub fn array(func: FuncId, rows: &Rows, params: &[CellValue]) -> Vec<CellValue> {
100    if let Some(bad) = params.iter().find(|v| matches!(v, CellValue::Err(_))) {
101        return vec![bad.clone()];
102    }
103    match func.0 {
104        1250..=1299 => array::array(func.0, rows, params),
105        1850..=1999 => analysis::array(func.0, rows, params),
106        _ => vec![CellValue::Err(err::NAME)],
107    }
108}
109
110/// SUM … XOR through their scalar twins (an error in the group is the
111/// result: the first in item order).
112fn core(id: u32, rows: &Rows) -> CellValue {
113    if let Some(bad) = rows.first_error() {
114        return bad;
115    }
116    let twin = match id {
117        1 => 22,
118        2 => 23,
119        3 => 20,
120        4 => 21,
121        5 => 24,
122        6 => 25,
123        7 => 26,
124        8 => 27,
125        _ => 28,
126    };
127    let vals: Vec<CellValue> = rows.col(0).cloned().collect();
128    builtins::eval(FuncId(twin), &vals)
129}
130
131/// FIRST, LAST, S, TEXTJOIN: they read the rows in item order.
132fn ordered(id: u32, rows: &Rows, params: &[CellValue]) -> CellValue {
133    match id {
134        906 | 908 => {
135            let Some(v) = rows.col(0).next().cloned() else {
136                return CellValue::Err(err::NA);
137            };
138            if id == 908 {
139                return match v {
140                    CellValue::Text(_) | CellValue::Err(_) => v,
141                    _ => CellValue::Text(String::new()),
142                };
143            }
144            v
145        }
146        907 => rows
147            .col(0)
148            .last()
149            .cloned()
150            .unwrap_or(CellValue::Err(err::NA)),
151        _ => {
152            // TEXTJOIN(sep, skip_empty, x OVER T): params are (sep, skip).
153            let sep = match params.first().map(text_of) {
154                Some(Ok(s)) => s,
155                Some(Err(c)) => return CellValue::Err(c),
156                None => String::new(),
157            };
158            let skip = matches!(params.get(1), Some(CellValue::Bool(true)));
159            let mut parts = Vec::new();
160            for v in rows.col(0) {
161                match text_of(v) {
162                    Ok(s) if skip && s.is_empty() => {}
163                    Ok(s) => parts.push(s),
164                    Err(c) => return CellValue::Err(c),
165                }
166            }
167            let joined = parts.join(&sep);
168            if joined.chars().count() > 32_767 {
169                return CellValue::Err(err::VALUE);
170            }
171            CellValue::Text(joined)
172        }
173    }
174}
175
176#[cfg(test)]
177#[cfg_attr(coverage_nightly, coverage(off))]
178mod tests {
179    use super::*;
180
181    fn rows(vals: Vec<CellValue>) -> Rows {
182        Rows {
183            pos: (0..vals.len() as u32).map(|i| vec![i]).collect(),
184            vals: vals.into_iter().map(|v| vec![v]).collect(),
185            capacity: 4,
186        }
187    }
188
189    #[test]
190    fn core_and_ordered() {
191        let n = CellValue::num;
192        let r = rows(vec![n(4.0), n(1.0), n(7.0)]);
193        assert_eq!(reduce(FuncId(1), &r, &[]), n(12.0));
194        assert_eq!(reduce(FuncId(906), &r, &[]), n(4.0));
195        assert_eq!(reduce(FuncId(907), &r, &[]), n(7.0));
196        let t = rows(vec![
197            CellValue::Text("a".into()),
198            CellValue::Text(String::new()),
199            n(2.0),
200        ]);
201        let sep = CellValue::Text(", ".into());
202        assert_eq!(
203            reduce(FuncId(909), &t, &[sep.clone(), CellValue::Bool(true)]),
204            CellValue::Text("a, 2".into())
205        );
206        assert_eq!(
207            reduce(FuncId(909), &t, &[sep, CellValue::Bool(false)]),
208            CellValue::Text("a, , 2".into())
209        );
210        assert_eq!(reduce(FuncId(908), &r, &[]), CellValue::Text(String::new()));
211        assert_eq!(reduce(FuncId(908), &t, &[]), CellValue::Text("a".into()));
212        let e = rows(vec![n(1.0), CellValue::Err(err::NA)]);
213        assert_eq!(reduce(FuncId(1), &e, &[]), CellValue::Err(err::NA));
214        assert_eq!(
215            reduce(FuncId(1), &r, &[CellValue::Err(err::DIV0)]),
216            CellValue::Err(err::DIV0)
217        );
218        assert_eq!(reduce(FuncId(999), &r, &[]), CellValue::Err(err::NAME));
219        assert_eq!(
220            per_item(FuncId(999), &r, &[]),
221            vec![CellValue::Err(err::NAME); 3]
222        );
223        assert_eq!(
224            per_item(FuncId(999), &r, &[CellValue::Err(err::NA)]),
225            vec![CellValue::Err(err::NA); 3]
226        );
227        assert_eq!(r.len(), 3);
228        assert!(!r.is_empty());
229        // An array function's error parameter is its whole result.
230        assert_eq!(
231            array(FuncId(1250), &r, &[CellValue::Err(err::NA)]),
232            vec![CellValue::Err(err::NA)]
233        );
234        // TEXTJOIN: an error value or separator, and an over-long result.
235        let te = rows(vec![CellValue::Text("a".into()), CellValue::Err(err::NUM)]);
236        assert_eq!(
237            reduce(FuncId(909), &te, &[CellValue::Text(",".into())]),
238            CellValue::Err(err::NUM)
239        );
240        assert_eq!(reduce(FuncId(909), &t, &[]), CellValue::Text("a2".into()));
241        let long = rows(vec![CellValue::Text("x".repeat(20_000)); 2]);
242        assert_eq!(
243            reduce(FuncId(909), &long, &[CellValue::Text(String::new())]),
244            CellValue::Err(err::VALUE)
245        );
246        assert_eq!(reduce(FuncId(908), &e, &[]), CellValue::Text(String::new()));
247        let er = rows(vec![CellValue::Err(err::NA)]);
248        assert_eq!(reduce(FuncId(908), &er, &[]), CellValue::Err(err::NA));
249        assert_eq!(e.first_error(), Some(CellValue::Err(err::NA)));
250        let empty = rows(vec![]);
251        assert_eq!(reduce(FuncId(906), &empty, &[]), CellValue::Err(err::NA));
252        assert_eq!(reduce(FuncId(907), &empty, &[]), CellValue::Err(err::NA));
253    }
254}