formualizer_eval/
map_ctx.rs

1// Note: keep imports minimal; coercion is centralized via crate::coercion
2use crate::broadcast::{Shape2D, broadcast_shape, project_index};
3use crate::traits::{ArgumentHandle, FunctionContext};
4use formualizer_common::{ExcelError, ExcelErrorKind, LiteralValue};
5
6/// Elementwise mapping context (minimal unary-numeric support for Milestone 3)
7///
8/// This initial version targets unary numeric builtins (e.g., SIN/COS).
9/// It detects an array/range input, iterates elementwise in row-major order,
10/// applies the provided unary function, and produces an output `LiteralValue`:
11/// - Scalar input => caller should prefer scalar fallback; this context is
12///   designed for array/range cases and returns an error if no array present.
13pub struct SimpleMapCtx<'a, 'b> {
14    args: &'a [ArgumentHandle<'a, 'b>],
15    ctx: &'a dyn FunctionContext,
16    shape: (usize, usize),
17    output_rows: Vec<Vec<LiteralValue>>,
18}
19
20impl<'a, 'b> SimpleMapCtx<'a, 'b> {
21    pub fn new(args: &'a [ArgumentHandle<'a, 'b>], ctx: &'a dyn FunctionContext) -> Self {
22        // Determine broadcast shape across all inputs
23        let mut shapes: Vec<Shape2D> = Vec::with_capacity(args.len().max(1));
24        if args.is_empty() {
25            shapes.push((1, 1));
26        } else {
27            for a in args.iter() {
28                if let Ok(rv) = a.range_view() {
29                    shapes.push(rv.dims());
30                } else if let Ok(v) = a.value() {
31                    if let LiteralValue::Array(arr) = v.as_ref() {
32                        let rows = arr.len();
33                        let cols = arr.first().map(|r| r.len()).unwrap_or(0);
34                        shapes.push((rows, cols));
35                    } else {
36                        shapes.push((1, 1));
37                    }
38                } else {
39                    shapes.push((1, 1));
40                }
41            }
42        }
43        let shape = broadcast_shape(&shapes).unwrap_or((1, 1));
44        Self {
45            args,
46            ctx,
47            shape,
48            output_rows: Vec::new(),
49        }
50    }
51
52    pub fn input_count(&self) -> usize {
53        self.args.len()
54    }
55
56    pub fn broadcast_shape(&self) -> (usize, usize) {
57        self.shape
58    }
59
60    pub fn is_array_context(&self) -> bool {
61        self.shape != (1, 1)
62    }
63
64    /// Map a unary numeric function over the (broadcasted) input.
65    /// Policy: NumberLenientText; non-coercible values yield #VALUE! per cell.
66    pub fn map_unary_numeric<F>(&mut self, mut f: F) -> Result<(), ExcelError>
67    where
68        F: FnMut(f64) -> Result<LiteralValue, ExcelError>,
69    {
70        if self.args.is_empty() {
71            return Err(ExcelError::new(ExcelErrorKind::Value)
72                .with_message("No arguments provided to elementwise function"));
73        }
74
75        // Determine input as RangeView or Array or Scalar
76        // Prefer range_view streaming path when available
77        let first = &self.args[0];
78        if let Ok(view) = first.range_view() {
79            let (rows, _cols) = self.shape;
80            let mut row_idx = 0usize;
81            self.output_rows.clear();
82            view.for_each_row(&mut |row| {
83                let mut out_row: Vec<LiteralValue> = Vec::with_capacity(row.len());
84                for cell in row.iter() {
85                    let num_opt = match cell {
86                        LiteralValue::Error(e) => return Err(e.clone()),
87                        other => crate::coercion::to_number_lenient_with_locale(
88                            other,
89                            &self.ctx.locale(),
90                        )
91                        .ok(),
92                    };
93                    match num_opt {
94                        Some(n) => out_row.push(f(n)?),
95                        None => out_row.push(LiteralValue::Error(
96                            ExcelError::new(ExcelErrorKind::Value)
97                                .with_message("Element is not coercible to number"),
98                        )),
99                    }
100                }
101                self.output_rows.push(out_row);
102                row_idx += 1;
103                Ok(())
104            })?;
105            // In case of jagged/empty, normalize to intended rows
106            if self.output_rows.is_empty() && rows == 0 {
107                self.output_rows = Vec::new();
108            }
109            return Ok(());
110        }
111
112        // Fallback: literal array
113        if let Ok(v) = first.value() {
114            if let LiteralValue::Array(arr) = v.clone().into_owned() {
115                self.output_rows.clear();
116                for row in arr.into_iter() {
117                    let mut out_row: Vec<LiteralValue> = Vec::with_capacity(row.len());
118                    for cell in row.into_iter() {
119                        let num_opt = match cell {
120                            LiteralValue::Error(e) => return Err(e),
121                            other => crate::coercion::to_number_lenient_with_locale(
122                                &other,
123                                &self.ctx.locale(),
124                            )
125                            .ok(),
126                        };
127                        match num_opt {
128                            Some(n) => out_row.push(f(n)?),
129                            None => out_row.push(LiteralValue::Error(
130                                ExcelError::new(ExcelErrorKind::Value)
131                                    .with_message("Element is not coercible to number"),
132                            )),
133                        }
134                    }
135                    self.output_rows.push(out_row);
136                }
137                return Ok(());
138            }
139            // Scalar: map single value
140            match v.as_ref() {
141                LiteralValue::Error(e) => return Err(e.clone()),
142                other => {
143                    let as_num =
144                        crate::coercion::to_number_lenient_with_locale(other, &self.ctx.locale())
145                            .ok();
146                    let out = match as_num {
147                        Some(n) => f(n)?,
148                        None => LiteralValue::Error(
149                            ExcelError::new(ExcelErrorKind::Value)
150                                .with_message("Value is not coercible to number"),
151                        ),
152                    };
153                    self.output_rows.clear();
154                    self.output_rows.push(vec![out]);
155                    self.shape = (1, 1);
156                    return Ok(());
157                }
158            }
159        }
160
161        // If we reach here, there is no array/range; treat as #VALUE!
162        Err(ExcelError::new(ExcelErrorKind::Value)
163            .with_message("No array or scalar value provided for elementwise map"))
164    }
165
166    /// Binary numeric map with broadcasting across two inputs (args[0], args[1]).
167    pub fn map_binary_numeric<F>(&mut self, mut f: F) -> Result<(), ExcelError>
168    where
169        F: FnMut(f64, f64) -> Result<LiteralValue, ExcelError>,
170    {
171        if self.args.len() < 2 {
172            return Err(ExcelError::new(ExcelErrorKind::Value)
173                .with_message("Binary elementwise function requires two args"));
174        }
175        let a0 = &self.args[0];
176        let a1 = &self.args[1];
177        let target = self.shape;
178        self.output_rows.clear();
179
180        // Materialize both inputs as arrays of LiteralValue for now (future: streaming stripes)
181        let to_array = |ah: &ArgumentHandle| -> Result<Vec<Vec<LiteralValue>>, ExcelError> {
182            if let Ok(rv) = ah.range_view() {
183                let mut rows: Vec<Vec<LiteralValue>> = Vec::new();
184                rv.for_each_row(&mut |row| {
185                    rows.push(row.to_vec());
186                    Ok(())
187                })?;
188                Ok(rows)
189            } else {
190                let v = ah.value()?;
191                Ok(match v.as_ref() {
192                    LiteralValue::Array(arr) => arr.clone(),
193                    other => vec![vec![other.clone()]],
194                })
195            }
196        };
197
198        let arr0 = to_array(a0)?;
199        let arr1 = to_array(a1)?;
200        let shape0 = (arr0.len(), arr0.first().map(|r| r.len()).unwrap_or(0));
201        let shape1 = (arr1.len(), arr1.first().map(|r| r.len()).unwrap_or(0));
202        let _ = broadcast_shape(&[shape0, shape1])?; // validate compatible
203
204        for r in 0..target.0 {
205            let mut out_row = Vec::with_capacity(target.1);
206            for c in 0..target.1 {
207                let (r0, c0) = project_index((r, c), shape0);
208                let (r1, c1) = project_index((r, c), shape1);
209                let lv0 = arr0
210                    .get(r0)
211                    .and_then(|row| row.get(c0))
212                    .cloned()
213                    .unwrap_or(LiteralValue::Empty);
214                let lv1 = arr1
215                    .get(r1)
216                    .and_then(|row| row.get(c1))
217                    .cloned()
218                    .unwrap_or(LiteralValue::Empty);
219
220                let n0 = match &lv0 {
221                    LiteralValue::Number(n) => Some(*n),
222                    LiteralValue::Int(i) => Some(*i as f64),
223                    LiteralValue::Boolean(b) => Some(if *b { 1.0 } else { 0.0 }),
224                    LiteralValue::Empty => Some(0.0),
225                    LiteralValue::Text(s) => s.trim().parse::<f64>().ok(),
226                    LiteralValue::Error(e) => return Err(e.clone()),
227                    _ => None,
228                };
229                let n1 = match &lv1 {
230                    LiteralValue::Number(n) => Some(*n),
231                    LiteralValue::Int(i) => Some(*i as f64),
232                    LiteralValue::Boolean(b) => Some(if *b { 1.0 } else { 0.0 }),
233                    LiteralValue::Empty => Some(0.0),
234                    LiteralValue::Text(s) => s.trim().parse::<f64>().ok(),
235                    LiteralValue::Error(e) => return Err(e.clone()),
236                    _ => None,
237                };
238                let out_cell = match (n0, n1) {
239                    (Some(a), Some(b)) => f(a, b)?,
240                    _ => LiteralValue::Error(
241                        ExcelError::new(ExcelErrorKind::Value)
242                            .with_message("Elements are not coercible to numbers"),
243                    ),
244                };
245                out_row.push(out_cell);
246            }
247            self.output_rows.push(out_row);
248        }
249        Ok(())
250    }
251
252    pub fn take_output(self) -> LiteralValue {
253        if self.shape == (1, 1) {
254            if let Some(row) = self.output_rows.first() {
255                if let Some(cell) = row.first() {
256                    return cell.clone();
257                }
258            }
259            LiteralValue::Empty
260        } else {
261            LiteralValue::Array(self.output_rows)
262        }
263    }
264}
265
266impl<'a, 'b> crate::function::FnMapCtx for SimpleMapCtx<'a, 'b> {
267    fn is_array_context(&self) -> bool {
268        self.is_array_context()
269    }
270
271    fn map_unary_numeric(
272        &mut self,
273        f: &mut dyn FnMut(f64) -> Result<LiteralValue, ExcelError>,
274    ) -> Result<(), ExcelError> {
275        self.map_unary_numeric(f)
276    }
277
278    fn map_binary_numeric(
279        &mut self,
280        f: &mut dyn FnMut(f64, f64) -> Result<LiteralValue, ExcelError>,
281    ) -> Result<(), ExcelError> {
282        self.map_binary_numeric(f)
283    }
284
285    fn finalize(&mut self) -> LiteralValue {
286        // Construct a shallow move by swapping out the buffer
287        let rows = std::mem::take(&mut self.output_rows);
288        if self.shape == (1, 1) {
289            LiteralValue::Empty
290        } else {
291            LiteralValue::Array(rows)
292        }
293    }
294}