formualizer_eval/builtins/text/
mid_sub_replace.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
8// MID(text, start_num, num_chars)
9#[derive(Debug)]
10pub struct MidFn;
11impl Function for MidFn {
12    func_caps!(PURE);
13    fn name(&self) -> &'static str {
14        "MID"
15    }
16    fn min_args(&self) -> usize {
17        3
18    }
19    fn arg_schema(&self) -> &'static [ArgSchema] {
20        &ARG_ANY_ONE[..]
21    }
22    fn eval_scalar<'a, 'b>(
23        &self,
24        args: &'a [ArgumentHandle<'a, 'b>],
25        _: &dyn FunctionContext,
26    ) -> Result<LiteralValue, ExcelError> {
27        if args.len() != 3 {
28            return Ok(LiteralValue::Error(ExcelError::new_value()));
29        }
30        let s = to_text(&args[0])?;
31        let start = number_like(&args[1])?;
32        let count = number_like(&args[2])?;
33        if start < 1 || count < 0 {
34            return Ok(LiteralValue::Error(ExcelError::new_value()));
35        }
36        let chars: Vec<char> = s.chars().collect();
37        if (start as usize) > chars.len() {
38            return Ok(LiteralValue::Text(String::new()));
39        }
40        let end = ((start - 1) + count) as usize;
41        let end = min(end, chars.len());
42        Ok(LiteralValue::Text(
43            chars[(start as usize - 1)..end].iter().collect(),
44        ))
45    }
46}
47
48// SUBSTITUTE(text, old_text, new_text, [instance_num]) - limited semantics
49#[derive(Debug)]
50pub struct SubstituteFn;
51impl Function for SubstituteFn {
52    func_caps!(PURE);
53    fn name(&self) -> &'static str {
54        "SUBSTITUTE"
55    }
56    fn min_args(&self) -> usize {
57        3
58    }
59    fn variadic(&self) -> bool {
60        true
61    }
62    fn arg_schema(&self) -> &'static [ArgSchema] {
63        &ARG_ANY_ONE[..]
64    }
65    fn eval_scalar<'a, 'b>(
66        &self,
67        args: &'a [ArgumentHandle<'a, 'b>],
68        _: &dyn FunctionContext,
69    ) -> Result<LiteralValue, ExcelError> {
70        if args.len() < 3 || args.len() > 4 {
71            return Ok(LiteralValue::Error(ExcelError::new_value()));
72        }
73        let text = to_text(&args[0])?;
74        let old = to_text(&args[1])?;
75        let new = to_text(&args[2])?;
76        if old.is_empty() {
77            return Ok(LiteralValue::Text(text));
78        }
79        if args.len() == 4 {
80            let instance = number_like(&args[3])?;
81            if instance <= 0 {
82                return Ok(LiteralValue::Error(ExcelError::new_value()));
83            }
84            let mut idx = 0;
85            let mut count = 0;
86            let mut out = String::new();
87            while let Some(pos) = text[idx..].find(&old) {
88                out.push_str(&text[idx..idx + pos]);
89                count += 1;
90                if count == instance {
91                    out.push_str(&new);
92                    out.push_str(&text[idx + pos + old.len()..]);
93                    return Ok(LiteralValue::Text(out));
94                } else {
95                    out.push_str(&old);
96                    idx += pos + old.len();
97                }
98            }
99            Ok(LiteralValue::Text(text))
100        } else {
101            Ok(LiteralValue::Text(text.replace(&old, &new)))
102        }
103    }
104}
105
106// REPLACE(old_text, start_num, num_chars, new_text)
107#[derive(Debug)]
108pub struct ReplaceFn;
109impl Function for ReplaceFn {
110    func_caps!(PURE);
111    fn name(&self) -> &'static str {
112        "REPLACE"
113    }
114    fn min_args(&self) -> usize {
115        4
116    }
117    fn arg_schema(&self) -> &'static [ArgSchema] {
118        &ARG_ANY_ONE[..]
119    }
120    fn eval_scalar<'a, 'b>(
121        &self,
122        args: &'a [ArgumentHandle<'a, 'b>],
123        _: &dyn FunctionContext,
124    ) -> Result<LiteralValue, ExcelError> {
125        if args.len() != 4 {
126            return Ok(LiteralValue::Error(ExcelError::new_value()));
127        }
128        let text = to_text(&args[0])?;
129        let start = number_like(&args[1])?;
130        let num = number_like(&args[2])?;
131        let new = to_text(&args[3])?;
132        if start < 1 || num < 0 {
133            return Ok(LiteralValue::Error(ExcelError::new_value()));
134        }
135        let mut chars: Vec<char> = text.chars().collect();
136        let len = chars.len();
137        let start_idx = (start as usize).saturating_sub(1);
138        if start_idx > len {
139            return Ok(LiteralValue::Text(text));
140        }
141        let end_idx = (start_idx + num as usize).min(len);
142        chars.splice(start_idx..end_idx, new.chars());
143        Ok(LiteralValue::Text(chars.into_iter().collect()))
144    }
145}
146
147fn to_text<'a, 'b>(arg: &ArgumentHandle<'a, 'b>) -> Result<String, ExcelError> {
148    let v = arg.value()?;
149    Ok(match v.as_ref() {
150        LiteralValue::Text(s) => s.clone(),
151        LiteralValue::Empty => String::new(),
152        LiteralValue::Boolean(b) => {
153            if *b {
154                "TRUE".into()
155            } else {
156                "FALSE".into()
157            }
158        }
159        LiteralValue::Number(f) => {
160            let mut s = f.to_string();
161            if s.ends_with(".0") {
162                s.truncate(s.len() - 2);
163            }
164            s
165        }
166        LiteralValue::Int(i) => i.to_string(),
167        LiteralValue::Error(e) => return Err(e.clone()),
168        other => other.to_string(),
169    })
170}
171fn number_like<'a, 'b>(arg: &ArgumentHandle<'a, 'b>) -> Result<i64, ExcelError> {
172    let v = arg.value()?;
173    Ok(match v.as_ref() {
174        LiteralValue::Int(i) => *i,
175        LiteralValue::Number(f) => *f as i64,
176        LiteralValue::Text(t) => t.parse::<i64>().unwrap_or(0),
177        LiteralValue::Boolean(b) => {
178            if *b {
179                1
180            } else {
181                0
182            }
183        }
184        LiteralValue::Empty => 0,
185        LiteralValue::Error(e) => return Err(e.clone()),
186        other => other.to_string().parse::<i64>().unwrap_or(0),
187    })
188}
189
190use std::cmp::min;
191
192pub fn register_builtins() {
193    use std::sync::Arc;
194    crate::function_registry::register_function(Arc::new(MidFn));
195    crate::function_registry::register_function(Arc::new(SubstituteFn));
196    crate::function_registry::register_function(Arc::new(ReplaceFn));
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202    use crate::test_workbook::TestWorkbook;
203    use crate::traits::ArgumentHandle;
204    use formualizer_common::LiteralValue;
205    use formualizer_parse::parser::{ASTNode, ASTNodeType};
206    fn lit(v: LiteralValue) -> ASTNode {
207        ASTNode::new(ASTNodeType::Literal(v), None)
208    }
209    #[test]
210    fn mid_basic() {
211        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(MidFn));
212        let ctx = wb.interpreter();
213        let f = ctx.context.get_function("", "MID").unwrap();
214        let s = lit(LiteralValue::Text("hello".into()));
215        let start = lit(LiteralValue::Int(2));
216        let cnt = lit(LiteralValue::Int(3));
217        let out = f
218            .dispatch(
219                &[
220                    ArgumentHandle::new(&s, &ctx),
221                    ArgumentHandle::new(&start, &ctx),
222                    ArgumentHandle::new(&cnt, &ctx),
223                ],
224                &ctx.function_context(None),
225            )
226            .unwrap();
227        assert_eq!(out, LiteralValue::Text("ell".into()));
228    }
229    #[test]
230    fn substitute_all() {
231        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(SubstituteFn));
232        let ctx = wb.interpreter();
233        let f = ctx.context.get_function("", "SUBSTITUTE").unwrap();
234        let text = lit(LiteralValue::Text("a_b_a".into()));
235        let old = lit(LiteralValue::Text("_".into()));
236        let new = lit(LiteralValue::Text("-".into()));
237        let out = f
238            .dispatch(
239                &[
240                    ArgumentHandle::new(&text, &ctx),
241                    ArgumentHandle::new(&old, &ctx),
242                    ArgumentHandle::new(&new, &ctx),
243                ],
244                &ctx.function_context(None),
245            )
246            .unwrap();
247        assert_eq!(out, LiteralValue::Text("a-b-a".into()));
248    }
249    #[test]
250    fn replace_basic() {
251        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(ReplaceFn));
252        let ctx = wb.interpreter();
253        let f = ctx.context.get_function("", "REPLACE").unwrap();
254        let text = lit(LiteralValue::Text("hello".into()));
255        let start = lit(LiteralValue::Int(2));
256        let num = lit(LiteralValue::Int(2));
257        let new = lit(LiteralValue::Text("YY".into()));
258        let out = f
259            .dispatch(
260                &[
261                    ArgumentHandle::new(&text, &ctx),
262                    ArgumentHandle::new(&start, &ctx),
263                    ArgumentHandle::new(&num, &ctx),
264                    ArgumentHandle::new(&new, &ctx),
265                ],
266                &ctx.function_context(None),
267            )
268            .unwrap();
269        assert_eq!(out, LiteralValue::Text("hYYlo".into()));
270    }
271}