formualizer_eval/builtins/text/
value_text.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 to_text<'a, 'b>(a: &ArgumentHandle<'a, 'b>) -> Result<String, ExcelError> {
9    let v = a.value()?;
10    Ok(match v.as_ref() {
11        LiteralValue::Text(s) => s.clone(),
12        LiteralValue::Empty => String::new(),
13        LiteralValue::Boolean(b) => {
14            if *b {
15                "TRUE".into()
16            } else {
17                "FALSE".into()
18            }
19        }
20        LiteralValue::Int(i) => i.to_string(),
21        LiteralValue::Number(f) => f.to_string(),
22        LiteralValue::Error(e) => return Err(e.clone()),
23        other => other.to_string(),
24    })
25}
26
27// VALUE(text) - parse number
28#[derive(Debug)]
29pub struct ValueFn;
30impl Function for ValueFn {
31    func_caps!(PURE);
32    fn name(&self) -> &'static str {
33        "VALUE"
34    }
35    fn min_args(&self) -> usize {
36        1
37    }
38    fn arg_schema(&self) -> &'static [ArgSchema] {
39        &ARG_ANY_ONE[..]
40    }
41    fn eval_scalar<'a, 'b>(
42        &self,
43        a: &'a [ArgumentHandle<'a, 'b>],
44        _: &dyn FunctionContext,
45    ) -> Result<LiteralValue, ExcelError> {
46        let s = to_text(&a[0])?;
47        match s.trim().parse::<f64>() {
48            Ok(n) => Ok(LiteralValue::Number(n)),
49            Err(_) => Ok(LiteralValue::Error(ExcelError::new_value())),
50        }
51    }
52}
53
54// TEXT(value, format_text) - limited formatting (#,0,0.00, percent, yyyy, mm, dd, hh:mm) naive
55#[derive(Debug)]
56pub struct TextFn;
57impl Function for TextFn {
58    func_caps!(PURE);
59    fn name(&self) -> &'static str {
60        "TEXT"
61    }
62    fn min_args(&self) -> usize {
63        2
64    }
65    fn arg_schema(&self) -> &'static [ArgSchema] {
66        &ARG_ANY_ONE[..]
67    }
68    fn eval_scalar<'a, 'b>(
69        &self,
70        a: &'a [ArgumentHandle<'a, 'b>],
71        _: &dyn FunctionContext,
72    ) -> Result<LiteralValue, ExcelError> {
73        if a.len() != 2 {
74            return Ok(LiteralValue::Error(ExcelError::new_value()));
75        }
76        let val = a[0].value()?;
77        if let LiteralValue::Error(e) = val.as_ref() {
78            return Ok(LiteralValue::Error(e.clone()));
79        }
80        let fmt = to_text(&a[1])?;
81        let num = match val.as_ref() {
82            LiteralValue::Number(f) => *f,
83            LiteralValue::Int(i) => *i as f64,
84            LiteralValue::Text(t) => t.parse::<f64>().unwrap_or(0.0),
85            LiteralValue::Boolean(b) => {
86                if *b {
87                    1.0
88                } else {
89                    0.0
90                }
91            }
92            LiteralValue::Empty => 0.0,
93            _ => 0.0,
94        };
95        let out = if fmt.contains('%') {
96            format_percent(num)
97        } else if fmt.contains("0.00") {
98            format!("{num:.2}")
99        } else if fmt.contains("0") {
100            if fmt.contains(".00") {
101                format!("{num:.2}")
102            } else {
103                format_number_basic(num)
104            }
105        } else {
106            // date tokens naive from serial
107            if fmt.contains("yyyy") || fmt.contains("dd") || fmt.contains("mm") {
108                format_serial_date(num, &fmt)
109            } else {
110                num.to_string()
111            }
112        };
113        Ok(LiteralValue::Text(out))
114    }
115}
116
117fn format_percent(n: f64) -> String {
118    format!("{:.0}%", n * 100.0)
119}
120fn format_number_basic(n: f64) -> String {
121    if n.fract() == 0.0 {
122        format!("{n:.0}")
123    } else {
124        n.to_string()
125    }
126}
127
128// very naive: treat integer part as days since 1899-12-31 ignoring leap bug for now
129fn format_serial_date(n: f64, fmt: &str) -> String {
130    use chrono::Datelike;
131    let days = n.trunc() as i64;
132    let base = chrono::NaiveDate::from_ymd_opt(1899, 12, 31).unwrap();
133    let date = base
134        .checked_add_signed(chrono::TimeDelta::days(days))
135        .unwrap_or(base);
136    let mut out = fmt.to_string();
137    out = out.replace("yyyy", &format!("{:04}", date.year()));
138    out = out.replace("mm", &format!("{:02}", date.month()));
139    out = out.replace("dd", &format!("{:02}", date.day()));
140    if out.contains("hh:mm") {
141        let frac = n.fract();
142        let total_minutes = (frac * 24.0 * 60.0).round() as i64;
143        let hh = (total_minutes / 60) % 24;
144        let mm = total_minutes % 60;
145        out = out.replace("hh:mm", &format!("{hh:02}:{mm:02}"));
146    }
147    out
148}
149
150pub fn register_builtins() {
151    use std::sync::Arc;
152    crate::function_registry::register_function(Arc::new(ValueFn));
153    crate::function_registry::register_function(Arc::new(TextFn));
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159    use crate::test_workbook::TestWorkbook;
160    use crate::traits::ArgumentHandle;
161    use formualizer_common::LiteralValue;
162    use formualizer_parse::parser::{ASTNode, ASTNodeType};
163    fn lit(v: LiteralValue) -> ASTNode {
164        ASTNode::new(ASTNodeType::Literal(v), None)
165    }
166    #[test]
167    fn value_basic() {
168        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(ValueFn));
169        let ctx = wb.interpreter();
170        let f = ctx.context.get_function("", "VALUE").unwrap();
171        let s = lit(LiteralValue::Text("12.5".into()));
172        let out = f
173            .dispatch(
174                &[ArgumentHandle::new(&s, &ctx)],
175                &ctx.function_context(None),
176            )
177            .unwrap();
178        assert_eq!(out, LiteralValue::Number(12.5));
179    }
180    #[test]
181    fn text_basic_number() {
182        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(TextFn));
183        let ctx = wb.interpreter();
184        let f = ctx.context.get_function("", "TEXT").unwrap();
185        let n = lit(LiteralValue::Number(12.34));
186        let fmt = lit(LiteralValue::Text("0.00".into()));
187        let out = f
188            .dispatch(
189                &[
190                    ArgumentHandle::new(&n, &ctx),
191                    ArgumentHandle::new(&fmt, &ctx),
192                ],
193                &ctx.function_context(None),
194            )
195            .unwrap();
196        assert_eq!(out, LiteralValue::Text("12.34".into()));
197    }
198}