Skip to main content

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