formualizer_eval/builtins/lookup/
array_shape.rs1use super::super::utils::collapse_if_scalar;
4use crate::args::{ArgSchema, CoercionPolicy, ShapeKind};
5use crate::function::Function;
6use crate::traits::{ArgumentHandle, FunctionContext};
7use formualizer_common::{ArgKind, ExcelError, ExcelErrorKind, LiteralValue};
8use formualizer_macros::func_caps;
9
10#[derive(Debug)]
41pub struct ToColFn;
42
43#[derive(Debug)]
74pub struct ToRowFn;
75
76fn schema() -> &'static [ArgSchema] {
77 use once_cell::sync::Lazy;
78 static SCHEMA: Lazy<Vec<ArgSchema>> = Lazy::new(|| {
79 vec![
80 ArgSchema {
81 kinds: smallvec::smallvec![ArgKind::Range, ArgKind::Any],
82 required: true,
83 by_ref: false,
84 shape: ShapeKind::Range,
85 coercion: CoercionPolicy::None,
86 max: None,
87 repeating: None,
88 default: None,
89 },
90 ArgSchema {
91 kinds: smallvec::smallvec![ArgKind::Number],
92 required: false,
93 by_ref: false,
94 shape: ShapeKind::Scalar,
95 coercion: CoercionPolicy::NumberLenientText,
96 max: None,
97 repeating: None,
98 default: Some(LiteralValue::Int(0)),
99 },
100 ArgSchema {
101 kinds: smallvec::smallvec![ArgKind::Logical, ArgKind::Number],
102 required: false,
103 by_ref: false,
104 shape: ShapeKind::Scalar,
105 coercion: CoercionPolicy::None,
106 max: None,
107 repeating: None,
108 default: Some(LiteralValue::Boolean(false)),
109 },
110 ]
111 });
112 &SCHEMA
113}
114
115fn materialize_arg<'b>(arg: &ArgumentHandle<'_, 'b>) -> Result<Vec<Vec<LiteralValue>>, ExcelError> {
116 if let Ok(view) = arg.range_view() {
117 let mut rows = Vec::new();
118 view.for_each_row(&mut |row| {
119 rows.push(row.to_vec());
120 Ok(())
121 })?;
122 return Ok(rows);
123 }
124
125 Ok(match arg.value()?.into_literal() {
126 LiteralValue::Array(rows) => rows,
127 v => vec![vec![v]],
128 })
129}
130
131fn ignore_mode<'b>(args: &[ArgumentHandle<'_, 'b>]) -> Result<i64, ExcelError> {
132 if args.len() < 2 {
133 return Ok(0);
134 }
135 let raw = args[1].value()?.into_literal();
136 let n = match raw {
137 LiteralValue::Int(i) => i,
138 LiteralValue::Number(n) => n as i64,
139 LiteralValue::Error(e) => return Err(e),
140 other => crate::coercion::to_number_lenient(&other)? as i64,
141 };
142 if !(0..=3).contains(&n) {
143 return Err(
144 ExcelError::new(ExcelErrorKind::Value).with_message("ignore must be 0, 1, 2, or 3")
145 );
146 }
147 Ok(n)
148}
149
150fn scan_by_column<'b>(args: &[ArgumentHandle<'_, 'b>]) -> Result<bool, ExcelError> {
151 if args.len() < 3 {
152 return Ok(false);
153 }
154 crate::coercion::to_logical(&args[2].value()?.into_literal())
155}
156
157fn include_cell(v: &LiteralValue, ignore: i64) -> bool {
158 let is_blank = matches!(v, LiteralValue::Empty);
159 let is_error = matches!(v, LiteralValue::Error(_));
160 match ignore {
161 1 => !is_blank,
162 2 => !is_error,
163 3 => !is_blank && !is_error,
164 _ => true,
165 }
166}
167
168fn flatten_array<'b>(args: &[ArgumentHandle<'_, 'b>]) -> Result<Vec<LiteralValue>, ExcelError> {
169 if args.is_empty() || args.len() > 3 {
170 return Err(ExcelError::new(ExcelErrorKind::Value));
171 }
172 let data = materialize_arg(&args[0])?;
173 let ignore = ignore_mode(args)?;
174 let scan_by_col = scan_by_column(args)?;
175
176 let rows = data.len();
177 let cols = data.iter().map(Vec::len).max().unwrap_or(0);
178 let mut flat = Vec::with_capacity(rows.saturating_mul(cols));
179
180 if scan_by_col {
181 for c in 0..cols {
182 for row in &data {
183 let v = row.get(c).cloned().unwrap_or(LiteralValue::Empty);
184 if include_cell(&v, ignore) {
185 flat.push(v);
186 }
187 }
188 }
189 } else {
190 for row in &data {
191 for c in 0..cols {
192 let v = row.get(c).cloned().unwrap_or(LiteralValue::Empty);
193 if include_cell(&v, ignore) {
194 flat.push(v);
195 }
196 }
197 }
198 }
199
200 if flat.is_empty() {
201 return Err(ExcelError::new(ExcelErrorKind::Calc)
202 .with_message("TOCOL/TOROW returned an empty array"));
203 }
204 Ok(flat)
205}
206
207impl Function for ToColFn {
218 func_caps!(PURE);
219 fn name(&self) -> &'static str {
220 "TOCOL"
221 }
222 fn min_args(&self) -> usize {
223 1
224 }
225 fn variadic(&self) -> bool {
226 true
227 }
228 fn arg_schema(&self) -> &'static [ArgSchema] {
229 schema()
230 }
231 fn eval<'a, 'b, 'c>(
232 &self,
233 args: &'c [ArgumentHandle<'a, 'b>],
234 ctx: &dyn FunctionContext<'b>,
235 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
236 match flatten_array(args) {
237 Ok(flat) => Ok(collapse_if_scalar(
238 flat.into_iter().map(|v| vec![v]).collect(),
239 ctx.date_system(),
240 )),
241 Err(e) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
242 }
243 }
244}
245
246impl Function for ToRowFn {
257 func_caps!(PURE);
258 fn name(&self) -> &'static str {
259 "TOROW"
260 }
261 fn min_args(&self) -> usize {
262 1
263 }
264 fn variadic(&self) -> bool {
265 true
266 }
267 fn arg_schema(&self) -> &'static [ArgSchema] {
268 schema()
269 }
270 fn eval<'a, 'b, 'c>(
271 &self,
272 args: &'c [ArgumentHandle<'a, 'b>],
273 ctx: &dyn FunctionContext<'b>,
274 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
275 match flatten_array(args) {
276 Ok(flat) => Ok(collapse_if_scalar(vec![flat], ctx.date_system())),
277 Err(e) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
278 }
279 }
280}
281
282pub fn register_builtins() {
283 use crate::function_registry::register_function;
284 use std::sync::Arc;
285
286 register_function(Arc::new(ToColFn));
287 register_function(Arc::new(ToRowFn));
288}
289
290#[cfg(test)]
291mod tests {
292 use crate::builtins::logical::{FalseFn, TrueFn};
293 use crate::test_workbook::TestWorkbook;
294 use formualizer_common::{ExcelError, ExcelErrorKind, LiteralValue};
295 use formualizer_parse::parser::parse;
296 use std::sync::Arc;
297
298 fn eval(formula: &str) -> LiteralValue {
299 let wb = TestWorkbook::new()
300 .with_function(Arc::new(super::ToColFn))
301 .with_function(Arc::new(super::ToRowFn))
302 .with_function(Arc::new(TrueFn))
303 .with_function(Arc::new(FalseFn));
304 let interp = wb.interpreter();
305 let ast = parse(formula).expect("parse");
306 interp.evaluate_ast(&ast).expect("eval").into_literal()
307 }
308
309 #[test]
310 fn tocol_flattens_rows_by_default() {
311 assert_eq!(
312 eval("=TOCOL({1,2;3,4})"),
313 LiteralValue::Array(vec![
314 vec![LiteralValue::Number(1.0)],
315 vec![LiteralValue::Number(2.0)],
316 vec![LiteralValue::Number(3.0)],
317 vec![LiteralValue::Number(4.0)],
318 ])
319 );
320 }
321
322 #[test]
323 fn torow_can_scan_by_column() {
324 assert_eq!(
325 eval("=TOROW({1,2;3,4},0,TRUE)"),
326 LiteralValue::Array(vec![vec![
327 LiteralValue::Number(1.0),
328 LiteralValue::Number(3.0),
329 LiteralValue::Number(2.0),
330 LiteralValue::Number(4.0),
331 ]])
332 );
333 }
334
335 #[test]
336 fn ignores_blanks_and_errors() {
337 let value = eval("=TOROW({1,#N/A;\"\",2},2,FALSE)");
338 assert_eq!(
339 value,
340 LiteralValue::Array(vec![vec![
341 LiteralValue::Number(1.0),
342 LiteralValue::Text(String::new()),
343 LiteralValue::Number(2.0),
344 ]])
345 );
346
347 let value = eval("=TOROW({#N/A},2)");
348 assert!(matches!(value, LiteralValue::Error(e) if e.kind == ExcelErrorKind::Calc));
349 }
350
351 #[test]
352 fn rejects_invalid_ignore_mode() {
353 let value = eval("=TOCOL({1,2},4)");
354 assert!(matches!(
355 value,
356 LiteralValue::Error(ExcelError {
357 kind: ExcelErrorKind::Value,
358 ..
359 })
360 ));
361 }
362}