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, 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
18fn to_text<'a, 'b>(a: &ArgumentHandle<'a, 'b>) -> Result<String, ExcelError> {
19 let v = scalar_like_value(a)?;
20 Ok(match v {
21 LiteralValue::Text(s) => s,
22 LiteralValue::Empty => String::new(),
23 LiteralValue::Boolean(b) => {
24 if b {
25 "TRUE".into()
26 } else {
27 "FALSE".into()
28 }
29 }
30 LiteralValue::Int(i) => i.to_string(),
31 LiteralValue::Number(f) => f.to_string(),
32 LiteralValue::Error(e) => return Err(e),
33 other => other.to_string(),
34 })
35}
36
37#[derive(Debug)]
39pub struct ValueFn;
40impl Function for ValueFn {
82 func_caps!(PURE);
83 fn name(&self) -> &'static str {
84 "VALUE"
85 }
86 fn min_args(&self) -> usize {
87 1
88 }
89 fn arg_schema(&self) -> &'static [ArgSchema] {
90 &ARG_ANY_ONE[..]
91 }
92 fn eval<'a, 'b, 'c>(
93 &self,
94 args: &'c [ArgumentHandle<'a, 'b>],
95 ctx: &dyn FunctionContext<'b>,
96 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
97 let s = to_text(&args[0])?;
98 let Some(n) = ctx.locale().parse_number_invariant(&s) else {
99 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
100 ExcelError::new_value(),
101 )));
102 };
103 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(n)))
104 }
105}
106
107#[derive(Debug)]
139pub struct NumberValueFn;
140
141impl Function for NumberValueFn {
152 func_caps!(PURE);
153 fn name(&self) -> &'static str {
154 "NUMBERVALUE"
155 }
156 fn min_args(&self) -> usize {
157 1
158 }
159 fn variadic(&self) -> bool {
160 true
161 }
162 fn arg_schema(&self) -> &'static [ArgSchema] {
163 &ARG_ANY_ONE[..]
164 }
165 fn eval<'a, 'b, 'c>(
166 &self,
167 args: &'c [ArgumentHandle<'a, 'b>],
168 _ctx: &dyn FunctionContext<'b>,
169 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
170 if args.is_empty() || args.len() > 3 {
171 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
172 ExcelError::new_value(),
173 )));
174 }
175
176 let text = to_text(&args[0])?;
177 let decimal_sep = if args.len() >= 2 {
178 to_text(&args[1])?
179 } else {
180 ".".to_string()
181 };
182 let group_sep = if args.len() >= 3 {
183 to_text(&args[2])?
184 } else {
185 ",".to_string()
186 };
187
188 if decimal_sep.is_empty() || decimal_sep == group_sep {
189 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
190 ExcelError::new_value(),
191 )));
192 }
193
194 let mut trimmed = text.trim();
195 let mut pct_count = 0u32;
196 while let Some(prefix) = trimmed.strip_suffix('%') {
197 trimmed = prefix.trim_end();
198 pct_count += 1;
199 }
200 if trimmed.is_empty() {
201 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
202 ExcelError::new_value(),
203 )));
204 }
205
206 let cleaned = trimmed.replace(&group_sep, "").replace(&decimal_sep, ".");
207 if cleaned.matches('.').count() > 1 {
208 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
209 ExcelError::new_value(),
210 )));
211 }
212
213 let Ok(mut n) = cleaned.parse::<f64>() else {
214 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
215 ExcelError::new_value(),
216 )));
217 };
218 for _ in 0..pct_count {
219 n /= 100.0;
220 }
221
222 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(n)))
223 }
224}
225
226#[derive(Debug)]
228pub struct TextFn;
229impl Function for TextFn {
273 func_caps!(PURE);
274 fn name(&self) -> &'static str {
275 "TEXT"
276 }
277 fn min_args(&self) -> usize {
278 2
279 }
280 fn arg_schema(&self) -> &'static [ArgSchema] {
281 &ARG_ANY_ONE[..]
282 }
283 fn eval<'a, 'b, 'c>(
284 &self,
285 args: &'c [ArgumentHandle<'a, 'b>],
286 ctx: &dyn FunctionContext<'b>,
287 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
288 if args.len() != 2 {
289 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
290 ExcelError::new_value(),
291 )));
292 }
293 let val = scalar_like_value(&args[0])?;
294 if let LiteralValue::Error(e) = val {
295 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
296 }
297 let fmt = to_text(&args[1])?;
298 let num = match val {
299 LiteralValue::Number(f) => f,
300 LiteralValue::Int(i) => i as f64,
301 LiteralValue::Text(t) => {
302 let Some(n) = ctx.locale().parse_number_invariant(&t) else {
303 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
304 ExcelError::new_value(),
305 )));
306 };
307 n
308 }
309 LiteralValue::Boolean(b) => {
310 if b {
311 1.0
312 } else {
313 0.0
314 }
315 }
316 LiteralValue::Empty => 0.0,
317 LiteralValue::Error(e) => {
318 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
319 }
320 _ => 0.0,
321 };
322 let out = if fmt.contains('%') {
323 format_percent(num)
324 } else if fmt.contains('#') && fmt.contains(',') {
325 format_with_thousands(num, &fmt)
327 } else if fmt.contains("0.00") {
328 format!("{num:.2}")
329 } else if fmt.contains("0") {
330 if fmt.contains(".00") {
331 format!("{num:.2}")
332 } else {
333 format_number_basic(num)
334 }
335 } else {
336 if fmt.contains("yyyy") || fmt.contains("dd") || fmt.contains("mm") {
338 format_serial_date(num, &fmt)
339 } else {
340 num.to_string()
341 }
342 };
343 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Text(out)))
344 }
345}
346
347fn format_percent(n: f64) -> String {
348 format!("{:.0}%", n * 100.0)
349}
350fn format_number_basic(n: f64) -> String {
351 if n.fract() == 0.0 {
352 format!("{n:.0}")
353 } else {
354 n.to_string()
355 }
356}
357
358fn format_with_thousands(n: f64, fmt: &str) -> String {
359 let decimal_places = if fmt.contains(".00") {
361 2
362 } else if fmt.contains(".0") {
363 1
364 } else {
365 0
366 };
367
368 let abs_n = n.abs();
369 let formatted = if decimal_places > 0 {
370 format!("{:.prec$}", abs_n, prec = decimal_places)
371 } else {
372 format!("{:.0}", abs_n)
373 };
374
375 let parts: Vec<&str> = formatted.split('.').collect();
377 let int_part = parts[0];
378 let dec_part = parts.get(1);
379
380 let int_with_commas: String = int_part
382 .chars()
383 .rev()
384 .enumerate()
385 .flat_map(|(i, c)| {
386 if i > 0 && i % 3 == 0 {
387 vec![',', c]
388 } else {
389 vec![c]
390 }
391 })
392 .collect::<String>()
393 .chars()
394 .rev()
395 .collect();
396
397 let result = if let Some(dec) = dec_part {
399 format!("{}.{}", int_with_commas, dec)
400 } else {
401 int_with_commas
402 };
403
404 if n < 0.0 {
406 format!("-{}", result)
407 } else {
408 result
409 }
410}
411
412fn format_serial_date(n: f64, fmt: &str) -> String {
414 use chrono::Datelike;
415 let days = n.trunc() as i64;
416 let base = chrono::NaiveDate::from_ymd_opt(1899, 12, 31).unwrap();
417 let date = base
418 .checked_add_signed(chrono::TimeDelta::days(days))
419 .unwrap_or(base);
420 let mut out = fmt.to_string();
421 out = out.replace("yyyy", &format!("{:04}", date.year()));
422 out = out.replace("mm", &format!("{:02}", date.month()));
423 out = out.replace("dd", &format!("{:02}", date.day()));
424 if out.contains("hh:mm") {
425 let frac = n.fract();
426 let total_minutes = (frac * 24.0 * 60.0).round() as i64;
427 let hh = (total_minutes / 60) % 24;
428 let mm = total_minutes % 60;
429 out = out.replace("hh:mm", &format!("{hh:02}:{mm:02}"));
430 }
431 out
432}
433
434pub fn register_builtins() {
435 use std::sync::Arc;
436 crate::function_registry::register_function(Arc::new(ValueFn));
437 crate::function_registry::register_function(Arc::new(NumberValueFn));
438 crate::function_registry::register_function(Arc::new(TextFn));
439}
440
441#[cfg(test)]
442mod tests {
443 use super::*;
444 use crate::test_workbook::TestWorkbook;
445 use crate::traits::ArgumentHandle;
446 use formualizer_common::{ExcelErrorKind, LiteralValue};
447 use formualizer_parse::parser::{ASTNode, ASTNodeType};
448 fn lit(v: LiteralValue) -> ASTNode {
449 ASTNode::new(ASTNodeType::Literal(v), None)
450 }
451 #[test]
452 fn value_basic() {
453 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(ValueFn));
454 let ctx = wb.interpreter();
455 let f = ctx.context.get_function("", "VALUE").unwrap();
456 let s = lit(LiteralValue::Text("12.5".into()));
457 let out = f
458 .dispatch(
459 &[ArgumentHandle::new(&s, &ctx)],
460 &ctx.function_context(None),
461 )
462 .unwrap()
463 .into_literal();
464 assert_eq!(out, LiteralValue::Number(12.5));
465 }
466
467 #[test]
468 fn value_percent_text() {
469 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(ValueFn));
470 let ctx = wb.interpreter();
471 let f = ctx.context.get_function("", "VALUE").unwrap();
472 let s = lit(LiteralValue::Text("90%".into()));
473 let out = f
474 .dispatch(
475 &[ArgumentHandle::new(&s, &ctx)],
476 &ctx.function_context(None),
477 )
478 .unwrap()
479 .into_literal();
480 assert_eq!(out, LiteralValue::Number(0.9));
481 }
482
483 #[test]
484 fn numbervalue_supports_explicit_separators_and_percent() {
485 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(NumberValueFn));
486 let ctx = wb.interpreter();
487 let f = ctx.context.get_function("", "NUMBERVALUE").unwrap();
488 let text = lit(LiteralValue::Text(" 1.234,50%% ".into()));
489 let dec = lit(LiteralValue::Text(",".into()));
490 let grp = lit(LiteralValue::Text(".".into()));
491 let out = f
492 .dispatch(
493 &[
494 ArgumentHandle::new(&text, &ctx),
495 ArgumentHandle::new(&dec, &ctx),
496 ArgumentHandle::new(&grp, &ctx),
497 ],
498 &ctx.function_context(None),
499 )
500 .unwrap()
501 .into_literal();
502 assert_eq!(out, LiteralValue::Number(0.12345));
503 }
504
505 #[test]
506 fn numbervalue_rejects_bad_separators_and_multiple_decimals() {
507 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(NumberValueFn));
508 let ctx = wb.interpreter();
509 let f = ctx.context.get_function("", "NUMBERVALUE").unwrap();
510 let text = lit(LiteralValue::Text("1.2.3".into()));
511 let out = f
512 .dispatch(
513 &[ArgumentHandle::new(&text, &ctx)],
514 &ctx.function_context(None),
515 )
516 .unwrap()
517 .into_literal();
518 assert!(matches!(out, LiteralValue::Error(e) if e.kind == ExcelErrorKind::Value));
519
520 let sep = lit(LiteralValue::Text(".".into()));
521 let out = f
522 .dispatch(
523 &[
524 ArgumentHandle::new(&lit(LiteralValue::Text("1.2".into())), &ctx),
525 ArgumentHandle::new(&sep, &ctx),
526 ArgumentHandle::new(&sep, &ctx),
527 ],
528 &ctx.function_context(None),
529 )
530 .unwrap()
531 .into_literal();
532 assert!(matches!(out, LiteralValue::Error(e) if e.kind == ExcelErrorKind::Value));
533 }
534
535 #[test]
536 fn text_basic_number() {
537 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(TextFn));
538 let ctx = wb.interpreter();
539 let f = ctx.context.get_function("", "TEXT").unwrap();
540 let n = lit(LiteralValue::Number(12.34));
541 let fmt = lit(LiteralValue::Text("0.00".into()));
542 let out = f
543 .dispatch(
544 &[
545 ArgumentHandle::new(&n, &ctx),
546 ArgumentHandle::new(&fmt, &ctx),
547 ],
548 &ctx.function_context(None),
549 )
550 .unwrap()
551 .into_literal();
552 assert_eq!(out, LiteralValue::Text("12.34".into()));
553 }
554}