formualizer_eval/builtins/
info.rs

1use crate::args::ArgSchema;
2use crate::function::Function;
3use crate::traits::{ArgumentHandle, FunctionContext};
4use formualizer_common::{ExcelError, ExcelErrorKind, LiteralValue};
5use formualizer_macros::func_caps;
6
7use super::utils::ARG_ANY_ONE;
8
9/*
10Sprint 9 – Info / Error Introspection Functions
11
12Implemented:
13  ISNUMBER, ISTEXT, ISLOGICAL, ISBLANK, ISERROR, ISERR, ISNA, ISFORMULA, TYPE,
14  NA, N, T
15
16Excel semantic notes (baseline):
17  - ISNUMBER returns TRUE for numeric types (Int, Number) and also Date/DateTime/Time/Duration
18    because Excel stores these as serial numbers. (If this diverges from desired behavior,
19    adjust by removing temporal variants.)
20  - ISBLANK is TRUE only for truly empty cells (LiteralValue::Empty), NOT for empty string "".
21  - ISERROR matches all error kinds; ISERR excludes #N/A.
22  - TYPE codes (Excel): 1 Number, 2 Text, 4 Logical, 16 Error, 64 Array. Blank coerces to 1.
23    Date/DateTime/Time/Duration mapped to 1 (numeric) for now.
24  - NA() returns the canonical #N/A error.
25  - N(value) coercion (Excel): number -> itself; date/time -> serial; TRUE->1, FALSE->0; text->0;
26    error -> propagates error; empty -> 0; other (array) -> first element via implicit (TODO) currently returns 0 with TODO flag.
27  - T(value): if text -> text; if error -> error; else -> empty text "".
28  - ISFORMULA requires formula provenance metadata (not yet tracked). Returns FALSE always (unless
29    we detect a formula node later). Marked TODO.
30
31TODO(excel-nuance): Implement implicit intersection for N() over arrays if/when model finalised.
32TODO(excel-nuance): Track formula provenance to support ISFORMULA.
33*/
34
35#[derive(Debug)]
36pub struct IsNumberFn;
37impl Function for IsNumberFn {
38    func_caps!(PURE);
39    fn name(&self) -> &'static str {
40        "ISNUMBER"
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_scalar<'a, 'b>(
49        &self,
50        args: &'a [ArgumentHandle<'a, 'b>],
51        _ctx: &dyn FunctionContext,
52    ) -> Result<LiteralValue, ExcelError> {
53        if args.len() != 1 {
54            return Ok(LiteralValue::Error(ExcelError::new_value()));
55        }
56        let v = args[0].value()?;
57        let is_num = matches!(
58            v.as_ref(),
59            LiteralValue::Int(_)
60                | LiteralValue::Number(_)
61                | LiteralValue::Date(_)
62                | LiteralValue::DateTime(_)
63                | LiteralValue::Time(_)
64                | LiteralValue::Duration(_)
65        );
66        Ok(LiteralValue::Boolean(is_num))
67    }
68}
69
70#[derive(Debug)]
71pub struct IsTextFn;
72impl Function for IsTextFn {
73    func_caps!(PURE);
74    fn name(&self) -> &'static str {
75        "ISTEXT"
76    }
77    fn min_args(&self) -> usize {
78        1
79    }
80    fn arg_schema(&self) -> &'static [ArgSchema] {
81        &ARG_ANY_ONE[..]
82    }
83    fn eval_scalar<'a, 'b>(
84        &self,
85        args: &'a [ArgumentHandle<'a, 'b>],
86        _ctx: &dyn FunctionContext,
87    ) -> Result<LiteralValue, ExcelError> {
88        if args.len() != 1 {
89            return Ok(LiteralValue::Error(ExcelError::new_value()));
90        }
91        let v = args[0].value()?;
92        Ok(LiteralValue::Boolean(matches!(
93            v.as_ref(),
94            LiteralValue::Text(_)
95        )))
96    }
97}
98
99#[derive(Debug)]
100pub struct IsLogicalFn;
101impl Function for IsLogicalFn {
102    func_caps!(PURE);
103    fn name(&self) -> &'static str {
104        "ISLOGICAL"
105    }
106    fn min_args(&self) -> usize {
107        1
108    }
109    fn arg_schema(&self) -> &'static [ArgSchema] {
110        &ARG_ANY_ONE[..]
111    }
112    fn eval_scalar<'a, 'b>(
113        &self,
114        args: &'a [ArgumentHandle<'a, 'b>],
115        _ctx: &dyn FunctionContext,
116    ) -> Result<LiteralValue, ExcelError> {
117        if args.len() != 1 {
118            return Ok(LiteralValue::Error(ExcelError::new_value()));
119        }
120        let v = args[0].value()?;
121        Ok(LiteralValue::Boolean(matches!(
122            v.as_ref(),
123            LiteralValue::Boolean(_)
124        )))
125    }
126}
127
128#[derive(Debug)]
129pub struct IsBlankFn;
130impl Function for IsBlankFn {
131    func_caps!(PURE);
132    fn name(&self) -> &'static str {
133        "ISBLANK"
134    }
135    fn min_args(&self) -> usize {
136        1
137    }
138    fn arg_schema(&self) -> &'static [ArgSchema] {
139        &ARG_ANY_ONE[..]
140    }
141    fn eval_scalar<'a, 'b>(
142        &self,
143        args: &'a [ArgumentHandle<'a, 'b>],
144        _ctx: &dyn FunctionContext,
145    ) -> Result<LiteralValue, ExcelError> {
146        if args.len() != 1 {
147            return Ok(LiteralValue::Error(ExcelError::new_value()));
148        }
149        let v = args[0].value()?;
150        Ok(LiteralValue::Boolean(matches!(
151            v.as_ref(),
152            LiteralValue::Empty
153        )))
154    }
155}
156
157#[derive(Debug)]
158pub struct IsErrorFn; // TRUE for any error (#N/A included)
159impl Function for IsErrorFn {
160    func_caps!(PURE);
161    fn name(&self) -> &'static str {
162        "ISERROR"
163    }
164    fn min_args(&self) -> usize {
165        1
166    }
167    fn arg_schema(&self) -> &'static [ArgSchema] {
168        &ARG_ANY_ONE[..]
169    }
170    fn eval_scalar<'a, 'b>(
171        &self,
172        args: &'a [ArgumentHandle<'a, 'b>],
173        _ctx: &dyn FunctionContext,
174    ) -> Result<LiteralValue, ExcelError> {
175        if args.len() != 1 {
176            return Ok(LiteralValue::Error(ExcelError::new_value()));
177        }
178        let v = args[0].value()?;
179        Ok(LiteralValue::Boolean(matches!(
180            v.as_ref(),
181            LiteralValue::Error(_)
182        )))
183    }
184}
185
186#[derive(Debug)]
187pub struct IsErrFn; // TRUE for any error except #N/A
188impl Function for IsErrFn {
189    func_caps!(PURE);
190    fn name(&self) -> &'static str {
191        "ISERR"
192    }
193    fn min_args(&self) -> usize {
194        1
195    }
196    fn arg_schema(&self) -> &'static [ArgSchema] {
197        &ARG_ANY_ONE[..]
198    }
199    fn eval_scalar<'a, 'b>(
200        &self,
201        args: &'a [ArgumentHandle<'a, 'b>],
202        _ctx: &dyn FunctionContext,
203    ) -> Result<LiteralValue, ExcelError> {
204        if args.len() != 1 {
205            return Ok(LiteralValue::Error(ExcelError::new_value()));
206        }
207        let v = args[0].value()?;
208        let is_err = match v.as_ref() {
209            LiteralValue::Error(e) => e.kind != ExcelErrorKind::Na,
210            _ => false,
211        };
212        Ok(LiteralValue::Boolean(is_err))
213    }
214}
215
216#[derive(Debug)]
217pub struct IsNaFn; // TRUE only for #N/A
218impl Function for IsNaFn {
219    func_caps!(PURE);
220    fn name(&self) -> &'static str {
221        "ISNA"
222    }
223    fn min_args(&self) -> usize {
224        1
225    }
226    fn arg_schema(&self) -> &'static [ArgSchema] {
227        &ARG_ANY_ONE[..]
228    }
229    fn eval_scalar<'a, 'b>(
230        &self,
231        args: &'a [ArgumentHandle<'a, 'b>],
232        _ctx: &dyn FunctionContext,
233    ) -> Result<LiteralValue, ExcelError> {
234        if args.len() != 1 {
235            return Ok(LiteralValue::Error(ExcelError::new_value()));
236        }
237        let v = args[0].value()?;
238        let is_na = matches!(v.as_ref(), LiteralValue::Error(e) if e.kind==ExcelErrorKind::Na);
239        Ok(LiteralValue::Boolean(is_na))
240    }
241}
242
243#[derive(Debug)]
244pub struct IsFormulaFn; // Requires provenance tracking (not yet) => always FALSE.
245impl Function for IsFormulaFn {
246    func_caps!(PURE);
247    fn name(&self) -> &'static str {
248        "ISFORMULA"
249    }
250    fn min_args(&self) -> usize {
251        1
252    }
253    fn arg_schema(&self) -> &'static [ArgSchema] {
254        &ARG_ANY_ONE[..]
255    }
256    fn eval_scalar<'a, 'b>(
257        &self,
258        args: &'a [ArgumentHandle<'a, 'b>],
259        _ctx: &dyn FunctionContext,
260    ) -> Result<LiteralValue, ExcelError> {
261        if args.len() != 1 {
262            return Ok(LiteralValue::Error(ExcelError::new_value()));
263        }
264        // TODO(excel-nuance): formula provenance once AST metadata is plumbed.
265        Ok(LiteralValue::Boolean(false))
266    }
267}
268
269#[derive(Debug)]
270pub struct TypeFn;
271impl Function for TypeFn {
272    func_caps!(PURE);
273    fn name(&self) -> &'static str {
274        "TYPE"
275    }
276    fn min_args(&self) -> usize {
277        1
278    }
279    fn arg_schema(&self) -> &'static [ArgSchema] {
280        &ARG_ANY_ONE[..]
281    }
282    fn eval_scalar<'a, 'b>(
283        &self,
284        args: &'a [ArgumentHandle<'a, 'b>],
285        _ctx: &dyn FunctionContext,
286    ) -> Result<LiteralValue, ExcelError> {
287        if args.len() != 1 {
288            return Ok(LiteralValue::Error(ExcelError::new_value()));
289        }
290        let v = args[0].value()?; // Propagate errors directly
291        if let LiteralValue::Error(e) = v.as_ref() {
292            return Ok(LiteralValue::Error(e.clone()));
293        }
294        let code = match v.as_ref() {
295            LiteralValue::Int(_)
296            | LiteralValue::Number(_)
297            | LiteralValue::Empty
298            | LiteralValue::Date(_)
299            | LiteralValue::DateTime(_)
300            | LiteralValue::Time(_)
301            | LiteralValue::Duration(_) => 1,
302            LiteralValue::Text(_) => 2,
303            LiteralValue::Boolean(_) => 4,
304            LiteralValue::Array(_) => 64,
305            LiteralValue::Error(_) => unreachable!(),
306            LiteralValue::Pending => 1, // treat as blank/zero numeric; may change
307        };
308        Ok(LiteralValue::Int(code))
309    }
310}
311
312#[derive(Debug)]
313pub struct NaFn; // NA() -> #N/A error
314impl Function for NaFn {
315    func_caps!(PURE);
316    fn name(&self) -> &'static str {
317        "NA"
318    }
319    fn min_args(&self) -> usize {
320        0
321    }
322    fn eval_scalar<'a, 'b>(
323        &self,
324        _args: &'a [ArgumentHandle<'a, 'b>],
325        _ctx: &dyn FunctionContext,
326    ) -> Result<LiteralValue, ExcelError> {
327        Ok(LiteralValue::Error(ExcelError::new(ExcelErrorKind::Na)))
328    }
329}
330
331#[derive(Debug)]
332pub struct NFn; // N(value)
333impl Function for NFn {
334    func_caps!(PURE);
335    fn name(&self) -> &'static str {
336        "N"
337    }
338    fn min_args(&self) -> usize {
339        1
340    }
341    fn arg_schema(&self) -> &'static [ArgSchema] {
342        &ARG_ANY_ONE[..]
343    }
344    fn eval_scalar<'a, 'b>(
345        &self,
346        args: &'a [ArgumentHandle<'a, 'b>],
347        _ctx: &dyn FunctionContext,
348    ) -> Result<LiteralValue, ExcelError> {
349        if args.len() != 1 {
350            return Ok(LiteralValue::Error(ExcelError::new_value()));
351        }
352        let v = args[0].value()?;
353        match v.as_ref() {
354            LiteralValue::Int(i) => Ok(LiteralValue::Int(*i)),
355            LiteralValue::Number(n) => Ok(LiteralValue::Number(*n)),
356            LiteralValue::Date(_)
357            | LiteralValue::DateTime(_)
358            | LiteralValue::Time(_)
359            | LiteralValue::Duration(_) => {
360                // Convert via serial number helper
361                if let Some(serial) = v.as_ref().as_serial_number() {
362                    Ok(LiteralValue::Number(serial))
363                } else {
364                    Ok(LiteralValue::Int(0))
365                }
366            }
367            LiteralValue::Boolean(b) => Ok(LiteralValue::Int(if *b { 1 } else { 0 })),
368            LiteralValue::Text(_) => Ok(LiteralValue::Int(0)),
369            LiteralValue::Empty => Ok(LiteralValue::Int(0)),
370            LiteralValue::Array(_) => {
371                // TODO(excel-nuance): implicit intersection; for now return 0
372                Ok(LiteralValue::Int(0))
373            }
374            LiteralValue::Error(e) => Ok(LiteralValue::Error(e.clone())),
375            LiteralValue::Pending => Ok(LiteralValue::Int(0)),
376        }
377    }
378}
379
380#[derive(Debug)]
381pub struct TFn; // T(value)
382impl Function for TFn {
383    func_caps!(PURE);
384    fn name(&self) -> &'static str {
385        "T"
386    }
387    fn min_args(&self) -> usize {
388        1
389    }
390    fn arg_schema(&self) -> &'static [ArgSchema] {
391        &ARG_ANY_ONE[..]
392    }
393    fn eval_scalar<'a, 'b>(
394        &self,
395        args: &'a [ArgumentHandle<'a, 'b>],
396        _ctx: &dyn FunctionContext,
397    ) -> Result<LiteralValue, ExcelError> {
398        if args.len() != 1 {
399            return Ok(LiteralValue::Error(ExcelError::new_value()));
400        }
401        let v = args[0].value()?;
402        match v.as_ref() {
403            LiteralValue::Text(s) => Ok(LiteralValue::Text(s.clone())),
404            LiteralValue::Error(e) => Ok(LiteralValue::Error(e.clone())),
405            _ => Ok(LiteralValue::Text(String::new())),
406        }
407    }
408}
409
410pub fn register_builtins() {
411    use std::sync::Arc;
412    crate::function_registry::register_function(Arc::new(IsNumberFn));
413    crate::function_registry::register_function(Arc::new(IsTextFn));
414    crate::function_registry::register_function(Arc::new(IsLogicalFn));
415    crate::function_registry::register_function(Arc::new(IsBlankFn));
416    crate::function_registry::register_function(Arc::new(IsErrorFn));
417    crate::function_registry::register_function(Arc::new(IsErrFn));
418    crate::function_registry::register_function(Arc::new(IsNaFn));
419    crate::function_registry::register_function(Arc::new(IsFormulaFn));
420    crate::function_registry::register_function(Arc::new(TypeFn));
421    crate::function_registry::register_function(Arc::new(NaFn));
422    crate::function_registry::register_function(Arc::new(NFn));
423    crate::function_registry::register_function(Arc::new(TFn));
424}
425
426#[cfg(test)]
427mod tests {
428    use super::*;
429    use crate::test_workbook::TestWorkbook;
430    use formualizer_parse::parser::{ASTNode, ASTNodeType};
431    fn interp(wb: &TestWorkbook) -> crate::interpreter::Interpreter<'_> {
432        wb.interpreter()
433    }
434
435    #[test]
436    fn isnumber_numeric_and_date() {
437        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(IsNumberFn));
438        let ctx = interp(&wb);
439        let f = ctx.context.get_function("", "ISNUMBER").unwrap();
440        let num = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(3.14)), None);
441        let date = ASTNode::new(
442            ASTNodeType::Literal(LiteralValue::Date(
443                chrono::NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
444            )),
445            None,
446        );
447        let txt = ASTNode::new(ASTNodeType::Literal(LiteralValue::Text("x".into())), None);
448        let args_num = vec![crate::traits::ArgumentHandle::new(&num, &ctx)];
449        let args_date = vec![crate::traits::ArgumentHandle::new(&date, &ctx)];
450        let args_txt = vec![crate::traits::ArgumentHandle::new(&txt, &ctx)];
451        assert_eq!(
452            f.dispatch(&args_num, &ctx.function_context(None)).unwrap(),
453            LiteralValue::Boolean(true)
454        );
455        assert_eq!(
456            f.dispatch(&args_date, &ctx.function_context(None)).unwrap(),
457            LiteralValue::Boolean(true)
458        );
459        assert_eq!(
460            f.dispatch(&args_txt, &ctx.function_context(None)).unwrap(),
461            LiteralValue::Boolean(false)
462        );
463    }
464
465    #[test]
466    fn istest_and_isblank() {
467        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(IsTextFn));
468        let ctx = interp(&wb);
469        let f = ctx.context.get_function("", "ISTEXT").unwrap();
470        let t = ASTNode::new(ASTNodeType::Literal(LiteralValue::Text("abc".into())), None);
471        let n = ASTNode::new(ASTNodeType::Literal(LiteralValue::Int(5)), None);
472        let args_t = vec![crate::traits::ArgumentHandle::new(&t, &ctx)];
473        let args_n = vec![crate::traits::ArgumentHandle::new(&n, &ctx)];
474        assert_eq!(
475            f.dispatch(&args_t, &ctx.function_context(None)).unwrap(),
476            LiteralValue::Boolean(true)
477        );
478        assert_eq!(
479            f.dispatch(&args_n, &ctx.function_context(None)).unwrap(),
480            LiteralValue::Boolean(false)
481        );
482
483        // ISBLANK
484        let wb2 = TestWorkbook::new().with_function(std::sync::Arc::new(IsBlankFn));
485        let ctx2 = interp(&wb2);
486        let f2 = ctx2.context.get_function("", "ISBLANK").unwrap();
487        let blank = ASTNode::new(ASTNodeType::Literal(LiteralValue::Empty), None);
488        let blank_args = vec![crate::traits::ArgumentHandle::new(&blank, &ctx2)];
489        assert_eq!(
490            f2.dispatch(&blank_args, &ctx2.function_context(None))
491                .unwrap(),
492            LiteralValue::Boolean(true)
493        );
494    }
495
496    #[test]
497    fn iserror_variants() {
498        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(IsErrorFn));
499        let ctx = interp(&wb);
500        let f = ctx.context.get_function("", "ISERROR").unwrap();
501        let err = ASTNode::new(
502            ASTNodeType::Literal(LiteralValue::Error(ExcelError::new(ExcelErrorKind::Div))),
503            None,
504        );
505        let ok = ASTNode::new(ASTNodeType::Literal(LiteralValue::Int(1)), None);
506        let a_err = vec![crate::traits::ArgumentHandle::new(&err, &ctx)];
507        let a_ok = vec![crate::traits::ArgumentHandle::new(&ok, &ctx)];
508        assert_eq!(
509            f.dispatch(&a_err, &ctx.function_context(None)).unwrap(),
510            LiteralValue::Boolean(true)
511        );
512        assert_eq!(
513            f.dispatch(&a_ok, &ctx.function_context(None)).unwrap(),
514            LiteralValue::Boolean(false)
515        );
516    }
517
518    #[test]
519    fn type_codes_basic() {
520        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(TypeFn));
521        let ctx = interp(&wb);
522        let f = ctx.context.get_function("", "TYPE").unwrap();
523        let v_num = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(2.0)), None);
524        let v_txt = ASTNode::new(ASTNodeType::Literal(LiteralValue::Text("hi".into())), None);
525        let v_bool = ASTNode::new(ASTNodeType::Literal(LiteralValue::Boolean(true)), None);
526        let v_err = ASTNode::new(
527            ASTNodeType::Literal(LiteralValue::Error(ExcelError::new(ExcelErrorKind::Value))),
528            None,
529        );
530        let v_arr = ASTNode::new(
531            ASTNodeType::Literal(LiteralValue::Array(vec![vec![LiteralValue::Int(1)]])),
532            None,
533        );
534        let a_num = vec![crate::traits::ArgumentHandle::new(&v_num, &ctx)];
535        let a_txt = vec![crate::traits::ArgumentHandle::new(&v_txt, &ctx)];
536        let a_bool = vec![crate::traits::ArgumentHandle::new(&v_bool, &ctx)];
537        let a_err = vec![crate::traits::ArgumentHandle::new(&v_err, &ctx)];
538        let a_arr = vec![crate::traits::ArgumentHandle::new(&v_arr, &ctx)];
539        assert_eq!(
540            f.dispatch(&a_num, &ctx.function_context(None)).unwrap(),
541            LiteralValue::Int(1)
542        );
543        assert_eq!(
544            f.dispatch(&a_txt, &ctx.function_context(None)).unwrap(),
545            LiteralValue::Int(2)
546        );
547        assert_eq!(
548            f.dispatch(&a_bool, &ctx.function_context(None)).unwrap(),
549            LiteralValue::Int(4)
550        );
551        match f.dispatch(&a_err, &ctx.function_context(None)).unwrap() {
552            LiteralValue::Error(e) => assert_eq!(e, "#VALUE!"),
553            _ => panic!(),
554        }
555        assert_eq!(
556            f.dispatch(&a_arr, &ctx.function_context(None)).unwrap(),
557            LiteralValue::Int(64)
558        );
559    }
560
561    #[test]
562    fn na_and_n_and_t() {
563        let wb = TestWorkbook::new()
564            .with_function(std::sync::Arc::new(NaFn))
565            .with_function(std::sync::Arc::new(NFn))
566            .with_function(std::sync::Arc::new(TFn));
567        let ctx = wb.interpreter();
568        // NA()
569        let na_fn = ctx.context.get_function("", "NA").unwrap();
570        match na_fn.eval_scalar(&[], &ctx.function_context(None)).unwrap() {
571            LiteralValue::Error(e) => assert_eq!(e, "#N/A"),
572            _ => panic!(),
573        }
574        // N()
575        let n_fn = ctx.context.get_function("", "N").unwrap();
576        let val = ASTNode::new(ASTNodeType::Literal(LiteralValue::Boolean(true)), None);
577        let args = vec![crate::traits::ArgumentHandle::new(&val, &ctx)];
578        assert_eq!(
579            n_fn.dispatch(&args, &ctx.function_context(None)).unwrap(),
580            LiteralValue::Int(1)
581        );
582        // T()
583        let t_fn = ctx.context.get_function("", "T").unwrap();
584        let txt = ASTNode::new(ASTNodeType::Literal(LiteralValue::Text("abc".into())), None);
585        let args_t = vec![crate::traits::ArgumentHandle::new(&txt, &ctx)];
586        assert_eq!(
587            t_fn.dispatch(&args_t, &ctx.function_context(None)).unwrap(),
588            LiteralValue::Text("abc".into())
589        );
590    }
591}