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