formualizer-eval 0.5.6

High-performance Arrow-backed Excel formula engine with dependency graph and incremental recalculation
Documentation
use super::super::utils::ARG_ANY_ONE;
use crate::args::ArgSchema;
use crate::function::Function;
use crate::traits::{ArgumentHandle, FunctionContext};
use formualizer_common::{ExcelError, ExcelErrorKind, LiteralValue};
use formualizer_macros::func_caps;

fn scalar_like_value(arg: &ArgumentHandle<'_, '_>) -> Result<LiteralValue, ExcelError> {
    Ok(match arg.value()? {
        crate::traits::CalcValue::Scalar(v) => v,
        crate::traits::CalcValue::Range(rv) => rv.get_cell(0, 0),
        crate::traits::CalcValue::Callable(_) => LiteralValue::Error(
            ExcelError::new(ExcelErrorKind::Calc).with_message("LAMBDA value must be invoked"),
        ),
    })
}

#[derive(Debug)]
pub struct LenFn;
/// Returns the number of characters in a text value.
///
/// # Remarks
/// - Counts Unicode scalar characters, not bytes.
/// - Empty values return `0`.
/// - Non-text values are converted to their text form before counting.
/// - Errors are propagated unchanged.
///
/// # Examples
///
/// ```yaml,sandbox
/// title: "Basic text length"
/// formula: '=LEN("hello")'
/// expected: 5
/// ```
///
/// ```yaml,sandbox
/// title: "Whitespace is counted"
/// formula: '=LEN("a b")'
/// expected: 3
/// ```
///
/// ```yaml,docs
/// related:
///   - LEFT
///   - RIGHT
///   - MID
/// faq:
///   - q: "Does LEN ignore spaces?"
///     a: "No. LEN counts spaces and other visible characters as part of the total length."
/// ```
/// [formualizer-docgen:schema:start]
/// Name: LEN
/// Type: LenFn
/// Min args: 1
/// Max args: 1
/// Variadic: false
/// Signature: LEN(arg1: any@scalar)
/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
/// Caps: PURE
/// [formualizer-docgen:schema:end]
impl Function for LenFn {
    func_caps!(PURE);
    fn name(&self) -> &'static str {
        "LEN"
    }
    fn min_args(&self) -> usize {
        1
    }
    fn arg_schema(&self) -> &'static [ArgSchema] {
        &ARG_ANY_ONE[..]
    }
    fn eval<'a, 'b, 'c>(
        &self,
        args: &'c [ArgumentHandle<'a, 'b>],
        _: &dyn FunctionContext<'b>,
    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
        let v = scalar_like_value(&args[0])?;
        let count = match v {
            LiteralValue::Text(s) => s.chars().count() as i64,
            LiteralValue::Empty => 0,
            LiteralValue::Error(e) => {
                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
            }
            other => other.to_string().chars().count() as i64,
        };
        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(count)))
    }
}

#[derive(Debug)]
pub struct LeftFn;
/// Returns the leftmost characters from a text value.
///
/// # Remarks
/// - `num_chars` defaults to `1` when omitted.
/// - Negative `num_chars` returns `#VALUE!`.
/// - If `num_chars` exceeds length, the full text is returned.
/// - Non-text values are coerced to text before slicing.
///
/// # Examples
///
/// ```yaml,sandbox
/// title: "Take first two characters"
/// formula: '=LEFT("Formualizer", 2)'
/// expected: "Fo"
/// ```
///
/// ```yaml,sandbox
/// title: "Default count is one"
/// formula: '=LEFT("Data")'
/// expected: "D"
/// ```
///
/// ```yaml,docs
/// related:
///   - RIGHT
///   - MID
///   - LEN
/// faq:
///   - q: "What if num_chars is negative?"
///     a: "LEFT returns #VALUE! when num_chars is below zero."
/// ```
/// [formualizer-docgen:schema:start]
/// Name: LEFT
/// Type: LeftFn
/// Min args: 1
/// Max args: variadic
/// Variadic: true
/// Signature: LEFT(arg1...: any@scalar)
/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
/// Caps: PURE
/// [formualizer-docgen:schema:end]
impl Function for LeftFn {
    func_caps!(PURE);
    fn name(&self) -> &'static str {
        "LEFT"
    }
    fn min_args(&self) -> usize {
        1
    }
    fn variadic(&self) -> bool {
        true
    }
    fn arg_schema(&self) -> &'static [ArgSchema] {
        &ARG_ANY_ONE[..]
    }
    fn eval<'a, 'b, 'c>(
        &self,
        args: &'c [ArgumentHandle<'a, 'b>],
        _: &dyn FunctionContext<'b>,
    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
        if args.is_empty() || args.len() > 2 {
            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
                ExcelError::new_value(),
            )));
        }
        let s_val = scalar_like_value(&args[0])?;
        let s = match s_val {
            LiteralValue::Text(t) => t,
            LiteralValue::Empty => String::new(),
            LiteralValue::Error(e) => {
                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
            }
            other => other.to_string(),
        };
        let n: i64 = if args.len() == 2 {
            number_like(&args[1])?
        } else {
            1
        };
        if n < 0 {
            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
                ExcelError::new_value(),
            )));
        }
        let chars: Vec<char> = s.chars().collect();
        let take = (n as usize).min(chars.len());
        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Text(
            chars[..take].iter().collect(),
        )))
    }
}

#[derive(Debug)]
pub struct RightFn;
/// Returns the rightmost characters from a text value.
///
/// # Remarks
/// - `num_chars` defaults to `1` when omitted.
/// - Negative `num_chars` returns `#VALUE!`.
/// - If `num_chars` exceeds length, the full text is returned.
/// - Non-text values are coerced to text before slicing.
///
/// # Examples
///
/// ```yaml,sandbox
/// title: "Take last three characters"
/// formula: '=RIGHT("engine", 3)'
/// expected: "ine"
/// ```
///
/// ```yaml,sandbox
/// title: "Default count is one"
/// formula: '=RIGHT("abc")'
/// expected: "c"
/// ```
///
/// ```yaml,docs
/// related:
///   - LEFT
///   - MID
///   - LEN
/// faq:
///   - q: "If num_chars is larger than the text length, what is returned?"
///     a: "RIGHT returns the full text when the requested count exceeds available characters."
/// ```
/// [formualizer-docgen:schema:start]
/// Name: RIGHT
/// Type: RightFn
/// Min args: 1
/// Max args: variadic
/// Variadic: true
/// Signature: RIGHT(arg1...: any@scalar)
/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
/// Caps: PURE
/// [formualizer-docgen:schema:end]
impl Function for RightFn {
    func_caps!(PURE);
    fn name(&self) -> &'static str {
        "RIGHT"
    }
    fn min_args(&self) -> usize {
        1
    }
    fn variadic(&self) -> bool {
        true
    }
    fn arg_schema(&self) -> &'static [ArgSchema] {
        &ARG_ANY_ONE[..]
    }
    fn eval<'a, 'b, 'c>(
        &self,
        args: &'c [ArgumentHandle<'a, 'b>],
        _: &dyn FunctionContext<'b>,
    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
        if args.is_empty() || args.len() > 2 {
            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
                ExcelError::new_value(),
            )));
        }
        let s_val = scalar_like_value(&args[0])?;
        let s = match s_val {
            LiteralValue::Text(t) => t,
            LiteralValue::Empty => String::new(),
            LiteralValue::Error(e) => {
                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
            }
            other => other.to_string(),
        };
        let n: i64 = if args.len() == 2 {
            number_like(&args[1])?
        } else {
            1
        };
        if n < 0 {
            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
                ExcelError::new_value(),
            )));
        }
        let chars: Vec<char> = s.chars().collect();
        let len = chars.len();
        let start = len.saturating_sub(n as usize);
        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Text(
            chars[start..].iter().collect(),
        )))
    }
}

fn number_like<'a, 'b>(arg: &ArgumentHandle<'a, 'b>) -> Result<i64, ExcelError> {
    let v = scalar_like_value(arg)?;
    Ok(match v {
        LiteralValue::Int(i) => i,
        LiteralValue::Number(f) => f as i64,
        LiteralValue::Empty => 0,
        LiteralValue::Text(t) => t.parse::<i64>().unwrap_or(0),
        LiteralValue::Boolean(b) => {
            if b {
                1
            } else {
                0
            }
        }
        LiteralValue::Error(e) => return Err(e),
        other => other.to_string().parse::<i64>().unwrap_or(0),
    })
}

pub fn register_builtins() {
    use std::sync::Arc;
    crate::function_registry::register_function(Arc::new(LenFn));
    crate::function_registry::register_function(Arc::new(LeftFn));
    crate::function_registry::register_function(Arc::new(RightFn));
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::test_workbook::TestWorkbook;
    use crate::traits::ArgumentHandle;
    use formualizer_common::LiteralValue;
    use formualizer_parse::parser::{ASTNode, ASTNodeType};
    fn lit(v: LiteralValue) -> ASTNode {
        ASTNode::new(ASTNodeType::Literal(v), None)
    }
    #[test]
    fn len_basic() {
        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(LenFn));
        let ctx = wb.interpreter();
        let f = ctx.context.get_function("", "LEN").unwrap();
        let s = lit(LiteralValue::Text("abc".into()));
        let out = f
            .dispatch(
                &[ArgumentHandle::new(&s, &ctx)],
                &ctx.function_context(None),
            )
            .unwrap();
        assert_eq!(out.into_literal(), LiteralValue::Int(3));
    }
    #[test]
    fn left_right() {
        let wb = TestWorkbook::new()
            .with_function(std::sync::Arc::new(LeftFn))
            .with_function(std::sync::Arc::new(RightFn));
        let ctx = wb.interpreter();
        let l = ctx.context.get_function("", "LEFT").unwrap();
        let r = ctx.context.get_function("", "RIGHT").unwrap();
        let s = lit(LiteralValue::Text("hello".into()));
        let n = lit(LiteralValue::Int(2));
        assert_eq!(
            l.dispatch(
                &[ArgumentHandle::new(&s, &ctx), ArgumentHandle::new(&n, &ctx)],
                &ctx.function_context(None)
            )
            .unwrap()
            .into_literal(),
            LiteralValue::Text("he".into())
        );
        assert_eq!(
            r.dispatch(
                &[ArgumentHandle::new(&s, &ctx), ArgumentHandle::new(&n, &ctx)],
                &ctx.function_context(None)
            )
            .unwrap()
            .into_literal(),
            LiteralValue::Text("lo".into())
        );
    }
}