Skip to main content

formualizer_eval/builtins/text/
trim_case_concat.rs

1use super::super::utils::ARG_ANY_ONE;
2use crate::args::ArgSchema;
3use crate::function::Function;
4use crate::traits::{ArgumentHandle, FunctionContext};
5use formualizer_common::{ExcelError, LiteralValue};
6use formualizer_macros::func_caps;
7
8fn scalar_like_value(arg: &ArgumentHandle<'_, '_>) -> Result<LiteralValue, ExcelError> {
9    Ok(match arg.value()? {
10        crate::traits::CalcValue::Scalar(v) => v,
11        crate::traits::CalcValue::Range(rv) => rv.get_cell(0, 0),
12    })
13}
14
15fn to_text<'a, 'b>(a: &ArgumentHandle<'a, 'b>) -> Result<String, ExcelError> {
16    let v = scalar_like_value(a)?;
17    Ok(match v {
18        LiteralValue::Text(s) => s,
19        LiteralValue::Empty => String::new(),
20        LiteralValue::Boolean(b) => {
21            if b {
22                "TRUE".into()
23            } else {
24                "FALSE".into()
25            }
26        }
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].into()
32            } else {
33                s
34            }
35        }
36        LiteralValue::Error(e) => return Err(e),
37        other => other.to_string(),
38    })
39}
40
41#[derive(Debug)]
42pub struct TrimFn;
43impl Function for TrimFn {
44    func_caps!(PURE);
45    fn name(&self) -> &'static str {
46        "TRIM"
47    }
48    fn min_args(&self) -> usize {
49        1
50    }
51    fn arg_schema(&self) -> &'static [ArgSchema] {
52        &ARG_ANY_ONE[..]
53    }
54    fn eval<'a, 'b, 'c>(
55        &self,
56        args: &'c [ArgumentHandle<'a, 'b>],
57        _: &dyn FunctionContext<'b>,
58    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
59        let s = to_text(&args[0])?;
60        let mut out = String::new();
61        let mut prev_space = false;
62        for ch in s.chars() {
63            if ch.is_whitespace() {
64                prev_space = true;
65            } else {
66                if prev_space && !out.is_empty() {
67                    out.push(' ');
68                }
69                out.push(ch);
70                prev_space = false;
71            }
72        }
73        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Text(
74            out.trim().into(),
75        )))
76    }
77}
78
79#[derive(Debug)]
80pub struct UpperFn;
81impl Function for UpperFn {
82    func_caps!(PURE);
83    fn name(&self) -> &'static str {
84        "UPPER"
85    }
86    fn min_args(&self) -> usize {
87        1
88    }
89    fn arg_schema(&self) -> &'static [ArgSchema] {
90        &ARG_ANY_ONE[..]
91    }
92    fn eval<'a, 'b, 'c>(
93        &self,
94        args: &'c [ArgumentHandle<'a, 'b>],
95        _: &dyn FunctionContext<'b>,
96    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
97        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Text(
98            to_text(&args[0])?.to_ascii_uppercase(),
99        )))
100    }
101}
102#[derive(Debug)]
103pub struct LowerFn;
104impl Function for LowerFn {
105    func_caps!(PURE);
106    fn name(&self) -> &'static str {
107        "LOWER"
108    }
109    fn min_args(&self) -> usize {
110        1
111    }
112    fn arg_schema(&self) -> &'static [ArgSchema] {
113        &ARG_ANY_ONE[..]
114    }
115    fn eval<'a, 'b, 'c>(
116        &self,
117        args: &'c [ArgumentHandle<'a, 'b>],
118        _: &dyn FunctionContext<'b>,
119    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
120        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Text(
121            to_text(&args[0])?.to_ascii_lowercase(),
122        )))
123    }
124}
125#[derive(Debug)]
126pub struct ProperFn;
127impl Function for ProperFn {
128    func_caps!(PURE);
129    fn name(&self) -> &'static str {
130        "PROPER"
131    }
132    fn min_args(&self) -> usize {
133        1
134    }
135    fn arg_schema(&self) -> &'static [ArgSchema] {
136        &ARG_ANY_ONE[..]
137    }
138    fn eval<'a, 'b, 'c>(
139        &self,
140        args: &'c [ArgumentHandle<'a, 'b>],
141        _: &dyn FunctionContext<'b>,
142    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
143        let s = to_text(&args[0])?;
144        let mut out = String::new();
145        let mut new_word = true;
146        for ch in s.chars() {
147            if ch.is_alphanumeric() {
148                if new_word {
149                    for c in ch.to_uppercase() {
150                        out.push(c);
151                    }
152                } else {
153                    for c in ch.to_lowercase() {
154                        out.push(c);
155                    }
156                }
157                new_word = false;
158            } else {
159                out.push(ch);
160                new_word = true;
161            }
162        }
163        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Text(out)))
164    }
165}
166
167// CONCAT(text1, text2, ...)
168#[derive(Debug)]
169pub struct ConcatFn;
170impl Function for ConcatFn {
171    func_caps!(PURE);
172    fn name(&self) -> &'static str {
173        "CONCAT"
174    }
175    fn min_args(&self) -> usize {
176        1
177    }
178    fn variadic(&self) -> bool {
179        true
180    }
181    fn arg_schema(&self) -> &'static [ArgSchema] {
182        &ARG_ANY_ONE[..]
183    }
184    fn eval<'a, 'b, 'c>(
185        &self,
186        args: &'c [ArgumentHandle<'a, 'b>],
187        _: &dyn FunctionContext<'b>,
188    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
189        let mut out = String::new();
190        for a in args {
191            out.push_str(&to_text(a)?);
192        }
193        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Text(out)))
194    }
195}
196// CONCATENATE (alias semantics)
197#[derive(Debug)]
198pub struct ConcatenateFn;
199impl Function for ConcatenateFn {
200    func_caps!(PURE);
201    fn name(&self) -> &'static str {
202        "CONCATENATE"
203    }
204    fn min_args(&self) -> usize {
205        1
206    }
207    fn variadic(&self) -> bool {
208        true
209    }
210    fn arg_schema(&self) -> &'static [ArgSchema] {
211        &ARG_ANY_ONE[..]
212    }
213    fn eval<'a, 'b, 'c>(
214        &self,
215        args: &'c [ArgumentHandle<'a, 'b>],
216        ctx: &dyn FunctionContext<'b>,
217    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
218        ConcatFn.eval(args, ctx)
219    }
220}
221
222// TEXTJOIN(delimiter, ignore_empty, text1, [text2, ...])
223#[derive(Debug)]
224pub struct TextJoinFn;
225impl Function for TextJoinFn {
226    func_caps!(PURE);
227    fn name(&self) -> &'static str {
228        "TEXTJOIN"
229    }
230    fn min_args(&self) -> usize {
231        3
232    }
233    fn variadic(&self) -> bool {
234        true
235    }
236    fn arg_schema(&self) -> &'static [ArgSchema] {
237        &ARG_ANY_ONE[..]
238    }
239    fn eval<'a, 'b, 'c>(
240        &self,
241        args: &'c [ArgumentHandle<'a, 'b>],
242        _: &dyn FunctionContext<'b>,
243    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
244        if args.len() < 3 {
245            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
246                ExcelError::new_value(),
247            )));
248        }
249
250        // Get delimiter
251        let delimiter = to_text(&args[0])?;
252
253        // Get ignore_empty flag
254        let ignore_empty = match scalar_like_value(&args[1])? {
255            LiteralValue::Boolean(b) => b,
256            LiteralValue::Int(i) => i != 0,
257            LiteralValue::Number(f) => f != 0.0,
258            LiteralValue::Text(t) => t.to_uppercase() == "TRUE",
259            LiteralValue::Empty => false,
260            LiteralValue::Error(e) => {
261                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
262            }
263            _ => false,
264        };
265
266        // Collect text values
267        let mut parts = Vec::new();
268        for arg in args.iter().skip(2) {
269            match scalar_like_value(arg)? {
270                LiteralValue::Error(e) => {
271                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
272                }
273                LiteralValue::Empty => {
274                    if !ignore_empty {
275                        parts.push(String::new());
276                    }
277                }
278                v => {
279                    let s = match v {
280                        LiteralValue::Text(t) => t,
281                        LiteralValue::Boolean(b) => {
282                            if b {
283                                "TRUE".to_string()
284                            } else {
285                                "FALSE".to_string()
286                            }
287                        }
288                        LiteralValue::Int(i) => i.to_string(),
289                        LiteralValue::Number(f) => f.to_string(),
290                        _ => v.to_string(),
291                    };
292                    if !ignore_empty || !s.is_empty() {
293                        parts.push(s);
294                    }
295                }
296            }
297        }
298
299        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Text(
300            parts.join(&delimiter),
301        )))
302    }
303}
304
305pub fn register_builtins() {
306    use std::sync::Arc;
307    crate::function_registry::register_function(Arc::new(TrimFn));
308    crate::function_registry::register_function(Arc::new(UpperFn));
309    crate::function_registry::register_function(Arc::new(LowerFn));
310    crate::function_registry::register_function(Arc::new(ProperFn));
311    crate::function_registry::register_function(Arc::new(ConcatFn));
312    crate::function_registry::register_function(Arc::new(ConcatenateFn));
313    crate::function_registry::register_function(Arc::new(TextJoinFn));
314}
315
316#[cfg(test)]
317mod tests {
318    use super::*;
319    use crate::test_workbook::TestWorkbook;
320    use crate::traits::ArgumentHandle;
321    use formualizer_common::LiteralValue;
322    use formualizer_parse::parser::{ASTNode, ASTNodeType};
323    fn lit(v: LiteralValue) -> ASTNode {
324        ASTNode::new(ASTNodeType::Literal(v), None)
325    }
326    #[test]
327    fn trim_basic() {
328        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(TrimFn));
329        let ctx = wb.interpreter();
330        let f = ctx.context.get_function("", "TRIM").unwrap();
331        let s = lit(LiteralValue::Text("  a   b  ".into()));
332        let out = f
333            .dispatch(
334                &[ArgumentHandle::new(&s, &ctx)],
335                &ctx.function_context(None),
336            )
337            .unwrap();
338        assert_eq!(out, LiteralValue::Text("a b".into()));
339    }
340    #[test]
341    fn concat_variants() {
342        let wb = TestWorkbook::new()
343            .with_function(std::sync::Arc::new(ConcatFn))
344            .with_function(std::sync::Arc::new(ConcatenateFn));
345        let ctx = wb.interpreter();
346        let c = ctx.context.get_function("", "CONCAT").unwrap();
347        let ce = ctx.context.get_function("", "CONCATENATE").unwrap();
348        let a = lit(LiteralValue::Text("a".into()));
349        let b = lit(LiteralValue::Text("b".into()));
350        assert_eq!(
351            c.dispatch(
352                &[ArgumentHandle::new(&a, &ctx), ArgumentHandle::new(&b, &ctx)],
353                &ctx.function_context(None)
354            )
355            .unwrap()
356            .into_literal(),
357            LiteralValue::Text("ab".into())
358        );
359        assert_eq!(
360            ce.dispatch(
361                &[ArgumentHandle::new(&a, &ctx), ArgumentHandle::new(&b, &ctx)],
362                &ctx.function_context(None)
363            )
364            .unwrap()
365            .into_literal(),
366            LiteralValue::Text("ab".into())
367        );
368    }
369
370    #[test]
371    fn textjoin_basic() {
372        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(TextJoinFn));
373        let ctx = wb.interpreter();
374        let f = ctx.context.get_function("", "TEXTJOIN").unwrap();
375        let delim = lit(LiteralValue::Text(",".into()));
376        let ignore = lit(LiteralValue::Boolean(true));
377        let a = lit(LiteralValue::Text("a".into()));
378        let b = lit(LiteralValue::Text("b".into()));
379        let c = lit(LiteralValue::Empty);
380        let d = lit(LiteralValue::Text("d".into()));
381        let out = f
382            .dispatch(
383                &[
384                    ArgumentHandle::new(&delim, &ctx),
385                    ArgumentHandle::new(&ignore, &ctx),
386                    ArgumentHandle::new(&a, &ctx),
387                    ArgumentHandle::new(&b, &ctx),
388                    ArgumentHandle::new(&c, &ctx),
389                    ArgumentHandle::new(&d, &ctx),
390                ],
391                &ctx.function_context(None),
392            )
393            .unwrap();
394        assert_eq!(out, LiteralValue::Text("a,b,d".into()));
395    }
396
397    #[test]
398    fn textjoin_no_ignore() {
399        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(TextJoinFn));
400        let ctx = wb.interpreter();
401        let f = ctx.context.get_function("", "TEXTJOIN").unwrap();
402        let delim = lit(LiteralValue::Text("-".into()));
403        let ignore = lit(LiteralValue::Boolean(false));
404        let a = lit(LiteralValue::Text("a".into()));
405        let b = lit(LiteralValue::Empty);
406        let c = lit(LiteralValue::Text("c".into()));
407        let out = f
408            .dispatch(
409                &[
410                    ArgumentHandle::new(&delim, &ctx),
411                    ArgumentHandle::new(&ignore, &ctx),
412                    ArgumentHandle::new(&a, &ctx),
413                    ArgumentHandle::new(&b, &ctx),
414                    ArgumentHandle::new(&c, &ctx),
415                ],
416                &ctx.function_context(None),
417            )
418            .unwrap();
419        assert_eq!(out, LiteralValue::Text("a--c".into()));
420    }
421}