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 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
15fn to_text<'a, 'b>(a: &ArgumentHandle<'a, 'b>) -> Result<String, ExcelError> {
16 let v = scalar_like_value(a)?;
17 Ok(match v {
18 LiteralValue::Text(s) => s,
19 LiteralValue::Empty => String::new(),
20 LiteralValue::Boolean(b) => {
21 if b {
22 "TRUE".into()
23 } else {
24 "FALSE".into()
25 }
26 }
27 LiteralValue::Int(i) => i.to_string(),
28 LiteralValue::Number(f) => f.to_string(),
29 LiteralValue::Error(e) => return Err(e),
30 other => other.to_string(),
31 })
32}
33
34#[derive(Debug)]
36pub struct ValueFn;
37impl Function for ValueFn {
38 func_caps!(PURE);
39 fn name(&self) -> &'static str {
40 "VALUE"
41 }
42 fn min_args(&self) -> usize {
43 1
44 }
45 fn arg_schema(&self) -> &'static [ArgSchema] {
46 &ARG_ANY_ONE[..]
47 }
48 fn eval<'a, 'b, 'c>(
49 &self,
50 args: &'c [ArgumentHandle<'a, 'b>],
51 ctx: &dyn FunctionContext<'b>,
52 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
53 let s = to_text(&args[0])?;
54 let Some(n) = ctx.locale().parse_number_invariant(&s) else {
55 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
56 ExcelError::new_value(),
57 )));
58 };
59 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(n)))
60 }
61}
62
63#[derive(Debug)]
65pub struct TextFn;
66impl Function for TextFn {
67 func_caps!(PURE);
68 fn name(&self) -> &'static str {
69 "TEXT"
70 }
71 fn min_args(&self) -> usize {
72 2
73 }
74 fn arg_schema(&self) -> &'static [ArgSchema] {
75 &ARG_ANY_ONE[..]
76 }
77 fn eval<'a, 'b, 'c>(
78 &self,
79 args: &'c [ArgumentHandle<'a, 'b>],
80 ctx: &dyn FunctionContext<'b>,
81 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
82 if args.len() != 2 {
83 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
84 ExcelError::new_value(),
85 )));
86 }
87 let val = scalar_like_value(&args[0])?;
88 if let LiteralValue::Error(e) = val {
89 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
90 }
91 let fmt = to_text(&args[1])?;
92 let num = match val {
93 LiteralValue::Number(f) => f,
94 LiteralValue::Int(i) => i as f64,
95 LiteralValue::Text(t) => {
96 let Some(n) = ctx.locale().parse_number_invariant(&t) else {
97 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
98 ExcelError::new_value(),
99 )));
100 };
101 n
102 }
103 LiteralValue::Boolean(b) => {
104 if b {
105 1.0
106 } else {
107 0.0
108 }
109 }
110 LiteralValue::Empty => 0.0,
111 LiteralValue::Error(e) => {
112 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
113 }
114 _ => 0.0,
115 };
116 let out = if fmt.contains('%') {
117 format_percent(num)
118 } else if fmt.contains('#') && fmt.contains(',') {
119 format_with_thousands(num, &fmt)
121 } else if fmt.contains("0.00") {
122 format!("{num:.2}")
123 } else if fmt.contains("0") {
124 if fmt.contains(".00") {
125 format!("{num:.2}")
126 } else {
127 format_number_basic(num)
128 }
129 } else {
130 if fmt.contains("yyyy") || fmt.contains("dd") || fmt.contains("mm") {
132 format_serial_date(num, &fmt)
133 } else {
134 num.to_string()
135 }
136 };
137 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Text(out)))
138 }
139}
140
141fn format_percent(n: f64) -> String {
142 format!("{:.0}%", n * 100.0)
143}
144fn format_number_basic(n: f64) -> String {
145 if n.fract() == 0.0 {
146 format!("{n:.0}")
147 } else {
148 n.to_string()
149 }
150}
151
152fn format_with_thousands(n: f64, fmt: &str) -> String {
153 let decimal_places = if fmt.contains(".00") {
155 2
156 } else if fmt.contains(".0") {
157 1
158 } else {
159 0
160 };
161
162 let abs_n = n.abs();
163 let formatted = if decimal_places > 0 {
164 format!("{:.prec$}", abs_n, prec = decimal_places)
165 } else {
166 format!("{:.0}", abs_n)
167 };
168
169 let parts: Vec<&str> = formatted.split('.').collect();
171 let int_part = parts[0];
172 let dec_part = parts.get(1);
173
174 let int_with_commas: String = int_part
176 .chars()
177 .rev()
178 .enumerate()
179 .flat_map(|(i, c)| {
180 if i > 0 && i % 3 == 0 {
181 vec![',', c]
182 } else {
183 vec![c]
184 }
185 })
186 .collect::<String>()
187 .chars()
188 .rev()
189 .collect();
190
191 let result = if let Some(dec) = dec_part {
193 format!("{}.{}", int_with_commas, dec)
194 } else {
195 int_with_commas
196 };
197
198 if n < 0.0 {
200 format!("-{}", result)
201 } else {
202 result
203 }
204}
205
206fn format_serial_date(n: f64, fmt: &str) -> String {
208 use chrono::Datelike;
209 let days = n.trunc() as i64;
210 let base = chrono::NaiveDate::from_ymd_opt(1899, 12, 31).unwrap();
211 let date = base
212 .checked_add_signed(chrono::TimeDelta::days(days))
213 .unwrap_or(base);
214 let mut out = fmt.to_string();
215 out = out.replace("yyyy", &format!("{:04}", date.year()));
216 out = out.replace("mm", &format!("{:02}", date.month()));
217 out = out.replace("dd", &format!("{:02}", date.day()));
218 if out.contains("hh:mm") {
219 let frac = n.fract();
220 let total_minutes = (frac * 24.0 * 60.0).round() as i64;
221 let hh = (total_minutes / 60) % 24;
222 let mm = total_minutes % 60;
223 out = out.replace("hh:mm", &format!("{hh:02}:{mm:02}"));
224 }
225 out
226}
227
228pub fn register_builtins() {
229 use std::sync::Arc;
230 crate::function_registry::register_function(Arc::new(ValueFn));
231 crate::function_registry::register_function(Arc::new(TextFn));
232}
233
234#[cfg(test)]
235mod tests {
236 use super::*;
237 use crate::test_workbook::TestWorkbook;
238 use crate::traits::ArgumentHandle;
239 use formualizer_common::LiteralValue;
240 use formualizer_parse::parser::{ASTNode, ASTNodeType};
241 fn lit(v: LiteralValue) -> ASTNode {
242 ASTNode::new(ASTNodeType::Literal(v), None)
243 }
244 #[test]
245 fn value_basic() {
246 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(ValueFn));
247 let ctx = wb.interpreter();
248 let f = ctx.context.get_function("", "VALUE").unwrap();
249 let s = lit(LiteralValue::Text("12.5".into()));
250 let out = f
251 .dispatch(
252 &[ArgumentHandle::new(&s, &ctx)],
253 &ctx.function_context(None),
254 )
255 .unwrap()
256 .into_literal();
257 assert_eq!(out, LiteralValue::Number(12.5));
258 }
259 #[test]
260 fn text_basic_number() {
261 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(TextFn));
262 let ctx = wb.interpreter();
263 let f = ctx.context.get_function("", "TEXT").unwrap();
264 let n = lit(LiteralValue::Number(12.34));
265 let fmt = lit(LiteralValue::Text("0.00".into()));
266 let out = f
267 .dispatch(
268 &[
269 ArgumentHandle::new(&n, &ctx),
270 ArgumentHandle::new(&fmt, &ctx),
271 ],
272 &ctx.function_context(None),
273 )
274 .unwrap()
275 .into_literal();
276 assert_eq!(out, LiteralValue::Text("12.34".into()));
277 }
278}