Skip to main content

formualizer_eval/builtins/math/
reduction.rs

1use super::super::utils::{ARG_RANGE_NUM_LENIENT_ONE, coerce_num};
2use crate::args::ArgSchema;
3use crate::function::Function;
4use crate::traits::{ArgumentHandle, FunctionContext};
5use arrow_array::Array;
6use formualizer_common::{ExcelError, LiteralValue};
7use formualizer_macros::func_caps;
8
9#[derive(Debug)]
10pub struct MinFn; // MIN(...)
11impl Function for MinFn {
12    func_caps!(PURE, REDUCTION, NUMERIC_ONLY);
13    fn name(&self) -> &'static str {
14        "MIN"
15    }
16    fn min_args(&self) -> usize {
17        1
18    }
19    fn variadic(&self) -> bool {
20        true
21    }
22    fn arg_schema(&self) -> &'static [ArgSchema] {
23        &ARG_RANGE_NUM_LENIENT_ONE[..]
24    }
25    fn eval<'a, 'b, 'c>(
26        &self,
27        args: &'c [ArgumentHandle<'a, 'b>],
28        _ctx: &dyn FunctionContext<'b>,
29    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
30        let mut mv: Option<f64> = None;
31        for a in args {
32            if let Ok(view) = a.range_view() {
33                // Propagate errors from range first
34                for res in view.errors_slices() {
35                    let (_, _, err_cols) = res?;
36                    for col in err_cols {
37                        if col.null_count() < col.len() {
38                            for i in 0..col.len() {
39                                if !col.is_null(i) {
40                                    return Ok(crate::traits::CalcValue::Scalar(
41                                        LiteralValue::Error(ExcelError::new(
42                                            crate::arrow_store::unmap_error_code(col.value(i)),
43                                        )),
44                                    ));
45                                }
46                            }
47                        }
48                    }
49                }
50
51                for res in view.numbers_slices() {
52                    let (_, _, num_cols) = res?;
53                    for col in num_cols {
54                        if let Some(n) = arrow::compute::kernels::aggregate::min(col.as_ref()) {
55                            mv = Some(mv.map(|m| m.min(n)).unwrap_or(n));
56                        }
57                    }
58                }
59            } else {
60                let v = a.value()?.into_literal();
61                match v {
62                    LiteralValue::Error(e) => {
63                        return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
64                    }
65                    other => {
66                        if let Ok(n) = coerce_num(&other) {
67                            mv = Some(mv.map(|m| m.min(n)).unwrap_or(n));
68                        }
69                    }
70                }
71            }
72        }
73        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
74            mv.unwrap_or(0.0),
75        )))
76    }
77}
78
79#[derive(Debug)]
80pub struct MaxFn; // MAX(...)
81impl Function for MaxFn {
82    func_caps!(PURE, REDUCTION, NUMERIC_ONLY);
83    fn name(&self) -> &'static str {
84        "MAX"
85    }
86    fn min_args(&self) -> usize {
87        1
88    }
89    fn variadic(&self) -> bool {
90        true
91    }
92    fn arg_schema(&self) -> &'static [ArgSchema] {
93        &ARG_RANGE_NUM_LENIENT_ONE[..]
94    }
95    fn eval<'a, 'b, 'c>(
96        &self,
97        args: &'c [ArgumentHandle<'a, 'b>],
98        _ctx: &dyn FunctionContext<'b>,
99    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
100        let mut mv: Option<f64> = None;
101        for a in args {
102            if let Ok(view) = a.range_view() {
103                // Propagate errors from range first
104                for res in view.errors_slices() {
105                    let (_, _, err_cols) = res?;
106                    for col in err_cols {
107                        if col.null_count() < col.len() {
108                            for i in 0..col.len() {
109                                if !col.is_null(i) {
110                                    return Ok(crate::traits::CalcValue::Scalar(
111                                        LiteralValue::Error(ExcelError::new(
112                                            crate::arrow_store::unmap_error_code(col.value(i)),
113                                        )),
114                                    ));
115                                }
116                            }
117                        }
118                    }
119                }
120
121                for res in view.numbers_slices() {
122                    let (_, _, num_cols) = res?;
123                    for col in num_cols {
124                        if let Some(n) = arrow::compute::kernels::aggregate::max(col.as_ref()) {
125                            mv = Some(mv.map(|m| m.max(n)).unwrap_or(n));
126                        }
127                    }
128                }
129            } else {
130                let v = a.value()?.into_literal();
131                match v {
132                    LiteralValue::Error(e) => {
133                        return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
134                    }
135                    other => {
136                        if let Ok(n) = coerce_num(&other) {
137                            mv = Some(mv.map(|m| m.max(n)).unwrap_or(n));
138                        }
139                    }
140                }
141            }
142        }
143        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
144            mv.unwrap_or(0.0),
145        )))
146    }
147}
148
149pub fn register_builtins() {
150    use std::sync::Arc;
151    crate::function_registry::register_function(Arc::new(MinFn));
152    crate::function_registry::register_function(Arc::new(MaxFn));
153}
154
155#[cfg(test)]
156mod tests_min_max {
157    use super::*;
158    use crate::test_workbook::TestWorkbook;
159    use crate::traits::ArgumentHandle;
160    use formualizer_common::LiteralValue;
161    use formualizer_parse::parser::{ASTNode, ASTNodeType};
162    fn interp(wb: &TestWorkbook) -> crate::interpreter::Interpreter<'_> {
163        wb.interpreter()
164    }
165
166    #[test]
167    fn min_basic_array_and_scalar() {
168        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(MinFn));
169        let ctx = interp(&wb);
170        let arr = ASTNode::new(
171            ASTNodeType::Literal(LiteralValue::Array(vec![vec![
172                LiteralValue::Int(5),
173                LiteralValue::Int(2),
174                LiteralValue::Int(9),
175            ]])),
176            None,
177        );
178        let extra = ASTNode::new(ASTNodeType::Literal(LiteralValue::Int(1)), None);
179        let f = ctx.context.get_function("", "MIN").unwrap();
180        let out = f
181            .dispatch(
182                &[
183                    ArgumentHandle::new(&arr, &ctx),
184                    ArgumentHandle::new(&extra, &ctx),
185                ],
186                &ctx.function_context(None),
187            )
188            .unwrap()
189            .into_literal();
190        assert_eq!(out, LiteralValue::Number(1.0));
191    }
192
193    #[test]
194    fn max_basic_with_text_ignored() {
195        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(MaxFn));
196        let ctx = interp(&wb);
197        let arr = ASTNode::new(
198            ASTNodeType::Literal(LiteralValue::Array(vec![vec![
199                LiteralValue::Int(5),
200                LiteralValue::Text("x".into()),
201                LiteralValue::Int(9),
202            ]])),
203            None,
204        );
205        let f = ctx.context.get_function("", "MAX").unwrap();
206        let out = f
207            .dispatch(
208                &[ArgumentHandle::new(&arr, &ctx)],
209                &ctx.function_context(None),
210            )
211            .unwrap()
212            .into_literal();
213        assert_eq!(out, LiteralValue::Number(9.0));
214    }
215
216    #[test]
217    fn min_error_propagates() {
218        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(MinFn));
219        let ctx = interp(&wb);
220        let err = ASTNode::new(
221            ASTNodeType::Literal(LiteralValue::Error(ExcelError::new_na())),
222            None,
223        );
224        let one = ASTNode::new(ASTNodeType::Literal(LiteralValue::Int(1)), None);
225        let f = ctx.context.get_function("", "MIN").unwrap();
226        let out = f
227            .dispatch(
228                &[
229                    ArgumentHandle::new(&err, &ctx),
230                    ArgumentHandle::new(&one, &ctx),
231                ],
232                &ctx.function_context(None),
233            )
234            .unwrap()
235            .into_literal();
236        match out {
237            LiteralValue::Error(e) => assert_eq!(e, "#N/A"),
238            v => panic!("expected error got {v:?}"),
239        }
240    }
241
242    #[test]
243    fn max_error_propagates() {
244        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(MaxFn));
245        let ctx = interp(&wb);
246        let err = ASTNode::new(
247            ASTNodeType::Literal(LiteralValue::Error(ExcelError::from_error_string(
248                "#DIV/0!",
249            ))),
250            None,
251        );
252        let one = ASTNode::new(ASTNodeType::Literal(LiteralValue::Int(1)), None);
253        let f = ctx.context.get_function("", "MAX").unwrap();
254        let out = f
255            .dispatch(
256                &[
257                    ArgumentHandle::new(&one, &ctx),
258                    ArgumentHandle::new(&err, &ctx),
259                ],
260                &ctx.function_context(None),
261            )
262            .unwrap()
263            .into_literal();
264        match out {
265            LiteralValue::Error(e) => assert_eq!(e, "#DIV/0!"),
266            v => panic!("expected error got {v:?}"),
267        }
268    }
269}