formualizer_eval/builtins/text/
value_text.rs1use 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#[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#[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 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
128fn 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}