Skip to main content

formualizer_eval/builtins/text/
array_text.rs

1//! Text array functions: TEXTSPLIT, VALUETOTEXT, ARRAYTOTEXT
2//!
3//! TEXTSPLIT: Splits text into a 2D array based on delimiters
4//! VALUETOTEXT: Converts a value to text representation
5//! ARRAYTOTEXT: Converts an array to text representation
6
7use super::super::utils::collapse_if_scalar;
8use crate::args::{ArgSchema, ShapeKind};
9use crate::function::Function;
10use crate::traits::{ArgumentHandle, CalcValue, FunctionContext};
11use formualizer_common::{ArgKind, CoercionPolicy, ExcelError, ExcelErrorKind, LiteralValue};
12use formualizer_macros::func_caps;
13
14fn scalar_like_value(arg: &ArgumentHandle<'_, '_>) -> Result<LiteralValue, ExcelError> {
15    Ok(match arg.value()? {
16        CalcValue::Scalar(v) => v,
17        CalcValue::Range(rv) => rv.get_cell(0, 0),
18    })
19}
20
21/// Coerce a LiteralValue to text
22fn coerce_text(v: &LiteralValue) -> String {
23    match v {
24        LiteralValue::Text(s) => s.clone(),
25        LiteralValue::Empty => String::new(),
26        LiteralValue::Boolean(b) => if *b { "TRUE" } else { "FALSE" }.to_string(),
27        LiteralValue::Int(i) => i.to_string(),
28        LiteralValue::Number(f) => {
29            let s = f.to_string();
30            if s.ends_with(".0") {
31                s[..s.len() - 2].to_string()
32            } else {
33                s
34            }
35        }
36        other => other.to_string(),
37    }
38}
39
40/// Get delimiters from an argument (can be single value or array)
41fn get_delimiters(arg: &ArgumentHandle<'_, '_>) -> Result<Vec<String>, ExcelError> {
42    let cv = arg.value()?;
43    match cv {
44        CalcValue::Scalar(v) => match v {
45            LiteralValue::Error(e) => Err(e),
46            LiteralValue::Array(arr) => {
47                let mut delims = Vec::new();
48                for row in arr {
49                    for cell in row {
50                        let s = coerce_text(&cell);
51                        if !s.is_empty() {
52                            delims.push(s);
53                        }
54                    }
55                }
56                Ok(delims)
57            }
58            other => {
59                let s = coerce_text(&other);
60                if s.is_empty() {
61                    Ok(vec![])
62                } else {
63                    Ok(vec![s])
64                }
65            }
66        },
67        CalcValue::Range(rv) => {
68            let mut delims = Vec::new();
69            rv.for_each_cell(&mut |cell| {
70                let s = coerce_text(cell);
71                if !s.is_empty() {
72                    delims.push(s);
73                }
74                Ok(())
75            })?;
76            Ok(delims)
77        }
78    }
79}
80
81// ============================================================================
82// TEXTSPLIT - Split text into 2D array based on delimiters
83// ============================================================================
84
85fn arg_textsplit() -> Vec<ArgSchema> {
86    vec![
87        // text
88        ArgSchema {
89            kinds: smallvec::smallvec![ArgKind::Any],
90            required: true,
91            by_ref: false,
92            shape: ShapeKind::Scalar,
93            coercion: CoercionPolicy::None,
94            max: None,
95            repeating: None,
96            default: None,
97        },
98        // col_delimiter
99        ArgSchema {
100            kinds: smallvec::smallvec![ArgKind::Any],
101            required: true,
102            by_ref: false,
103            shape: ShapeKind::Scalar,
104            coercion: CoercionPolicy::None,
105            max: None,
106            repeating: None,
107            default: None,
108        },
109        // row_delimiter (optional)
110        ArgSchema {
111            kinds: smallvec::smallvec![ArgKind::Any],
112            required: false,
113            by_ref: false,
114            shape: ShapeKind::Scalar,
115            coercion: CoercionPolicy::None,
116            max: None,
117            repeating: None,
118            default: None,
119        },
120        // ignore_empty (optional, default FALSE)
121        ArgSchema {
122            kinds: smallvec::smallvec![ArgKind::Logical],
123            required: false,
124            by_ref: false,
125            shape: ShapeKind::Scalar,
126            coercion: CoercionPolicy::Logical,
127            max: None,
128            repeating: None,
129            default: Some(LiteralValue::Boolean(false)),
130        },
131        // match_mode (optional, default 0)
132        ArgSchema {
133            kinds: smallvec::smallvec![ArgKind::Number],
134            required: false,
135            by_ref: false,
136            shape: ShapeKind::Scalar,
137            coercion: CoercionPolicy::NumberLenientText,
138            max: None,
139            repeating: None,
140            default: Some(LiteralValue::Number(0.0)),
141        },
142        // pad_with (optional, default #N/A)
143        ArgSchema {
144            kinds: smallvec::smallvec![ArgKind::Any],
145            required: false,
146            by_ref: false,
147            shape: ShapeKind::Scalar,
148            coercion: CoercionPolicy::None,
149            max: None,
150            repeating: None,
151            default: Some(LiteralValue::Error(ExcelError::new(ExcelErrorKind::Na))),
152        },
153    ]
154}
155
156/// Split text using any of the delimiters, with optional case-insensitive matching
157fn split_by_delimiters(text: &str, delimiters: &[String], case_insensitive: bool) -> Vec<String> {
158    if delimiters.is_empty() {
159        return vec![text.to_string()];
160    }
161
162    let working_text = if case_insensitive {
163        text.to_lowercase()
164    } else {
165        text.to_string()
166    };
167
168    let delims_working: Vec<String> = if case_insensitive {
169        delimiters.iter().map(|d| d.to_lowercase()).collect()
170    } else {
171        delimiters.to_vec()
172    };
173
174    let mut result = Vec::new();
175    let mut current_start = 0;
176
177    while current_start < text.len() {
178        let mut earliest_match: Option<(usize, usize)> = None; // (position, delimiter_len)
179
180        for delim in &delims_working {
181            if delim.is_empty() {
182                continue;
183            }
184            if let Some(pos) = working_text[current_start..].find(delim.as_str()) {
185                let abs_pos = current_start + pos;
186                match earliest_match {
187                    None => earliest_match = Some((abs_pos, delim.len())),
188                    Some((ep, _)) if abs_pos < ep => earliest_match = Some((abs_pos, delim.len())),
189                    _ => {}
190                }
191            }
192        }
193
194        match earliest_match {
195            Some((pos, len)) => {
196                result.push(text[current_start..pos].to_string());
197                current_start = pos + len;
198            }
199            None => {
200                result.push(text[current_start..].to_string());
201                break;
202            }
203        }
204    }
205
206    // If we ended exactly at a delimiter, add empty string at end
207    if current_start == text.len() && !text.is_empty() {
208        let ends_with_delim = delims_working.iter().any(|d| {
209            if d.is_empty() {
210                return false;
211            }
212            working_text.ends_with(d.as_str())
213        });
214        if ends_with_delim {
215            result.push(String::new());
216        }
217    }
218
219    result
220}
221
222#[derive(Debug)]
223pub struct TextSplitFn;
224
225impl Function for TextSplitFn {
226    func_caps!(PURE);
227
228    fn name(&self) -> &'static str {
229        "TEXTSPLIT"
230    }
231
232    fn min_args(&self) -> usize {
233        2
234    }
235
236    fn arg_schema(&self) -> &'static [ArgSchema] {
237        use once_cell::sync::Lazy;
238        static SCHEMA: Lazy<Vec<ArgSchema>> = Lazy::new(arg_textsplit);
239        &SCHEMA
240    }
241
242    fn eval<'a, 'b, 'c>(
243        &self,
244        args: &'c [ArgumentHandle<'a, 'b>],
245        ctx: &dyn FunctionContext<'b>,
246    ) -> Result<CalcValue<'b>, ExcelError> {
247        // Get text to split
248        let text_val = scalar_like_value(&args[0])?;
249        let text = match text_val {
250            LiteralValue::Error(e) => return Ok(CalcValue::Scalar(LiteralValue::Error(e))),
251            other => coerce_text(&other),
252        };
253
254        // Get column delimiters
255        let col_delimiters = get_delimiters(&args[1])?;
256
257        // Get optional row delimiters
258        let row_delimiters = if args.len() > 2 {
259            // Check if row_delimiter argument is provided and not omitted
260            let val = scalar_like_value(&args[2])?;
261            match val {
262                LiteralValue::Empty => vec![],
263                LiteralValue::Error(e) => return Ok(CalcValue::Scalar(LiteralValue::Error(e))),
264                _ => get_delimiters(&args[2])?,
265            }
266        } else {
267            vec![]
268        };
269
270        // Get ignore_empty (default FALSE)
271        let ignore_empty = if args.len() > 3 {
272            match scalar_like_value(&args[3])? {
273                LiteralValue::Error(e) => return Ok(CalcValue::Scalar(LiteralValue::Error(e))),
274                LiteralValue::Boolean(b) => b,
275                LiteralValue::Number(n) => n != 0.0,
276                LiteralValue::Int(i) => i != 0,
277                _ => false,
278            }
279        } else {
280            false
281        };
282
283        // Get match_mode (default 0 = case-sensitive)
284        let case_insensitive = if args.len() > 4 {
285            match scalar_like_value(&args[4])? {
286                LiteralValue::Error(e) => return Ok(CalcValue::Scalar(LiteralValue::Error(e))),
287                LiteralValue::Number(n) => n.trunc() as i32 == 1,
288                LiteralValue::Int(i) => i == 1,
289                _ => false,
290            }
291        } else {
292            false
293        };
294
295        // Get pad_with (default #N/A)
296        let pad_with = if args.len() > 5 {
297            scalar_like_value(&args[5])?
298        } else {
299            LiteralValue::Error(ExcelError::new(ExcelErrorKind::Na))
300        };
301
302        // First, split by row delimiters (if any)
303        let row_parts = if row_delimiters.is_empty() {
304            vec![text.clone()]
305        } else {
306            split_by_delimiters(&text, &row_delimiters, case_insensitive)
307        };
308
309        // Then split each row by column delimiters
310        let mut rows: Vec<Vec<LiteralValue>> = Vec::new();
311        let mut max_cols = 0;
312
313        for row_text in row_parts {
314            if ignore_empty && row_text.is_empty() {
315                continue;
316            }
317
318            let col_parts = split_by_delimiters(&row_text, &col_delimiters, case_insensitive);
319
320            let row: Vec<LiteralValue> = if ignore_empty {
321                col_parts
322                    .into_iter()
323                    .filter(|s| !s.is_empty())
324                    .map(LiteralValue::Text)
325                    .collect()
326            } else {
327                col_parts.into_iter().map(LiteralValue::Text).collect()
328            };
329
330            if !row.is_empty() {
331                max_cols = max_cols.max(row.len());
332                rows.push(row);
333            }
334        }
335
336        // Handle empty result
337        if rows.is_empty() {
338            return Ok(CalcValue::Scalar(LiteralValue::Text(String::new())));
339        }
340
341        // Pad rows to same width
342        for row in &mut rows {
343            while row.len() < max_cols {
344                row.push(pad_with.clone());
345            }
346        }
347
348        Ok(collapse_if_scalar(rows, ctx.date_system()))
349    }
350}
351
352// ============================================================================
353// VALUETOTEXT - Convert value to text representation
354// ============================================================================
355
356fn arg_valuetotext() -> Vec<ArgSchema> {
357    vec![
358        // value
359        ArgSchema {
360            kinds: smallvec::smallvec![ArgKind::Any],
361            required: true,
362            by_ref: false,
363            shape: ShapeKind::Scalar,
364            coercion: CoercionPolicy::None,
365            max: None,
366            repeating: None,
367            default: None,
368        },
369        // format (optional, default 0=concise)
370        ArgSchema {
371            kinds: smallvec::smallvec![ArgKind::Number],
372            required: false,
373            by_ref: false,
374            shape: ShapeKind::Scalar,
375            coercion: CoercionPolicy::NumberLenientText,
376            max: None,
377            repeating: None,
378            default: Some(LiteralValue::Number(0.0)),
379        },
380    ]
381}
382
383/// Convert a single value to its text representation
384fn value_to_text_repr(v: &LiteralValue, strict: bool) -> String {
385    match v {
386        LiteralValue::Text(s) => {
387            if strict {
388                format!("\"{}\"", s)
389            } else {
390                s.clone()
391            }
392        }
393        LiteralValue::Number(n) => {
394            let s = n.to_string();
395            if s.ends_with(".0") {
396                s[..s.len() - 2].to_string()
397            } else {
398                s
399            }
400        }
401        LiteralValue::Int(i) => i.to_string(),
402        LiteralValue::Boolean(b) => if *b { "TRUE" } else { "FALSE" }.to_string(),
403        LiteralValue::Empty => String::new(),
404        LiteralValue::Error(e) => e.to_string(),
405        LiteralValue::Array(arr) => {
406            // For arrays, use array syntax
407            let rows: Vec<String> = arr
408                .iter()
409                .map(|row| {
410                    row.iter()
411                        .map(|cell| value_to_text_repr(cell, strict))
412                        .collect::<Vec<_>>()
413                        .join(",")
414                })
415                .collect();
416            format!("{{{}}}", rows.join(";"))
417        }
418        LiteralValue::Date(d) => d.format("%Y-%m-%d").to_string(),
419        LiteralValue::DateTime(dt) => dt.format("%Y-%m-%d %H:%M:%S").to_string(),
420        LiteralValue::Time(t) => t.format("%H:%M:%S").to_string(),
421        LiteralValue::Duration(dur) => {
422            let total_secs = dur.num_seconds();
423            let hours = total_secs / 3600;
424            let mins = (total_secs % 3600) / 60;
425            let secs = total_secs % 60;
426            format!("{}:{:02}:{:02}", hours, mins, secs)
427        }
428        LiteralValue::Pending => String::new(),
429    }
430}
431
432#[derive(Debug)]
433pub struct ValueToTextFn;
434
435impl Function for ValueToTextFn {
436    func_caps!(PURE);
437
438    fn name(&self) -> &'static str {
439        "VALUETOTEXT"
440    }
441
442    fn min_args(&self) -> usize {
443        1
444    }
445
446    fn arg_schema(&self) -> &'static [ArgSchema] {
447        use once_cell::sync::Lazy;
448        static SCHEMA: Lazy<Vec<ArgSchema>> = Lazy::new(arg_valuetotext);
449        &SCHEMA
450    }
451
452    fn eval<'a, 'b, 'c>(
453        &self,
454        args: &'c [ArgumentHandle<'a, 'b>],
455        _ctx: &dyn FunctionContext<'b>,
456    ) -> Result<CalcValue<'b>, ExcelError> {
457        // Get value
458        let value = scalar_like_value(&args[0])?;
459
460        // Get format (0=concise, 1=strict)
461        let format = if args.len() > 1 {
462            match scalar_like_value(&args[1])? {
463                LiteralValue::Error(e) => return Ok(CalcValue::Scalar(LiteralValue::Error(e))),
464                LiteralValue::Number(n) => n.trunc() as i32,
465                LiteralValue::Int(i) => i as i32,
466                _ => 0,
467            }
468        } else {
469            0
470        };
471
472        let strict = format == 1;
473
474        // Handle error propagation for the value itself
475        if let LiteralValue::Error(e) = &value {
476            // In strict mode, errors become their text representation
477            // In concise mode, propagate the error
478            if strict {
479                return Ok(CalcValue::Scalar(LiteralValue::Text(e.to_string())));
480            } else {
481                return Ok(CalcValue::Scalar(LiteralValue::Error(e.clone())));
482            }
483        }
484
485        let result = value_to_text_repr(&value, strict);
486        Ok(CalcValue::Scalar(LiteralValue::Text(result)))
487    }
488}
489
490// ============================================================================
491// ARRAYTOTEXT - Convert array to text representation
492// ============================================================================
493
494fn arg_arraytotext() -> Vec<ArgSchema> {
495    vec![
496        // array
497        ArgSchema {
498            kinds: smallvec::smallvec![ArgKind::Any, ArgKind::Range],
499            required: true,
500            by_ref: false,
501            shape: ShapeKind::Range,
502            coercion: CoercionPolicy::None,
503            max: None,
504            repeating: None,
505            default: None,
506        },
507        // format (optional, default 0=concise)
508        ArgSchema {
509            kinds: smallvec::smallvec![ArgKind::Number],
510            required: false,
511            by_ref: false,
512            shape: ShapeKind::Scalar,
513            coercion: CoercionPolicy::NumberLenientText,
514            max: None,
515            repeating: None,
516            default: Some(LiteralValue::Number(0.0)),
517        },
518    ]
519}
520
521#[derive(Debug)]
522pub struct ArrayToTextFn;
523
524impl Function for ArrayToTextFn {
525    func_caps!(PURE);
526
527    fn name(&self) -> &'static str {
528        "ARRAYTOTEXT"
529    }
530
531    fn min_args(&self) -> usize {
532        1
533    }
534
535    fn arg_schema(&self) -> &'static [ArgSchema] {
536        use once_cell::sync::Lazy;
537        static SCHEMA: Lazy<Vec<ArgSchema>> = Lazy::new(arg_arraytotext);
538        &SCHEMA
539    }
540
541    fn eval<'a, 'b, 'c>(
542        &self,
543        args: &'c [ArgumentHandle<'a, 'b>],
544        _ctx: &dyn FunctionContext<'b>,
545    ) -> Result<CalcValue<'b>, ExcelError> {
546        // Get format (0=concise, 1=strict)
547        let format = if args.len() > 1 {
548            match scalar_like_value(&args[1])? {
549                LiteralValue::Error(e) => return Ok(CalcValue::Scalar(LiteralValue::Error(e))),
550                LiteralValue::Number(n) => n.trunc() as i32,
551                LiteralValue::Int(i) => i as i32,
552                _ => 0,
553            }
554        } else {
555            0
556        };
557
558        let strict = format == 1;
559
560        // Try to get array from argument
561        let rows: Vec<Vec<LiteralValue>> = if let Ok(rv) = args[0].range_view() {
562            let (num_rows, num_cols) = rv.dims();
563            let mut result = Vec::with_capacity(num_rows);
564            for r in 0..num_rows {
565                let mut row = Vec::with_capacity(num_cols);
566                for c in 0..num_cols {
567                    row.push(rv.get_cell(r, c));
568                }
569                result.push(row);
570            }
571            result
572        } else {
573            let cv = args[0].value()?;
574            match cv.into_literal() {
575                LiteralValue::Array(arr) => arr,
576                LiteralValue::Error(e) => return Ok(CalcValue::Scalar(LiteralValue::Error(e))),
577                other => vec![vec![other]],
578            }
579        };
580
581        let result = if strict {
582            // Strict format: {value;value;...} with rows separated by semicolons
583            // and columns by commas, with strings quoted
584            let row_strs: Vec<String> = rows
585                .iter()
586                .map(|row| {
587                    row.iter()
588                        .map(|cell| value_to_text_repr(cell, true))
589                        .collect::<Vec<_>>()
590                        .join(",")
591                })
592                .collect();
593            format!("{{{}}}", row_strs.join(";"))
594        } else {
595            // Concise format: comma-separated values (all cells flattened)
596            let all_values: Vec<String> = rows
597                .iter()
598                .flat_map(|row| row.iter().map(|cell| value_to_text_repr(cell, false)))
599                .collect();
600            all_values.join(", ")
601        };
602
603        Ok(CalcValue::Scalar(LiteralValue::Text(result)))
604    }
605}
606
607// ============================================================================
608// Registration
609// ============================================================================
610
611pub fn register_builtins() {
612    use crate::function_registry::register_function;
613    use std::sync::Arc;
614
615    register_function(Arc::new(TextSplitFn));
616    register_function(Arc::new(ValueToTextFn));
617    register_function(Arc::new(ArrayToTextFn));
618}
619
620// ============================================================================
621// Tests
622// ============================================================================
623
624#[cfg(test)]
625mod tests {
626    use super::*;
627    use crate::test_workbook::TestWorkbook;
628    use crate::traits::ArgumentHandle;
629    use formualizer_parse::parser::{ASTNode, ASTNodeType};
630    use std::sync::Arc;
631
632    fn lit(v: LiteralValue) -> ASTNode {
633        ASTNode::new(ASTNodeType::Literal(v), None)
634    }
635
636    fn interp(wb: &TestWorkbook) -> crate::interpreter::Interpreter<'_> {
637        wb.interpreter()
638    }
639
640    #[test]
641    fn test_valuetotext_concise() {
642        let wb = TestWorkbook::new().with_function(Arc::new(ValueToTextFn));
643        let ctx = interp(&wb);
644        let f = ctx.context.get_function("", "VALUETOTEXT").unwrap();
645
646        // Test number
647        let num = lit(LiteralValue::Number(123.0));
648        let args = vec![ArgumentHandle::new(&num, &ctx)];
649        match f
650            .dispatch(&args, &ctx.function_context(None))
651            .unwrap()
652            .into_literal()
653        {
654            LiteralValue::Text(s) => assert_eq!(s, "123"),
655            v => panic!("unexpected {v:?}"),
656        }
657
658        // Test text (concise = no quotes)
659        let text = lit(LiteralValue::Text("hello".to_string()));
660        let args = vec![ArgumentHandle::new(&text, &ctx)];
661        match f
662            .dispatch(&args, &ctx.function_context(None))
663            .unwrap()
664            .into_literal()
665        {
666            LiteralValue::Text(s) => assert_eq!(s, "hello"),
667            v => panic!("unexpected {v:?}"),
668        }
669    }
670
671    #[test]
672    fn test_valuetotext_strict() {
673        let wb = TestWorkbook::new().with_function(Arc::new(ValueToTextFn));
674        let ctx = interp(&wb);
675        let f = ctx.context.get_function("", "VALUETOTEXT").unwrap();
676
677        // Test text with strict format (quotes)
678        let text = lit(LiteralValue::Text("hello".to_string()));
679        let format = lit(LiteralValue::Number(1.0));
680        let args = vec![
681            ArgumentHandle::new(&text, &ctx),
682            ArgumentHandle::new(&format, &ctx),
683        ];
684        match f
685            .dispatch(&args, &ctx.function_context(None))
686            .unwrap()
687            .into_literal()
688        {
689            LiteralValue::Text(s) => assert_eq!(s, "\"hello\""),
690            v => panic!("unexpected {v:?}"),
691        }
692    }
693
694    #[test]
695    fn test_arraytotext_concise() {
696        let wb = TestWorkbook::new().with_function(Arc::new(ArrayToTextFn));
697        let ctx = interp(&wb);
698        let f = ctx.context.get_function("", "ARRAYTOTEXT").unwrap();
699
700        // Test simple array
701        let arr = lit(LiteralValue::Array(vec![vec![
702            LiteralValue::Number(1.0),
703            LiteralValue::Number(2.0),
704            LiteralValue::Number(3.0),
705        ]]));
706        let args = vec![ArgumentHandle::new(&arr, &ctx)];
707        match f
708            .dispatch(&args, &ctx.function_context(None))
709            .unwrap()
710            .into_literal()
711        {
712            LiteralValue::Text(s) => assert_eq!(s, "1, 2, 3"),
713            v => panic!("unexpected {v:?}"),
714        }
715    }
716
717    #[test]
718    fn test_arraytotext_strict() {
719        let wb = TestWorkbook::new().with_function(Arc::new(ArrayToTextFn));
720        let ctx = interp(&wb);
721        let f = ctx.context.get_function("", "ARRAYTOTEXT").unwrap();
722
723        // Test 2D array with strict format
724        let arr = lit(LiteralValue::Array(vec![
725            vec![LiteralValue::Number(1.0), LiteralValue::Number(2.0)],
726            vec![LiteralValue::Number(3.0), LiteralValue::Number(4.0)],
727        ]));
728        let format = lit(LiteralValue::Number(1.0));
729        let args = vec![
730            ArgumentHandle::new(&arr, &ctx),
731            ArgumentHandle::new(&format, &ctx),
732        ];
733        match f
734            .dispatch(&args, &ctx.function_context(None))
735            .unwrap()
736            .into_literal()
737        {
738            LiteralValue::Text(s) => assert_eq!(s, "{1,2;3,4}"),
739            v => panic!("unexpected {v:?}"),
740        }
741    }
742
743    #[test]
744    fn test_textsplit_basic() {
745        let wb = TestWorkbook::new().with_function(Arc::new(TextSplitFn));
746        let ctx = interp(&wb);
747        let f = ctx.context.get_function("", "TEXTSPLIT").unwrap();
748
749        // Test simple split
750        let text = lit(LiteralValue::Text("a,b,c".to_string()));
751        let delim = lit(LiteralValue::Text(",".to_string()));
752        let args = vec![
753            ArgumentHandle::new(&text, &ctx),
754            ArgumentHandle::new(&delim, &ctx),
755        ];
756        match f
757            .dispatch(&args, &ctx.function_context(None))
758            .unwrap()
759            .into_literal()
760        {
761            LiteralValue::Array(arr) => {
762                assert_eq!(arr.len(), 1);
763                assert_eq!(arr[0].len(), 3);
764                assert_eq!(arr[0][0], LiteralValue::Text("a".to_string()));
765                assert_eq!(arr[0][1], LiteralValue::Text("b".to_string()));
766                assert_eq!(arr[0][2], LiteralValue::Text("c".to_string()));
767            }
768            v => panic!("unexpected {v:?}"),
769        }
770    }
771
772    #[test]
773    fn test_textsplit_2d() {
774        let wb = TestWorkbook::new().with_function(Arc::new(TextSplitFn));
775        let ctx = interp(&wb);
776        let f = ctx.context.get_function("", "TEXTSPLIT").unwrap();
777
778        // Test 2D split with row and column delimiters
779        let text = lit(LiteralValue::Text("a,b;c,d".to_string()));
780        let col_delim = lit(LiteralValue::Text(",".to_string()));
781        let row_delim = lit(LiteralValue::Text(";".to_string()));
782        let args = vec![
783            ArgumentHandle::new(&text, &ctx),
784            ArgumentHandle::new(&col_delim, &ctx),
785            ArgumentHandle::new(&row_delim, &ctx),
786        ];
787        match f
788            .dispatch(&args, &ctx.function_context(None))
789            .unwrap()
790            .into_literal()
791        {
792            LiteralValue::Array(arr) => {
793                assert_eq!(arr.len(), 2);
794                assert_eq!(arr[0].len(), 2);
795                assert_eq!(arr[0][0], LiteralValue::Text("a".to_string()));
796                assert_eq!(arr[0][1], LiteralValue::Text("b".to_string()));
797                assert_eq!(arr[1][0], LiteralValue::Text("c".to_string()));
798                assert_eq!(arr[1][1], LiteralValue::Text("d".to_string()));
799            }
800            v => panic!("unexpected {v:?}"),
801        }
802    }
803
804    #[test]
805    fn test_textsplit_ignore_empty() {
806        let wb = TestWorkbook::new().with_function(Arc::new(TextSplitFn));
807        let ctx = interp(&wb);
808        let f = ctx.context.get_function("", "TEXTSPLIT").unwrap();
809
810        // Test with consecutive delimiters and ignore_empty=TRUE
811        let text = lit(LiteralValue::Text("a,,b".to_string()));
812        let delim = lit(LiteralValue::Text(",".to_string()));
813        let row_delim = lit(LiteralValue::Empty);
814        let ignore_empty = lit(LiteralValue::Boolean(true));
815        let args = vec![
816            ArgumentHandle::new(&text, &ctx),
817            ArgumentHandle::new(&delim, &ctx),
818            ArgumentHandle::new(&row_delim, &ctx),
819            ArgumentHandle::new(&ignore_empty, &ctx),
820        ];
821        match f
822            .dispatch(&args, &ctx.function_context(None))
823            .unwrap()
824            .into_literal()
825        {
826            LiteralValue::Array(arr) => {
827                assert_eq!(arr.len(), 1);
828                assert_eq!(arr[0].len(), 2);
829                assert_eq!(arr[0][0], LiteralValue::Text("a".to_string()));
830                assert_eq!(arr[0][1], LiteralValue::Text("b".to_string()));
831            }
832            v => panic!("unexpected {v:?}"),
833        }
834    }
835
836    #[test]
837    fn test_textsplit_case_insensitive() {
838        let wb = TestWorkbook::new().with_function(Arc::new(TextSplitFn));
839        let ctx = interp(&wb);
840        let f = ctx.context.get_function("", "TEXTSPLIT").unwrap();
841
842        // Test case-insensitive matching
843        let text = lit(LiteralValue::Text("aXbxc".to_string()));
844        let delim = lit(LiteralValue::Text("X".to_string()));
845        let row_delim = lit(LiteralValue::Empty);
846        let ignore_empty = lit(LiteralValue::Boolean(false));
847        let match_mode = lit(LiteralValue::Number(1.0)); // case-insensitive
848        let args = vec![
849            ArgumentHandle::new(&text, &ctx),
850            ArgumentHandle::new(&delim, &ctx),
851            ArgumentHandle::new(&row_delim, &ctx),
852            ArgumentHandle::new(&ignore_empty, &ctx),
853            ArgumentHandle::new(&match_mode, &ctx),
854        ];
855        match f
856            .dispatch(&args, &ctx.function_context(None))
857            .unwrap()
858            .into_literal()
859        {
860            LiteralValue::Array(arr) => {
861                assert_eq!(arr.len(), 1);
862                assert_eq!(arr[0].len(), 3);
863                assert_eq!(arr[0][0], LiteralValue::Text("a".to_string()));
864                assert_eq!(arr[0][1], LiteralValue::Text("b".to_string()));
865                assert_eq!(arr[0][2], LiteralValue::Text("c".to_string()));
866            }
867            v => panic!("unexpected {v:?}"),
868        }
869    }
870}