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 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#[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#[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#[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 let delimiter = to_text(&args[0])?;
252
253 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 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}