Skip to main content

formualizer_eval/builtins/text/
len_left_right.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, ExcelErrorKind, 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        crate::traits::CalcValue::Callable(_) => LiteralValue::Error(
13            ExcelError::new(ExcelErrorKind::Calc).with_message("LAMBDA value must be invoked"),
14        ),
15    })
16}
17
18#[derive(Debug)]
19pub struct LenFn;
20/// Returns the number of characters in a text value.
21///
22/// # Remarks
23/// - Counts Unicode scalar characters, not bytes.
24/// - Empty values return `0`.
25/// - Non-text values are converted to their text form before counting.
26/// - Errors are propagated unchanged.
27///
28/// # Examples
29///
30/// ```yaml,sandbox
31/// title: "Basic text length"
32/// formula: '=LEN("hello")'
33/// expected: 5
34/// ```
35///
36/// ```yaml,sandbox
37/// title: "Whitespace is counted"
38/// formula: '=LEN("a b")'
39/// expected: 3
40/// ```
41///
42/// ```yaml,docs
43/// related:
44///   - LEFT
45///   - RIGHT
46///   - MID
47/// faq:
48///   - q: "Does LEN ignore spaces?"
49///     a: "No. LEN counts spaces and other visible characters as part of the total length."
50/// ```
51/// [formualizer-docgen:schema:start]
52/// Name: LEN
53/// Type: LenFn
54/// Min args: 1
55/// Max args: 1
56/// Variadic: false
57/// Signature: LEN(arg1: any@scalar)
58/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
59/// Caps: PURE
60/// [formualizer-docgen:schema:end]
61impl Function for LenFn {
62    func_caps!(PURE);
63    fn name(&self) -> &'static str {
64        "LEN"
65    }
66    fn min_args(&self) -> usize {
67        1
68    }
69    fn arg_schema(&self) -> &'static [ArgSchema] {
70        &ARG_ANY_ONE[..]
71    }
72    fn eval<'a, 'b, 'c>(
73        &self,
74        args: &'c [ArgumentHandle<'a, 'b>],
75        _: &dyn FunctionContext<'b>,
76    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
77        let v = scalar_like_value(&args[0])?;
78        let count = match v {
79            LiteralValue::Text(s) => s.chars().count() as i64,
80            LiteralValue::Empty => 0,
81            LiteralValue::Error(e) => {
82                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
83            }
84            other => other.to_string().chars().count() as i64,
85        };
86        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(count)))
87    }
88}
89
90#[derive(Debug)]
91pub struct LeftFn;
92/// Returns the leftmost characters from a text value.
93///
94/// # Remarks
95/// - `num_chars` defaults to `1` when omitted.
96/// - Negative `num_chars` returns `#VALUE!`.
97/// - If `num_chars` exceeds length, the full text is returned.
98/// - Non-text values are coerced to text before slicing.
99///
100/// # Examples
101///
102/// ```yaml,sandbox
103/// title: "Take first two characters"
104/// formula: '=LEFT("Formualizer", 2)'
105/// expected: "Fo"
106/// ```
107///
108/// ```yaml,sandbox
109/// title: "Default count is one"
110/// formula: '=LEFT("Data")'
111/// expected: "D"
112/// ```
113///
114/// ```yaml,docs
115/// related:
116///   - RIGHT
117///   - MID
118///   - LEN
119/// faq:
120///   - q: "What if num_chars is negative?"
121///     a: "LEFT returns #VALUE! when num_chars is below zero."
122/// ```
123/// [formualizer-docgen:schema:start]
124/// Name: LEFT
125/// Type: LeftFn
126/// Min args: 1
127/// Max args: variadic
128/// Variadic: true
129/// Signature: LEFT(arg1...: any@scalar)
130/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
131/// Caps: PURE
132/// [formualizer-docgen:schema:end]
133impl Function for LeftFn {
134    func_caps!(PURE);
135    fn name(&self) -> &'static str {
136        "LEFT"
137    }
138    fn min_args(&self) -> usize {
139        1
140    }
141    fn variadic(&self) -> bool {
142        true
143    }
144    fn arg_schema(&self) -> &'static [ArgSchema] {
145        &ARG_ANY_ONE[..]
146    }
147    fn eval<'a, 'b, 'c>(
148        &self,
149        args: &'c [ArgumentHandle<'a, 'b>],
150        _: &dyn FunctionContext<'b>,
151    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
152        if args.is_empty() || args.len() > 2 {
153            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
154                ExcelError::new_value(),
155            )));
156        }
157        let s_val = scalar_like_value(&args[0])?;
158        let s = match s_val {
159            LiteralValue::Text(t) => t,
160            LiteralValue::Empty => String::new(),
161            LiteralValue::Error(e) => {
162                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
163            }
164            other => other.to_string(),
165        };
166        let n: i64 = if args.len() == 2 {
167            number_like(&args[1])?
168        } else {
169            1
170        };
171        if n < 0 {
172            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
173                ExcelError::new_value(),
174            )));
175        }
176        let chars: Vec<char> = s.chars().collect();
177        let take = (n as usize).min(chars.len());
178        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Text(
179            chars[..take].iter().collect(),
180        )))
181    }
182}
183
184#[derive(Debug)]
185pub struct RightFn;
186/// Returns the rightmost characters from a text value.
187///
188/// # Remarks
189/// - `num_chars` defaults to `1` when omitted.
190/// - Negative `num_chars` returns `#VALUE!`.
191/// - If `num_chars` exceeds length, the full text is returned.
192/// - Non-text values are coerced to text before slicing.
193///
194/// # Examples
195///
196/// ```yaml,sandbox
197/// title: "Take last three characters"
198/// formula: '=RIGHT("engine", 3)'
199/// expected: "ine"
200/// ```
201///
202/// ```yaml,sandbox
203/// title: "Default count is one"
204/// formula: '=RIGHT("abc")'
205/// expected: "c"
206/// ```
207///
208/// ```yaml,docs
209/// related:
210///   - LEFT
211///   - MID
212///   - LEN
213/// faq:
214///   - q: "If num_chars is larger than the text length, what is returned?"
215///     a: "RIGHT returns the full text when the requested count exceeds available characters."
216/// ```
217/// [formualizer-docgen:schema:start]
218/// Name: RIGHT
219/// Type: RightFn
220/// Min args: 1
221/// Max args: variadic
222/// Variadic: true
223/// Signature: RIGHT(arg1...: any@scalar)
224/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
225/// Caps: PURE
226/// [formualizer-docgen:schema:end]
227impl Function for RightFn {
228    func_caps!(PURE);
229    fn name(&self) -> &'static str {
230        "RIGHT"
231    }
232    fn min_args(&self) -> usize {
233        1
234    }
235    fn variadic(&self) -> bool {
236        true
237    }
238    fn arg_schema(&self) -> &'static [ArgSchema] {
239        &ARG_ANY_ONE[..]
240    }
241    fn eval<'a, 'b, 'c>(
242        &self,
243        args: &'c [ArgumentHandle<'a, 'b>],
244        _: &dyn FunctionContext<'b>,
245    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
246        if args.is_empty() || args.len() > 2 {
247            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
248                ExcelError::new_value(),
249            )));
250        }
251        let s_val = scalar_like_value(&args[0])?;
252        let s = match s_val {
253            LiteralValue::Text(t) => t,
254            LiteralValue::Empty => String::new(),
255            LiteralValue::Error(e) => {
256                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
257            }
258            other => other.to_string(),
259        };
260        let n: i64 = if args.len() == 2 {
261            number_like(&args[1])?
262        } else {
263            1
264        };
265        if n < 0 {
266            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
267                ExcelError::new_value(),
268            )));
269        }
270        let chars: Vec<char> = s.chars().collect();
271        let len = chars.len();
272        let start = len.saturating_sub(n as usize);
273        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Text(
274            chars[start..].iter().collect(),
275        )))
276    }
277}
278
279fn number_like<'a, 'b>(arg: &ArgumentHandle<'a, 'b>) -> Result<i64, ExcelError> {
280    let v = scalar_like_value(arg)?;
281    Ok(match v {
282        LiteralValue::Int(i) => i,
283        LiteralValue::Number(f) => f as i64,
284        LiteralValue::Empty => 0,
285        LiteralValue::Text(t) => t.parse::<i64>().unwrap_or(0),
286        LiteralValue::Boolean(b) => {
287            if b {
288                1
289            } else {
290                0
291            }
292        }
293        LiteralValue::Error(e) => return Err(e),
294        other => other.to_string().parse::<i64>().unwrap_or(0),
295    })
296}
297
298pub fn register_builtins() {
299    use std::sync::Arc;
300    crate::function_registry::register_function(Arc::new(LenFn));
301    crate::function_registry::register_function(Arc::new(LeftFn));
302    crate::function_registry::register_function(Arc::new(RightFn));
303}
304
305#[cfg(test)]
306mod tests {
307    use super::*;
308    use crate::test_workbook::TestWorkbook;
309    use crate::traits::ArgumentHandle;
310    use formualizer_common::LiteralValue;
311    use formualizer_parse::parser::{ASTNode, ASTNodeType};
312    fn lit(v: LiteralValue) -> ASTNode {
313        ASTNode::new(ASTNodeType::Literal(v), None)
314    }
315    #[test]
316    fn len_basic() {
317        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(LenFn));
318        let ctx = wb.interpreter();
319        let f = ctx.context.get_function("", "LEN").unwrap();
320        let s = lit(LiteralValue::Text("abc".into()));
321        let out = f
322            .dispatch(
323                &[ArgumentHandle::new(&s, &ctx)],
324                &ctx.function_context(None),
325            )
326            .unwrap();
327        assert_eq!(out.into_literal(), LiteralValue::Int(3));
328    }
329    #[test]
330    fn left_right() {
331        let wb = TestWorkbook::new()
332            .with_function(std::sync::Arc::new(LeftFn))
333            .with_function(std::sync::Arc::new(RightFn));
334        let ctx = wb.interpreter();
335        let l = ctx.context.get_function("", "LEFT").unwrap();
336        let r = ctx.context.get_function("", "RIGHT").unwrap();
337        let s = lit(LiteralValue::Text("hello".into()));
338        let n = lit(LiteralValue::Int(2));
339        assert_eq!(
340            l.dispatch(
341                &[ArgumentHandle::new(&s, &ctx), ArgumentHandle::new(&n, &ctx)],
342                &ctx.function_context(None)
343            )
344            .unwrap()
345            .into_literal(),
346            LiteralValue::Text("he".into())
347        );
348        assert_eq!(
349            r.dispatch(
350                &[ArgumentHandle::new(&s, &ctx), ArgumentHandle::new(&n, &ctx)],
351                &ctx.function_context(None)
352            )
353            .unwrap()
354            .into_literal(),
355            LiteralValue::Text("lo".into())
356        );
357    }
358}