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 {
276 func_caps!(PURE);
277 fn name(&self) -> &'static str {
278 "TEXT"
279 }
280 fn min_args(&self) -> usize {
281 2
282 }
283 fn arg_schema(&self) -> &'static [ArgSchema] {
284 &ARG_ANY_ONE[..]
285 }
286 fn eval<'a, 'b, 'c>(
287 &self,
288 args: &'c [ArgumentHandle<'a, 'b>],
289 ctx: &dyn FunctionContext<'b>,
290 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
291 if args.len() != 2 {
292 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
293 ExcelError::new_value(),
294 )));
295 }
296 let val = scalar_like_value(&args[0])?;
297 if let LiteralValue::Error(e) = val {
298 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
299 }
300 let fmt = to_text(&args[1])?;
301 let num = match val {
302 LiteralValue::Number(f) => f,
303 LiteralValue::Int(i) => i as f64,
304 LiteralValue::Text(t) => match ctx.locale().parse_number_invariant(&t) {
305 Some(n) => n,
306 None => {
307 if t.chars().any(|c| c.is_ascii_digit()) {
316 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
317 ExcelError::new_value(),
318 )));
319 }
320 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Text(t)));
321 }
322 },
323 LiteralValue::Boolean(b) => {
324 if b {
325 1.0
326 } else {
327 0.0
328 }
329 }
330 LiteralValue::Empty => 0.0,
331 LiteralValue::Error(e) => {
332 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
333 }
334 _ => 0.0,
335 };
336 let out = if fmt.contains('%') {
337 format_percent(num)
338 } else if fmt.contains('#') && fmt.contains(',') {
339 format_with_thousands(num, &fmt)
341 } else if fmt.contains("0.00") {
342 format!("{num:.2}")
343 } else if fmt.contains("0") {
344 if fmt.contains(".00") {
345 format!("{num:.2}")
346 } else {
347 format_number_basic(num)
348 }
349 } else {
350 if fmt.contains("yyyy") || fmt.contains("dd") || fmt.contains("mm") {
352 format_serial_date(num, &fmt)
353 } else {
354 num.to_string()
355 }
356 };
357 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Text(out)))
358 }
359}
360
361fn format_percent(n: f64) -> String {
362 format!("{:.0}%", n * 100.0)
363}
364fn format_number_basic(n: f64) -> String {
365 if n.fract() == 0.0 {
366 format!("{n:.0}")
367 } else {
368 n.to_string()
369 }
370}
371
372fn format_with_thousands(n: f64, fmt: &str) -> String {
373 let decimal_places = if fmt.contains(".00") {
375 2
376 } else if fmt.contains(".0") {
377 1
378 } else {
379 0
380 };
381
382 let abs_n = n.abs();
383 let formatted = if decimal_places > 0 {
384 format!("{:.prec$}", abs_n, prec = decimal_places)
385 } else {
386 format!("{:.0}", abs_n)
387 };
388
389 let parts: Vec<&str> = formatted.split('.').collect();
391 let int_part = parts[0];
392 let dec_part = parts.get(1);
393
394 let int_with_commas: String = int_part
396 .chars()
397 .rev()
398 .enumerate()
399 .flat_map(|(i, c)| {
400 if i > 0 && i % 3 == 0 {
401 vec![',', c]
402 } else {
403 vec![c]
404 }
405 })
406 .collect::<String>()
407 .chars()
408 .rev()
409 .collect();
410
411 let result = if let Some(dec) = dec_part {
413 format!("{}.{}", int_with_commas, dec)
414 } else {
415 int_with_commas
416 };
417
418 if n < 0.0 {
420 format!("-{}", result)
421 } else {
422 result
423 }
424}
425
426fn format_serial_date(n: f64, fmt: &str) -> String {
428 use chrono::Datelike;
429 let days = n.trunc() as i64;
430 let base = chrono::NaiveDate::from_ymd_opt(1899, 12, 31).unwrap();
431 let date = base
432 .checked_add_signed(chrono::TimeDelta::days(days))
433 .unwrap_or(base);
434 let mut out = fmt.to_string();
435 out = out.replace("yyyy", &format!("{:04}", date.year()));
436 out = out.replace("mm", &format!("{:02}", date.month()));
437 out = out.replace("dd", &format!("{:02}", date.day()));
438 if out.contains("hh:mm") {
439 let frac = n.fract();
440 let total_minutes = (frac * 24.0 * 60.0).round() as i64;
441 let hh = (total_minutes / 60) % 24;
442 let mm = total_minutes % 60;
443 out = out.replace("hh:mm", &format!("{hh:02}:{mm:02}"));
444 }
445 out
446}
447
448pub fn register_builtins() {
449 use std::sync::Arc;
450 crate::function_registry::register_function(Arc::new(ValueFn));
451 crate::function_registry::register_function(Arc::new(NumberValueFn));
452 crate::function_registry::register_function(Arc::new(TextFn));
453}
454
455#[cfg(test)]
456mod tests {
457 use super::*;
458 use crate::test_workbook::TestWorkbook;
459 use crate::traits::ArgumentHandle;
460 use formualizer_common::{ExcelErrorKind, LiteralValue};
461 use formualizer_parse::parser::{ASTNode, ASTNodeType};
462 fn lit(v: LiteralValue) -> ASTNode {
463 ASTNode::new(ASTNodeType::Literal(v), None)
464 }
465 #[test]
466 fn value_basic() {
467 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(ValueFn));
468 let ctx = wb.interpreter();
469 let f = ctx.context.get_function("", "VALUE").unwrap();
470 let s = lit(LiteralValue::Text("12.5".into()));
471 let out = f
472 .dispatch(
473 &[ArgumentHandle::new(&s, &ctx)],
474 &ctx.function_context(None),
475 )
476 .unwrap()
477 .into_literal();
478 assert_eq!(out, LiteralValue::Number(12.5));
479 }
480
481 #[test]
482 fn value_percent_text() {
483 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(ValueFn));
484 let ctx = wb.interpreter();
485 let f = ctx.context.get_function("", "VALUE").unwrap();
486 let s = lit(LiteralValue::Text("90%".into()));
487 let out = f
488 .dispatch(
489 &[ArgumentHandle::new(&s, &ctx)],
490 &ctx.function_context(None),
491 )
492 .unwrap()
493 .into_literal();
494 assert_eq!(out, LiteralValue::Number(0.9));
495 }
496
497 #[test]
498 fn numbervalue_supports_explicit_separators_and_percent() {
499 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(NumberValueFn));
500 let ctx = wb.interpreter();
501 let f = ctx.context.get_function("", "NUMBERVALUE").unwrap();
502 let text = lit(LiteralValue::Text(" 1.234,50%% ".into()));
503 let dec = lit(LiteralValue::Text(",".into()));
504 let grp = lit(LiteralValue::Text(".".into()));
505 let out = f
506 .dispatch(
507 &[
508 ArgumentHandle::new(&text, &ctx),
509 ArgumentHandle::new(&dec, &ctx),
510 ArgumentHandle::new(&grp, &ctx),
511 ],
512 &ctx.function_context(None),
513 )
514 .unwrap()
515 .into_literal();
516 assert_eq!(out, LiteralValue::Number(0.12345));
517 }
518
519 #[test]
520 fn numbervalue_rejects_bad_separators_and_multiple_decimals() {
521 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(NumberValueFn));
522 let ctx = wb.interpreter();
523 let f = ctx.context.get_function("", "NUMBERVALUE").unwrap();
524 let text = lit(LiteralValue::Text("1.2.3".into()));
525 let out = f
526 .dispatch(
527 &[ArgumentHandle::new(&text, &ctx)],
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 let sep = lit(LiteralValue::Text(".".into()));
535 let out = f
536 .dispatch(
537 &[
538 ArgumentHandle::new(&lit(LiteralValue::Text("1.2".into())), &ctx),
539 ArgumentHandle::new(&sep, &ctx),
540 ArgumentHandle::new(&sep, &ctx),
541 ],
542 &ctx.function_context(None),
543 )
544 .unwrap()
545 .into_literal();
546 assert!(matches!(out, LiteralValue::Error(e) if e.kind == ExcelErrorKind::Value));
547 }
548
549 #[test]
550 fn text_basic_number() {
551 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(TextFn));
552 let ctx = wb.interpreter();
553 let f = ctx.context.get_function("", "TEXT").unwrap();
554 let n = lit(LiteralValue::Number(12.34));
555 let fmt = lit(LiteralValue::Text("0.00".into()));
556 let out = f
557 .dispatch(
558 &[
559 ArgumentHandle::new(&n, &ctx),
560 ArgumentHandle::new(&fmt, &ctx),
561 ],
562 &ctx.function_context(None),
563 )
564 .unwrap()
565 .into_literal();
566 assert_eq!(out, LiteralValue::Text("12.34".into()));
567 }
568
569 #[test]
570 fn text_clearly_non_numeric_text_passes_through() {
571 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(TextFn));
575 let ctx = wb.interpreter();
576 let f = ctx.context.get_function("", "TEXT").unwrap();
577 for input in ["abc", "N/A", "hello world"] {
578 let v = lit(LiteralValue::Text(input.into()));
579 let fmt = lit(LiteralValue::Text("00".into()));
580 let out = f
581 .dispatch(
582 &[
583 ArgumentHandle::new(&v, &ctx),
584 ArgumentHandle::new(&fmt, &ctx),
585 ],
586 &ctx.function_context(None),
587 )
588 .unwrap()
589 .into_literal();
590 assert_eq!(
591 out,
592 LiteralValue::Text(input.into()),
593 "TEXT({input:?},\"00\")"
594 );
595 }
596 }
597
598 #[test]
599 fn text_digit_bearing_text_still_errors() {
600 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(TextFn));
605 let ctx = wb.interpreter();
606 let f = ctx.context.get_function("", "TEXT").unwrap();
607 for input in ["3-1", "10-", "1.234,56", "$5", "1/2"] {
608 let v = lit(LiteralValue::Text(input.into()));
609 let fmt = lit(LiteralValue::Text("00".into()));
610 let out = f
611 .dispatch(
612 &[
613 ArgumentHandle::new(&v, &ctx),
614 ArgumentHandle::new(&fmt, &ctx),
615 ],
616 &ctx.function_context(None),
617 )
618 .unwrap()
619 .into_literal();
620 match out {
621 LiteralValue::Error(e) => {
622 assert_eq!(e.to_string(), "#VALUE!", "TEXT({input:?},\"00\")")
623 }
624 other => panic!("expected #VALUE! for TEXT({input:?},\"00\"), got {other:?}"),
625 }
626 }
627 }
628}