formualizer_eval/builtins/text/
trim_case_concat.rs1use 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#[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#[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#[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 let delimiter = to_text(&args[0])?;
237
238 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 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}