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(
441            ASTNodeType::Literal(LiteralValue::Number(std::f64::consts::PI)),
442            None,
443        );
444        let date = ASTNode::new(
445            ASTNodeType::Literal(LiteralValue::Date(
446                chrono::NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
447            )),
448            None,
449        );
450        let txt = ASTNode::new(ASTNodeType::Literal(LiteralValue::Text("x".into())), None);
451        let args_num = vec![crate::traits::ArgumentHandle::new(&num, &ctx)];
452        let args_date = vec![crate::traits::ArgumentHandle::new(&date, &ctx)];
453        let args_txt = vec![crate::traits::ArgumentHandle::new(&txt, &ctx)];
454        assert_eq!(
455            f.dispatch(&args_num, &ctx.function_context(None)).unwrap(),
456            LiteralValue::Boolean(true)
457        );
458        assert_eq!(
459            f.dispatch(&args_date, &ctx.function_context(None)).unwrap(),
460            LiteralValue::Boolean(true)
461        );
462        assert_eq!(
463            f.dispatch(&args_txt, &ctx.function_context(None)).unwrap(),
464            LiteralValue::Boolean(false)
465        );
466    }
467
468    #[test]
469    fn istest_and_isblank() {
470        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(IsTextFn));
471        let ctx = interp(&wb);
472        let f = ctx.context.get_function("", "ISTEXT").unwrap();
473        let t = ASTNode::new(ASTNodeType::Literal(LiteralValue::Text("abc".into())), None);
474        let n = ASTNode::new(ASTNodeType::Literal(LiteralValue::Int(5)), None);
475        let args_t = vec![crate::traits::ArgumentHandle::new(&t, &ctx)];
476        let args_n = vec![crate::traits::ArgumentHandle::new(&n, &ctx)];
477        assert_eq!(
478            f.dispatch(&args_t, &ctx.function_context(None)).unwrap(),
479            LiteralValue::Boolean(true)
480        );
481        assert_eq!(
482            f.dispatch(&args_n, &ctx.function_context(None)).unwrap(),
483            LiteralValue::Boolean(false)
484        );
485
486        // ISBLANK
487        let wb2 = TestWorkbook::new().with_function(std::sync::Arc::new(IsBlankFn));
488        let ctx2 = interp(&wb2);
489        let f2 = ctx2.context.get_function("", "ISBLANK").unwrap();
490        let blank = ASTNode::new(ASTNodeType::Literal(LiteralValue::Empty), None);
491        let blank_args = vec![crate::traits::ArgumentHandle::new(&blank, &ctx2)];
492        assert_eq!(
493            f2.dispatch(&blank_args, &ctx2.function_context(None))
494                .unwrap(),
495            LiteralValue::Boolean(true)
496        );
497    }
498
499    #[test]
500    fn iserror_variants() {
501        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(IsErrorFn));
502        let ctx = interp(&wb);
503        let f = ctx.context.get_function("", "ISERROR").unwrap();
504        let err = ASTNode::new(
505            ASTNodeType::Literal(LiteralValue::Error(ExcelError::new(ExcelErrorKind::Div))),
506            None,
507        );
508        let ok = ASTNode::new(ASTNodeType::Literal(LiteralValue::Int(1)), None);
509        let a_err = vec![crate::traits::ArgumentHandle::new(&err, &ctx)];
510        let a_ok = vec![crate::traits::ArgumentHandle::new(&ok, &ctx)];
511        assert_eq!(
512            f.dispatch(&a_err, &ctx.function_context(None)).unwrap(),
513            LiteralValue::Boolean(true)
514        );
515        assert_eq!(
516            f.dispatch(&a_ok, &ctx.function_context(None)).unwrap(),
517            LiteralValue::Boolean(false)
518        );
519    }
520
521    #[test]
522    fn type_codes_basic() {
523        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(TypeFn));
524        let ctx = interp(&wb);
525        let f = ctx.context.get_function("", "TYPE").unwrap();
526        let v_num = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(2.0)), None);
527        let v_txt = ASTNode::new(ASTNodeType::Literal(LiteralValue::Text("hi".into())), None);
528        let v_bool = ASTNode::new(ASTNodeType::Literal(LiteralValue::Boolean(true)), None);
529        let v_err = ASTNode::new(
530            ASTNodeType::Literal(LiteralValue::Error(ExcelError::new(ExcelErrorKind::Value))),
531            None,
532        );
533        let v_arr = ASTNode::new(
534            ASTNodeType::Literal(LiteralValue::Array(vec![vec![LiteralValue::Int(1)]])),
535            None,
536        );
537        let a_num = vec![crate::traits::ArgumentHandle::new(&v_num, &ctx)];
538        let a_txt = vec![crate::traits::ArgumentHandle::new(&v_txt, &ctx)];
539        let a_bool = vec![crate::traits::ArgumentHandle::new(&v_bool, &ctx)];
540        let a_err = vec![crate::traits::ArgumentHandle::new(&v_err, &ctx)];
541        let a_arr = vec![crate::traits::ArgumentHandle::new(&v_arr, &ctx)];
542        assert_eq!(
543            f.dispatch(&a_num, &ctx.function_context(None)).unwrap(),
544            LiteralValue::Int(1)
545        );
546        assert_eq!(
547            f.dispatch(&a_txt, &ctx.function_context(None)).unwrap(),
548            LiteralValue::Int(2)
549        );
550        assert_eq!(
551            f.dispatch(&a_bool, &ctx.function_context(None)).unwrap(),
552            LiteralValue::Int(4)
553        );
554        match f.dispatch(&a_err, &ctx.function_context(None)).unwrap() {
555            LiteralValue::Error(e) => assert_eq!(e, "#VALUE!"),
556            _ => panic!(),
557        }
558        assert_eq!(
559            f.dispatch(&a_arr, &ctx.function_context(None)).unwrap(),
560            LiteralValue::Int(64)
561        );
562    }
563
564    #[test]
565    fn na_and_n_and_t() {
566        let wb = TestWorkbook::new()
567            .with_function(std::sync::Arc::new(NaFn))
568            .with_function(std::sync::Arc::new(NFn))
569            .with_function(std::sync::Arc::new(TFn));
570        let ctx = wb.interpreter();
571        // NA()
572        let na_fn = ctx.context.get_function("", "NA").unwrap();
573        match na_fn.eval_scalar(&[], &ctx.function_context(None)).unwrap() {
574            LiteralValue::Error(e) => assert_eq!(e, "#N/A"),
575            _ => panic!(),
576        }
577        // N()
578        let n_fn = ctx.context.get_function("", "N").unwrap();
579        let val = ASTNode::new(ASTNodeType::Literal(LiteralValue::Boolean(true)), None);
580        let args = vec![crate::traits::ArgumentHandle::new(&val, &ctx)];
581        assert_eq!(
582            n_fn.dispatch(&args, &ctx.function_context(None)).unwrap(),
583            LiteralValue::Int(1)
584        );
585        // T()
586        let t_fn = ctx.context.get_function("", "T").unwrap();
587        let txt = ASTNode::new(ASTNodeType::Literal(LiteralValue::Text("abc".into())), None);
588        let args_t = vec![crate::traits::ArgumentHandle::new(&txt, &ctx)];
589        assert_eq!(
590            t_fn.dispatch(&args_t, &ctx.function_context(None)).unwrap(),
591            LiteralValue::Text("abc".into())
592        );
593    }
594}