Skip to main content

formualizer_eval/builtins/
info.rs

1use crate::args::ArgSchema;
2use crate::function::Function;
3use crate::function_contract::FunctionDependencyContract;
4use crate::traits::{ArgumentHandle, CalcValue, FunctionContext};
5use formualizer_common::{ExcelError, ExcelErrorKind, LiteralValue};
6use formualizer_macros::func_caps;
7
8use super::utils::ARG_ANY_ONE;
9
10/* Info and type-introspection builtins for spreadsheet formulas. */
11
12fn scalar<'ctx>(value: LiteralValue) -> CalcValue<'ctx> {
13    CalcValue::Scalar(value)
14}
15
16fn error_value<'ctx>(kind: ExcelErrorKind) -> CalcValue<'ctx> {
17    scalar(LiteralValue::Error(ExcelError::new(kind)))
18}
19
20fn arity_error<'ctx>() -> Result<CalcValue<'ctx>, ExcelError> {
21    Ok(error_value(ExcelErrorKind::Value))
22}
23
24fn na_result<'ctx>() -> Result<CalcValue<'ctx>, ExcelError> {
25    Ok(error_value(ExcelErrorKind::Na))
26}
27
28#[derive(Debug)]
29pub struct IsNumberFn;
30/// Returns TRUE when the value is numeric.
31///
32/// This includes integer, floating-point, and temporal serial-compatible values.
33///
34/// # Remarks
35/// - Returns TRUE for `Int`, `Number`, `Date`, `DateTime`, `Time`, and `Duration`.
36/// - Text that looks numeric is still text and returns FALSE.
37/// - Errors are treated as non-numeric and return FALSE.
38///
39/// # Examples
40///
41/// ```yaml,sandbox
42/// title: "Number is numeric"
43/// formula: '=ISNUMBER(42)'
44/// expected: true
45/// ```
46///
47/// ```yaml,sandbox
48/// title: "Numeric text is not numeric"
49/// formula: '=ISNUMBER("42")'
50/// expected: false
51/// ```
52///
53/// ```yaml,docs
54/// related:
55///   - VALUE
56///   - N
57///   - TYPE
58/// faq:
59///   - q: "Does numeric-looking text count as a number?"
60///     a: "No. ISNUMBER checks the stored value type, so text like \"42\" returns FALSE."
61/// ```
62/// [formualizer-docgen:schema:start]
63/// Name: ISNUMBER
64/// Type: IsNumberFn
65/// Min args: 1
66/// Max args: 1
67/// Variadic: false
68/// Signature: ISNUMBER(arg1: any@scalar)
69/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
70/// Caps: PURE
71/// [formualizer-docgen:schema:end]
72impl Function for IsNumberFn {
73    func_caps!(PURE);
74    fn name(&self) -> &'static str {
75        "ISNUMBER"
76    }
77    fn min_args(&self) -> usize {
78        1
79    }
80    fn dependency_contract(&self, arity: usize) -> Option<FunctionDependencyContract> {
81        FunctionDependencyContract::static_scalar_all_args(arity)
82    }
83    fn arg_schema(&self) -> &'static [ArgSchema] {
84        &ARG_ANY_ONE[..]
85    }
86    fn eval<'a, 'b, 'c>(
87        &self,
88        args: &'c [ArgumentHandle<'a, 'b>],
89        _ctx: &dyn FunctionContext<'b>,
90    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
91        if args.len() != 1 {
92            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
93                ExcelError::new_value(),
94            )));
95        }
96        let v = args[0].value()?.into_literal();
97        let is_num = matches!(
98            v,
99            LiteralValue::Int(_)
100                | LiteralValue::Number(_)
101                | LiteralValue::Date(_)
102                | LiteralValue::DateTime(_)
103                | LiteralValue::Time(_)
104                | LiteralValue::Duration(_)
105        );
106        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Boolean(
107            is_num,
108        )))
109    }
110}
111
112#[derive(Debug)]
113pub struct IsTextFn;
114/// Returns TRUE when the value is text.
115///
116/// # Remarks
117/// - Only text literals return TRUE.
118/// - Numbers, booleans, blanks, and errors return FALSE.
119/// - No coercion from other types to text is performed for this check.
120///
121/// # Examples
122///
123/// ```yaml,sandbox
124/// title: "Detect text"
125/// formula: '=ISTEXT("alpha")'
126/// expected: true
127/// ```
128///
129/// ```yaml,sandbox
130/// title: "Number is not text"
131/// formula: '=ISTEXT(100)'
132/// expected: false
133/// ```
134///
135/// ```yaml,docs
136/// related:
137///   - T
138///   - TYPE
139///   - ISNUMBER
140/// faq:
141///   - q: "Is an empty string treated as text?"
142///     a: "Yes. An empty string literal is still text, so ISTEXT(\"\") returns TRUE."
143/// ```
144/// [formualizer-docgen:schema:start]
145/// Name: ISTEXT
146/// Type: IsTextFn
147/// Min args: 1
148/// Max args: 1
149/// Variadic: false
150/// Signature: ISTEXT(arg1: any@scalar)
151/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
152/// Caps: PURE
153/// [formualizer-docgen:schema:end]
154impl Function for IsTextFn {
155    func_caps!(PURE);
156    fn name(&self) -> &'static str {
157        "ISTEXT"
158    }
159    fn min_args(&self) -> usize {
160        1
161    }
162    fn dependency_contract(&self, arity: usize) -> Option<FunctionDependencyContract> {
163        FunctionDependencyContract::static_scalar_all_args(arity)
164    }
165    fn arg_schema(&self) -> &'static [ArgSchema] {
166        &ARG_ANY_ONE[..]
167    }
168    fn eval<'a, 'b, 'c>(
169        &self,
170        args: &'c [ArgumentHandle<'a, 'b>],
171        _ctx: &dyn FunctionContext<'b>,
172    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
173        if args.len() != 1 {
174            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
175                ExcelError::new_value(),
176            )));
177        }
178        let v = args[0].value()?.into_literal();
179        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Boolean(
180            matches!(v, LiteralValue::Text(_)),
181        )))
182    }
183}
184
185#[derive(Debug)]
186pub struct IsLogicalFn;
187/// Returns TRUE when the value is a boolean.
188///
189/// # Remarks
190/// - Only logical TRUE/FALSE values return TRUE.
191/// - Numeric truthy/falsy values are not considered logical by this predicate.
192/// - Errors return FALSE.
193///
194/// # Examples
195///
196/// ```yaml,sandbox
197/// title: "Boolean input"
198/// formula: '=ISLOGICAL(TRUE)'
199/// expected: true
200/// ```
201///
202/// ```yaml,sandbox
203/// title: "Numeric input"
204/// formula: '=ISLOGICAL(1)'
205/// expected: false
206/// ```
207///
208/// ```yaml,docs
209/// related:
210///   - TRUE
211///   - FALSE
212///   - TYPE
213/// faq:
214///   - q: "Do truthy numbers count as logical values?"
215///     a: "No. ISLOGICAL returns TRUE only for actual boolean TRUE/FALSE values."
216/// ```
217/// [formualizer-docgen:schema:start]
218/// Name: ISLOGICAL
219/// Type: IsLogicalFn
220/// Min args: 1
221/// Max args: 1
222/// Variadic: false
223/// Signature: ISLOGICAL(arg1: any@scalar)
224/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
225/// Caps: PURE
226/// [formualizer-docgen:schema:end]
227impl Function for IsLogicalFn {
228    func_caps!(PURE);
229    fn name(&self) -> &'static str {
230        "ISLOGICAL"
231    }
232    fn min_args(&self) -> usize {
233        1
234    }
235    fn dependency_contract(&self, arity: usize) -> Option<FunctionDependencyContract> {
236        FunctionDependencyContract::static_scalar_all_args(arity)
237    }
238    fn arg_schema(&self) -> &'static [ArgSchema] {
239        &ARG_ANY_ONE[..]
240    }
241    fn eval<'a, 'b, 'c>(
242        &self,
243        args: &'c [ArgumentHandle<'a, 'b>],
244        _ctx: &dyn FunctionContext<'b>,
245    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
246        if args.len() != 1 {
247            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
248                ExcelError::new_value(),
249            )));
250        }
251        let v = args[0].value()?.into_literal();
252        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Boolean(
253            matches!(v, LiteralValue::Boolean(_)),
254        )))
255    }
256}
257
258#[derive(Debug)]
259pub struct IsBlankFn;
260/// Returns TRUE only for a truly empty value.
261///
262/// # Remarks
263/// - Empty string `""` is text, not blank, so it returns FALSE.
264/// - Numeric zero and FALSE are not blank.
265/// - Errors return FALSE.
266///
267/// # Examples
268///
269/// ```yaml,sandbox
270/// title: "Reference to an empty cell"
271/// formula: '=ISBLANK(A1)'
272/// expected: true
273/// ```
274///
275/// ```yaml,sandbox
276/// title: "Empty string is not blank"
277/// formula: '=ISBLANK("")'
278/// expected: false
279/// ```
280///
281/// ```yaml,docs
282/// related:
283///   - ISTEXT
284///   - LEN
285///   - T
286/// faq:
287///   - q: "Why does ISBLANK(\"\") return FALSE?"
288///     a: "Because an empty string is text, not a truly empty cell value."
289/// ```
290/// [formualizer-docgen:schema:start]
291/// Name: ISBLANK
292/// Type: IsBlankFn
293/// Min args: 1
294/// Max args: 1
295/// Variadic: false
296/// Signature: ISBLANK(arg1: any@scalar)
297/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
298/// Caps: PURE
299/// [formualizer-docgen:schema:end]
300impl Function for IsBlankFn {
301    func_caps!(PURE);
302    fn name(&self) -> &'static str {
303        "ISBLANK"
304    }
305    fn min_args(&self) -> usize {
306        1
307    }
308    fn dependency_contract(&self, arity: usize) -> Option<FunctionDependencyContract> {
309        FunctionDependencyContract::static_scalar_all_args(arity)
310    }
311    fn arg_schema(&self) -> &'static [ArgSchema] {
312        &ARG_ANY_ONE[..]
313    }
314    fn eval<'a, 'b, 'c>(
315        &self,
316        args: &'c [ArgumentHandle<'a, 'b>],
317        _ctx: &dyn FunctionContext<'b>,
318    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
319        if args.len() != 1 {
320            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
321                ExcelError::new_value(),
322            )));
323        }
324        let v = args[0].value()?.into_literal();
325        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Boolean(
326            matches!(v, LiteralValue::Empty),
327        )))
328    }
329}
330
331#[derive(Debug)]
332pub struct IsErrorFn; // TRUE for any error (#N/A included)
333/// Returns TRUE for any error value.
334///
335/// # Remarks
336/// - Matches all error kinds, including `#N/A`.
337/// - Non-error values always return FALSE.
338/// - Arity mismatch returns `#VALUE!`.
339///
340/// # Examples
341///
342/// ```yaml,sandbox
343/// title: "Division error"
344/// formula: '=ISERROR(1/0)'
345/// expected: true
346/// ```
347///
348/// ```yaml,sandbox
349/// title: "Normal value"
350/// formula: '=ISERROR(123)'
351/// expected: false
352/// ```
353///
354/// ```yaml,docs
355/// related:
356///   - ISERR
357///   - ISNA
358///   - IFERROR
359/// faq:
360///   - q: "Does ISERROR include #N/A?"
361///     a: "Yes. ISERROR returns TRUE for all error kinds, including #N/A."
362/// ```
363/// [formualizer-docgen:schema:start]
364/// Name: ISERROR
365/// Type: IsErrorFn
366/// Min args: 1
367/// Max args: 1
368/// Variadic: false
369/// Signature: ISERROR(arg1: any@scalar)
370/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
371/// Caps: PURE
372/// [formualizer-docgen:schema:end]
373impl Function for IsErrorFn {
374    func_caps!(PURE);
375    fn name(&self) -> &'static str {
376        "ISERROR"
377    }
378    fn min_args(&self) -> usize {
379        1
380    }
381    fn dependency_contract(&self, arity: usize) -> Option<FunctionDependencyContract> {
382        FunctionDependencyContract::static_scalar_all_args(arity)
383    }
384    fn arg_schema(&self) -> &'static [ArgSchema] {
385        &ARG_ANY_ONE[..]
386    }
387    fn eval<'a, 'b, 'c>(
388        &self,
389        args: &'c [ArgumentHandle<'a, 'b>],
390        _ctx: &dyn FunctionContext<'b>,
391    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
392        if args.len() != 1 {
393            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
394                ExcelError::new_value(),
395            )));
396        }
397        let v = args[0].value()?.into_literal();
398        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Boolean(
399            matches!(v, LiteralValue::Error(_)),
400        )))
401    }
402}
403
404#[derive(Debug)]
405pub struct IsErrFn; // TRUE for any error except #N/A
406/// Returns TRUE for any error except `#N/A`.
407///
408/// # Remarks
409/// - `#N/A` specifically returns FALSE.
410/// - Other errors such as `#DIV/0!` or `#VALUE!` return TRUE.
411/// - Non-error values return FALSE.
412///
413/// # Examples
414///
415/// ```yaml,sandbox
416/// title: "DIV/0 is an error excluding N/A"
417/// formula: '=ISERR(1/0)'
418/// expected: true
419/// ```
420///
421/// ```yaml,sandbox
422/// title: "N/A is excluded"
423/// formula: '=ISERR(NA())'
424/// expected: false
425/// ```
426///
427/// ```yaml,docs
428/// related:
429///   - ISERROR
430///   - ISNA
431///   - IFERROR
432/// faq:
433///   - q: "What is the difference between ISERR and ISERROR?"
434///     a: "ISERR excludes #N/A, while ISERROR treats #N/A as an error too."
435/// ```
436/// [formualizer-docgen:schema:start]
437/// Name: ISERR
438/// Type: IsErrFn
439/// Min args: 1
440/// Max args: 1
441/// Variadic: false
442/// Signature: ISERR(arg1: any@scalar)
443/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
444/// Caps: PURE
445/// [formualizer-docgen:schema:end]
446impl Function for IsErrFn {
447    func_caps!(PURE);
448    fn name(&self) -> &'static str {
449        "ISERR"
450    }
451    fn min_args(&self) -> usize {
452        1
453    }
454    fn dependency_contract(&self, arity: usize) -> Option<FunctionDependencyContract> {
455        FunctionDependencyContract::static_scalar_all_args(arity)
456    }
457    fn arg_schema(&self) -> &'static [ArgSchema] {
458        &ARG_ANY_ONE[..]
459    }
460    fn eval<'a, 'b, 'c>(
461        &self,
462        args: &'c [ArgumentHandle<'a, 'b>],
463        _ctx: &dyn FunctionContext<'b>,
464    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
465        if args.len() != 1 {
466            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
467                ExcelError::new_value(),
468            )));
469        }
470        let v = args[0].value()?.into_literal();
471        let is_err = match v {
472            LiteralValue::Error(e) => e.kind != ExcelErrorKind::Na,
473            _ => false,
474        };
475        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Boolean(
476            is_err,
477        )))
478    }
479}
480
481#[derive(Debug)]
482pub struct IsNaFn; // TRUE only for #N/A
483/// Returns TRUE only for the `#N/A` error.
484///
485/// # Remarks
486/// - Other error kinds return FALSE.
487/// - Non-error values return FALSE.
488/// - Useful when `#N/A` has special business meaning.
489///
490/// # Examples
491///
492/// ```yaml,sandbox
493/// title: "Check for N/A"
494/// formula: '=ISNA(NA())'
495/// expected: true
496/// ```
497///
498/// ```yaml,sandbox
499/// title: "Other errors are not N/A"
500/// formula: '=ISNA(1/0)'
501/// expected: false
502/// ```
503///
504/// ```yaml,docs
505/// related:
506///   - NA
507///   - IFNA
508///   - ISERROR
509/// faq:
510///   - q: "Does ISNA return TRUE for errors other than #N/A?"
511///     a: "No. It returns TRUE only when the value is exactly #N/A."
512/// ```
513/// [formualizer-docgen:schema:start]
514/// Name: ISNA
515/// Type: IsNaFn
516/// Min args: 1
517/// Max args: 1
518/// Variadic: false
519/// Signature: ISNA(arg1: any@scalar)
520/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
521/// Caps: PURE
522/// [formualizer-docgen:schema:end]
523impl Function for IsNaFn {
524    func_caps!(PURE);
525    fn name(&self) -> &'static str {
526        "ISNA"
527    }
528    fn min_args(&self) -> usize {
529        1
530    }
531    fn dependency_contract(&self, arity: usize) -> Option<FunctionDependencyContract> {
532        FunctionDependencyContract::static_scalar_all_args(arity)
533    }
534    fn arg_schema(&self) -> &'static [ArgSchema] {
535        &ARG_ANY_ONE[..]
536    }
537    fn eval<'a, 'b, 'c>(
538        &self,
539        args: &'c [ArgumentHandle<'a, 'b>],
540        _ctx: &dyn FunctionContext<'b>,
541    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
542        if args.len() != 1 {
543            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
544                ExcelError::new_value(),
545            )));
546        }
547        let v = args[0].value()?.into_literal();
548        let is_na = matches!(v, LiteralValue::Error(e) if e.kind==ExcelErrorKind::Na);
549        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Boolean(
550            is_na,
551        )))
552    }
553}
554
555#[derive(Debug)]
556pub struct IsFormulaFn; // Requires provenance tracking (not yet) => always FALSE.
557/// Returns whether a value originates from a formula.
558///
559/// Current engine metadata does not track formula provenance at this call site.
560///
561/// # Remarks
562/// - This implementation currently returns FALSE for all inputs.
563/// - Errors are not raised solely due to provenance unavailability.
564/// - Arity mismatch returns `#VALUE!`.
565///
566/// # Examples
567///
568/// ```yaml,sandbox
569/// title: "Literal value"
570/// formula: '=ISFORMULA(10)'
571/// expected: false
572/// ```
573///
574/// ```yaml,sandbox
575/// title: "Computed value in expression context"
576/// formula: '=ISFORMULA(1+1)'
577/// expected: false
578/// ```
579///
580/// ```yaml,docs
581/// related:
582///   - TYPE
583///   - ISNUMBER
584///   - ISTEXT
585/// faq:
586///   - q: "Can ISFORMULA currently detect formula provenance?"
587///     a: "Not yet. This implementation always returns FALSE because provenance metadata is not tracked here."
588/// ```
589/// [formualizer-docgen:schema:start]
590/// Name: ISFORMULA
591/// Type: IsFormulaFn
592/// Min args: 1
593/// Max args: 1
594/// Variadic: false
595/// Signature: ISFORMULA(arg1: any@scalar)
596/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
597/// Caps: PURE
598/// [formualizer-docgen:schema:end]
599impl Function for IsFormulaFn {
600    func_caps!(PURE);
601    fn name(&self) -> &'static str {
602        "ISFORMULA"
603    }
604    fn min_args(&self) -> usize {
605        1
606    }
607    fn arg_schema(&self) -> &'static [ArgSchema] {
608        &ARG_ANY_ONE[..]
609    }
610    fn eval<'a, 'b, 'c>(
611        &self,
612        args: &'c [ArgumentHandle<'a, 'b>],
613        _ctx: &dyn FunctionContext<'b>,
614    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
615        if args.len() != 1 {
616            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
617                ExcelError::new_value(),
618            )));
619        }
620        // Formula provenance metadata is not tracked yet, so ISFORMULA currently returns FALSE.
621        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Boolean(
622            false,
623        )))
624    }
625}
626
627/// Returns TRUE when the argument resolves to a reference.
628///
629/// Checks reference metadata without materializing the referenced value or range.
630///
631/// ```yaml,sandbox
632/// title: "Cell reference"
633/// formula: "=ISREF(A1)"
634/// expected: true
635/// ```
636///
637/// ```yaml,sandbox
638/// title: "Expression is not a reference"
639/// formula: "=ISREF(1+1)"
640/// expected: false
641/// ```
642///
643/// ```yaml,docs
644/// related:
645///   - FORMULATEXT
646///   - SHEET
647///   - ISFORMULA
648/// faq:
649///   - q: "Does ISREF read cell values?"
650///     a: "No. It inspects whether the argument can resolve as a reference."
651/// ```
652#[derive(Debug)]
653pub struct IsRefFn;
654/// Returns TRUE when the argument resolves to a reference.
655///
656/// [formualizer-docgen:schema:start]
657/// Name: ISREF
658/// Type: IsRefFn
659/// Min args: 1
660/// Max args: 1
661/// Variadic: false
662/// Signature: ISREF(arg1: any@scalar)
663/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
664/// Caps: PURE
665/// [formualizer-docgen:schema:end]
666impl Function for IsRefFn {
667    func_caps!(PURE);
668    fn name(&self) -> &'static str {
669        "ISREF"
670    }
671    fn min_args(&self) -> usize {
672        1
673    }
674    fn arg_schema(&self) -> &'static [ArgSchema] {
675        &ARG_ANY_ONE[..]
676    }
677    fn dispatch<'a, 'b, 'c>(
678        &self,
679        args: &'c [ArgumentHandle<'a, 'b>],
680        ctx: &dyn FunctionContext<'b>,
681    ) -> Result<CalcValue<'b>, ExcelError> {
682        self.eval(args, ctx)
683    }
684    fn eval<'a, 'b, 'c>(
685        &self,
686        args: &'c [ArgumentHandle<'a, 'b>],
687        ctx: &dyn FunctionContext<'b>,
688    ) -> Result<CalcValue<'b>, ExcelError> {
689        if args.len() != 1 {
690            return arity_error();
691        }
692        let Ok(reference) = args[0].as_reference_or_eval() else {
693            return Ok(scalar(LiteralValue::Boolean(false)));
694        };
695        let is_ref = match ctx.inspect_reference(&reference) {
696            Ok(Some(info)) => info.first_cell.is_some() || info.sheet_count.is_some(),
697            Ok(None) => true,
698            Err(_) => false,
699        };
700        Ok(scalar(LiteralValue::Boolean(is_ref)))
701    }
702}
703
704/// Returns the formula text stored in the referenced cell.
705///
706/// Retrieves formula source text for a single referenced cell without evaluating
707/// that cell's value.
708///
709/// # Remarks
710/// - Returns `#N/A` if the reference does not point at a formula cell.
711/// - Staged formula text is preferred when present; otherwise canonical formula text is returned.
712///
713/// ```yaml,sandbox
714/// title: "Formula text"
715/// grid:
716///   A1: "=1+2"
717/// formula: "=FORMULATEXT(A1)"
718/// expected: "=1 + 2"
719/// ```
720///
721/// ```yaml,docs
722/// related:
723///   - ISFORMULA
724///   - ISREF
725///   - SHEET
726/// faq:
727///   - q: "Does FORMULATEXT evaluate the referenced formula?"
728///     a: "No. It retrieves formula provenance/source text only."
729/// ```
730#[derive(Debug)]
731pub struct FormulaTextFn;
732/// Returns the formula text stored in the referenced cell.
733///
734/// [formualizer-docgen:schema:start]
735/// Name: FORMULATEXT
736/// Type: FormulaTextFn
737/// Min args: 1
738/// Max args: 1
739/// Variadic: false
740/// Signature: FORMULATEXT(arg1: any@scalar)
741/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
742/// Caps: PURE
743/// [formualizer-docgen:schema:end]
744impl Function for FormulaTextFn {
745    func_caps!(PURE);
746    fn name(&self) -> &'static str {
747        "FORMULATEXT"
748    }
749    fn min_args(&self) -> usize {
750        1
751    }
752    fn arg_schema(&self) -> &'static [ArgSchema] {
753        &ARG_ANY_ONE[..]
754    }
755    fn dispatch<'a, 'b, 'c>(
756        &self,
757        args: &'c [ArgumentHandle<'a, 'b>],
758        ctx: &dyn FunctionContext<'b>,
759    ) -> Result<CalcValue<'b>, ExcelError> {
760        self.eval(args, ctx)
761    }
762    fn eval<'a, 'b, 'c>(
763        &self,
764        args: &'c [ArgumentHandle<'a, 'b>],
765        ctx: &dyn FunctionContext<'b>,
766    ) -> Result<CalcValue<'b>, ExcelError> {
767        if args.len() != 1 {
768            return arity_error();
769        }
770        let reference = match args[0].as_reference_or_eval() {
771            Ok(reference) => reference,
772            Err(_) => return na_result(),
773        };
774        let Some(info) = ctx.inspect_reference(&reference)? else {
775            return na_result();
776        };
777        let Some(cell) = info.first_cell else {
778            return na_result();
779        };
780        match ctx.formula_text_at_cell(cell)? {
781            Some(text) => Ok(scalar(LiteralValue::Text(text))),
782            None => na_result(),
783        }
784    }
785}
786
787/// Returns the 1-based sheet index for the current sheet or a reference.
788///
789/// With no argument, returns the index of the sheet containing the formula. With
790/// a reference or sheet-name text argument, returns that sheet's index.
791///
792/// ```yaml,sandbox
793/// title: "Current sheet index"
794/// formula: "=SHEET()"
795/// expected: 1
796/// ```
797///
798/// ```yaml,sandbox
799/// title: "Referenced sheet index"
800/// formula: "=SHEET(A1)"
801/// expected: 1
802/// ```
803///
804/// ```yaml,docs
805/// related:
806///   - SHEETS
807///   - ISREF
808///   - FORMULATEXT
809/// faq:
810///   - q: "Are sheet indexes 0-based?"
811///     a: "No. SHEET returns Excel-style 1-based sheet indexes."
812/// ```
813#[derive(Debug)]
814pub struct SheetFn;
815/// Returns the 1-based sheet index for the current sheet or a reference.
816///
817/// [formualizer-docgen:schema:start]
818/// Name: SHEET
819/// Type: SheetFn
820/// Min args: 0
821/// Max args: variadic
822/// Variadic: true
823/// Signature: SHEET(arg1...: any@scalar)
824/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
825/// Caps: PURE
826/// [formualizer-docgen:schema:end]
827impl Function for SheetFn {
828    func_caps!(PURE);
829    fn name(&self) -> &'static str {
830        "SHEET"
831    }
832    fn min_args(&self) -> usize {
833        0
834    }
835    fn variadic(&self) -> bool {
836        true
837    }
838    fn arg_schema(&self) -> &'static [ArgSchema] {
839        &ARG_ANY_ONE[..]
840    }
841    fn dispatch<'a, 'b, 'c>(
842        &self,
843        args: &'c [ArgumentHandle<'a, 'b>],
844        ctx: &dyn FunctionContext<'b>,
845    ) -> Result<CalcValue<'b>, ExcelError> {
846        self.eval(args, ctx)
847    }
848    fn eval<'a, 'b, 'c>(
849        &self,
850        args: &'c [ArgumentHandle<'a, 'b>],
851        ctx: &dyn FunctionContext<'b>,
852    ) -> Result<CalcValue<'b>, ExcelError> {
853        if args.len() > 1 {
854            return arity_error();
855        }
856        if args.is_empty() {
857            return ctx
858                .current_sheet_index()
859                .map(|idx| scalar(LiteralValue::Int(idx as i64)))
860                .map(Ok)
861                .unwrap_or_else(na_result);
862        }
863
864        if let Ok(reference) = args[0].as_reference_or_eval() {
865            let Some(info) = ctx.inspect_reference(&reference)? else {
866                return na_result();
867            };
868            return info
869                .first_sheet_index
870                .map(|idx| scalar(LiteralValue::Int(idx as i64)))
871                .map(Ok)
872                .unwrap_or_else(na_result);
873        }
874
875        match args[0].value()?.into_literal() {
876            LiteralValue::Text(name) => ctx
877                .sheet_index_by_name(name.as_ref())
878                .map(|idx| scalar(LiteralValue::Int(idx as i64)))
879                .map(Ok)
880                .unwrap_or_else(na_result),
881            LiteralValue::Error(e) => Ok(scalar(LiteralValue::Error(e))),
882            _ => arity_error(),
883        }
884    }
885}
886
887/// Returns the number of sheets in the workbook or reference span.
888///
889/// With no argument, returns the active workbook sheet count. With a reference,
890/// returns the number of sheets covered by that reference.
891///
892/// ```yaml,sandbox
893/// title: "Workbook sheet count"
894/// formula: "=SHEETS()"
895/// expected: 1
896/// ```
897///
898/// ```yaml,sandbox
899/// title: "Single-sheet reference count"
900/// formula: "=SHEETS(A1)"
901/// expected: 1
902/// ```
903///
904/// ```yaml,docs
905/// related:
906///   - SHEET
907///   - ISREF
908///   - FORMULATEXT
909/// faq:
910///   - q: "What does SHEETS return for ordinary references?"
911///     a: "Ordinary references cover one sheet, so the result is 1."
912/// ```
913#[derive(Debug)]
914pub struct SheetsFn;
915/// Returns the number of sheets in the workbook or covered by a 3D reference.
916///
917/// [formualizer-docgen:schema:start]
918/// Name: SHEETS
919/// Type: SheetsFn
920/// Min args: 0
921/// Max args: variadic
922/// Variadic: true
923/// Signature: SHEETS(arg1...: any@scalar)
924/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
925/// Caps: PURE
926/// [formualizer-docgen:schema:end]
927impl Function for SheetsFn {
928    func_caps!(PURE);
929    fn name(&self) -> &'static str {
930        "SHEETS"
931    }
932    fn min_args(&self) -> usize {
933        0
934    }
935    fn variadic(&self) -> bool {
936        true
937    }
938    fn arg_schema(&self) -> &'static [ArgSchema] {
939        &ARG_ANY_ONE[..]
940    }
941    fn dispatch<'a, 'b, 'c>(
942        &self,
943        args: &'c [ArgumentHandle<'a, 'b>],
944        ctx: &dyn FunctionContext<'b>,
945    ) -> Result<CalcValue<'b>, ExcelError> {
946        self.eval(args, ctx)
947    }
948    fn eval<'a, 'b, 'c>(
949        &self,
950        args: &'c [ArgumentHandle<'a, 'b>],
951        ctx: &dyn FunctionContext<'b>,
952    ) -> Result<CalcValue<'b>, ExcelError> {
953        if args.len() > 1 {
954            return arity_error();
955        }
956        if args.is_empty() {
957            return ctx
958                .workbook_sheet_count()
959                .map(|count| scalar(LiteralValue::Int(count as i64)))
960                .map(Ok)
961                .unwrap_or_else(na_result);
962        }
963        let reference = match args[0].as_reference_or_eval() {
964            Ok(reference) => reference,
965            Err(_) => return arity_error(),
966        };
967        let Some(info) = ctx.inspect_reference(&reference)? else {
968            return na_result();
969        };
970        info.sheet_count
971            .map(|count| scalar(LiteralValue::Int(count as i64)))
972            .map(Ok)
973            .unwrap_or_else(na_result)
974    }
975}
976
977#[derive(Debug)]
978pub struct TypeFn;
979/// Returns an Excel TYPE code describing the value category.
980///
981/// # Remarks
982/// - Codes: `1` number, `2` text, `4` logical, `64` array.
983/// - Errors are propagated unchanged instead of returning `16`.
984/// - Blank values map to numeric code `1` in this implementation.
985///
986/// # Examples
987///
988/// ```yaml,sandbox
989/// title: "Text type code"
990/// formula: '=TYPE("abc")'
991/// expected: 2
992/// ```
993///
994/// ```yaml,sandbox
995/// title: "Boolean type code"
996/// formula: '=TYPE(TRUE)'
997/// expected: 4
998/// ```
999///
1000/// ```yaml,docs
1001/// related:
1002///   - ISNUMBER
1003///   - ISTEXT
1004///   - ISLOGICAL
1005/// faq:
1006///   - q: "How are errors handled by TYPE?"
1007///     a: "Errors are propagated unchanged instead of returning Excel's error type code 16."
1008/// ```
1009/// [formualizer-docgen:schema:start]
1010/// Name: TYPE
1011/// Type: TypeFn
1012/// Min args: 1
1013/// Max args: 1
1014/// Variadic: false
1015/// Signature: TYPE(arg1: any@scalar)
1016/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
1017/// Caps: PURE
1018/// [formualizer-docgen:schema:end]
1019impl Function for TypeFn {
1020    func_caps!(PURE);
1021    fn name(&self) -> &'static str {
1022        "TYPE"
1023    }
1024    fn min_args(&self) -> usize {
1025        1
1026    }
1027    fn arg_schema(&self) -> &'static [ArgSchema] {
1028        &ARG_ANY_ONE[..]
1029    }
1030    fn eval<'a, 'b, 'c>(
1031        &self,
1032        args: &'c [ArgumentHandle<'a, 'b>],
1033        _ctx: &dyn FunctionContext<'b>,
1034    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1035        if args.len() != 1 {
1036            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1037                ExcelError::new_value(),
1038            )));
1039        }
1040        let v = args[0].value()?.into_literal(); // Propagate errors directly
1041        if let LiteralValue::Error(e) = v {
1042            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
1043        }
1044        let code = match v {
1045            LiteralValue::Int(_)
1046            | LiteralValue::Number(_)
1047            | LiteralValue::Empty
1048            | LiteralValue::Date(_)
1049            | LiteralValue::DateTime(_)
1050            | LiteralValue::Time(_)
1051            | LiteralValue::Duration(_) => 1,
1052            LiteralValue::Text(_) => 2,
1053            LiteralValue::Boolean(_) => 4,
1054            LiteralValue::Array(_) => 64,
1055            LiteralValue::Error(_) => unreachable!(),
1056            LiteralValue::Pending => 1, // treat as blank/zero numeric; may change
1057        };
1058        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(code)))
1059    }
1060}
1061
1062#[derive(Debug)]
1063pub struct NaFn; // NA() -> #N/A error
1064/// Returns the `#N/A` error value.
1065///
1066/// # Remarks
1067/// - `NA()` is commonly used to mark missing lookup results.
1068/// - The function takes no arguments.
1069/// - The returned value is an error and propagates through dependent formulas.
1070///
1071/// # Examples
1072///
1073/// ```yaml,sandbox
1074/// title: "Direct N/A"
1075/// formula: '=NA()'
1076/// expected: "#N/A"
1077/// ```
1078///
1079/// ```yaml,sandbox
1080/// title: "Detect N/A"
1081/// formula: '=ISNA(NA())'
1082/// expected: true
1083/// ```
1084///
1085/// ```yaml,docs
1086/// related:
1087///   - ISNA
1088///   - IFNA
1089///   - IFERROR
1090/// faq:
1091///   - q: "When should I use NA() intentionally?"
1092///     a: "Use it to mark missing data so lookups and downstream checks can distinguish absent values from blanks."
1093/// ```
1094/// [formualizer-docgen:schema:start]
1095/// Name: NA
1096/// Type: NaFn
1097/// Min args: 0
1098/// Max args: 0
1099/// Variadic: false
1100/// Signature: NA()
1101/// Arg schema: []
1102/// Caps: PURE
1103/// [formualizer-docgen:schema:end]
1104impl Function for NaFn {
1105    func_caps!(PURE);
1106    fn name(&self) -> &'static str {
1107        "NA"
1108    }
1109    fn min_args(&self) -> usize {
1110        0
1111    }
1112    fn eval<'a, 'b, 'c>(
1113        &self,
1114        _args: &'c [ArgumentHandle<'a, 'b>],
1115        _ctx: &dyn FunctionContext<'b>,
1116    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1117        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1118            ExcelError::new(ExcelErrorKind::Na),
1119        )))
1120    }
1121}
1122
1123#[derive(Debug)]
1124pub struct NFn; // N(value)
1125/// Converts a value to its numeric representation.
1126///
1127/// # Remarks
1128/// - Numbers pass through unchanged; booleans convert to `1`/`0`.
1129/// - Text and blank values convert to `0`.
1130/// - Errors propagate unchanged.
1131/// - Temporal values are converted using serial number representation.
1132///
1133/// # Examples
1134///
1135/// ```yaml,sandbox
1136/// title: "Boolean to number"
1137/// formula: '=N(TRUE)'
1138/// expected: 1
1139/// ```
1140///
1141/// ```yaml,sandbox
1142/// title: "Text to zero"
1143/// formula: '=N("hello")'
1144/// expected: 0
1145/// ```
1146///
1147/// ```yaml,docs
1148/// related:
1149///   - VALUE
1150///   - T
1151///   - TYPE
1152/// faq:
1153///   - q: "What does N do with text and blanks?"
1154///     a: "Text and blank values convert to 0, while existing errors are passed through."
1155/// ```
1156/// [formualizer-docgen:schema:start]
1157/// Name: N
1158/// Type: NFn
1159/// Min args: 1
1160/// Max args: 1
1161/// Variadic: false
1162/// Signature: N(arg1: any@scalar)
1163/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
1164/// Caps: PURE
1165/// [formualizer-docgen:schema:end]
1166impl Function for NFn {
1167    func_caps!(PURE);
1168    fn name(&self) -> &'static str {
1169        "N"
1170    }
1171    fn min_args(&self) -> usize {
1172        1
1173    }
1174    fn dependency_contract(&self, arity: usize) -> Option<FunctionDependencyContract> {
1175        FunctionDependencyContract::static_scalar_all_args(arity)
1176    }
1177    fn arg_schema(&self) -> &'static [ArgSchema] {
1178        &ARG_ANY_ONE[..]
1179    }
1180    fn eval<'a, 'b, 'c>(
1181        &self,
1182        args: &'c [ArgumentHandle<'a, 'b>],
1183        _ctx: &dyn FunctionContext<'b>,
1184    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1185        if args.len() != 1 {
1186            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1187                ExcelError::new_value(),
1188            )));
1189        }
1190        let v = args[0].value()?.into_literal();
1191        match v {
1192            LiteralValue::Int(i) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(i))),
1193            LiteralValue::Number(n) => {
1194                Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(n)))
1195            }
1196            LiteralValue::Date(_)
1197            | LiteralValue::DateTime(_)
1198            | LiteralValue::Time(_)
1199            | LiteralValue::Duration(_) => {
1200                // Convert via serial number helper
1201                if let Some(serial) = v.as_serial_number() {
1202                    Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
1203                        serial,
1204                    )))
1205                } else {
1206                    Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(0)))
1207                }
1208            }
1209            LiteralValue::Boolean(b) => {
1210                Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(if b {
1211                    1
1212                } else {
1213                    0
1214                })))
1215            }
1216            LiteralValue::Text(_) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(0))),
1217            LiteralValue::Empty => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(0))),
1218            LiteralValue::Array(_) => {
1219                // Array-to-scalar implicit intersection is not implemented here; returns 0.
1220                Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(0)))
1221            }
1222            LiteralValue::Error(e) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
1223            LiteralValue::Pending => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(0))),
1224        }
1225    }
1226}
1227
1228#[derive(Debug)]
1229pub struct TFn; // T(value)
1230/// Returns text when input is text, otherwise returns empty text.
1231///
1232/// # Remarks
1233/// - Text values pass through unchanged.
1234/// - Errors propagate unchanged.
1235/// - Numbers, booleans, and blanks return an empty string.
1236///
1237/// # Examples
1238///
1239/// ```yaml,sandbox
1240/// title: "Text passthrough"
1241/// formula: '=T("report")'
1242/// expected: "report"
1243/// ```
1244///
1245/// ```yaml,sandbox
1246/// title: "Number becomes empty text"
1247/// formula: '=T(99)'
1248/// expected: ""
1249/// ```
1250///
1251/// ```yaml,docs
1252/// related:
1253///   - N
1254///   - ISTEXT
1255///   - TYPE
1256/// faq:
1257///   - q: "Does T hide non-text values?"
1258///     a: "Yes. Non-text inputs become an empty string, but errors are still propagated."
1259/// ```
1260/// [formualizer-docgen:schema:start]
1261/// Name: T
1262/// Type: TFn
1263/// Min args: 1
1264/// Max args: 1
1265/// Variadic: false
1266/// Signature: T(arg1: any@scalar)
1267/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
1268/// Caps: PURE
1269/// [formualizer-docgen:schema:end]
1270impl Function for TFn {
1271    func_caps!(PURE);
1272    fn name(&self) -> &'static str {
1273        "T"
1274    }
1275    fn min_args(&self) -> usize {
1276        1
1277    }
1278    fn dependency_contract(&self, arity: usize) -> Option<FunctionDependencyContract> {
1279        FunctionDependencyContract::static_scalar_all_args(arity)
1280    }
1281    fn arg_schema(&self) -> &'static [ArgSchema] {
1282        &ARG_ANY_ONE[..]
1283    }
1284    fn eval<'a, 'b, 'c>(
1285        &self,
1286        args: &'c [ArgumentHandle<'a, 'b>],
1287        _ctx: &dyn FunctionContext<'b>,
1288    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1289        if args.len() != 1 {
1290            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1291                ExcelError::new_value(),
1292            )));
1293        }
1294        let v = args[0].value()?.into_literal();
1295        match v {
1296            LiteralValue::Text(s) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Text(s))),
1297            LiteralValue::Error(e) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
1298            _ => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Text(
1299                String::new(),
1300            ))),
1301        }
1302    }
1303}
1304
1305/// ISEVEN(number) - Returns TRUE if number is even
1306#[derive(Debug)]
1307pub struct IsEvenFn;
1308/// Returns TRUE when a number is even.
1309///
1310/// # Remarks
1311/// - Numeric input is truncated toward zero before parity is checked.
1312/// - Booleans are coerced (`TRUE` -> 1, `FALSE` -> 0).
1313/// - Non-numeric text returns `#VALUE!`.
1314/// - Errors propagate unchanged.
1315///
1316/// # Examples
1317///
1318/// ```yaml,sandbox
1319/// title: "Even integer"
1320/// formula: '=ISEVEN(6)'
1321/// expected: true
1322/// ```
1323///
1324/// ```yaml,sandbox
1325/// title: "Decimal truncation before parity"
1326/// formula: '=ISEVEN(3.9)'
1327/// expected: false
1328/// ```
1329///
1330/// ```yaml,docs
1331/// related:
1332///   - ISODD
1333///   - ISNUMBER
1334///   - N
1335/// faq:
1336///   - q: "How are decimals handled by ISEVEN?"
1337///     a: "The number is truncated toward zero before checking even/odd parity."
1338/// ```
1339/// [formualizer-docgen:schema:start]
1340/// Name: ISEVEN
1341/// Type: IsEvenFn
1342/// Min args: 1
1343/// Max args: 1
1344/// Variadic: false
1345/// Signature: ISEVEN(arg1: any@scalar)
1346/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
1347/// Caps: PURE
1348/// [formualizer-docgen:schema:end]
1349impl Function for IsEvenFn {
1350    func_caps!(PURE);
1351    fn name(&self) -> &'static str {
1352        "ISEVEN"
1353    }
1354    fn min_args(&self) -> usize {
1355        1
1356    }
1357    fn arg_schema(&self) -> &'static [ArgSchema] {
1358        &ARG_ANY_ONE[..]
1359    }
1360    fn eval<'a, 'b, 'c>(
1361        &self,
1362        args: &'c [ArgumentHandle<'a, 'b>],
1363        _ctx: &dyn FunctionContext<'b>,
1364    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1365        if args.len() != 1 {
1366            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1367                ExcelError::new_value(),
1368            )));
1369        }
1370        let v = args[0].value()?.into_literal();
1371        let n = match v {
1372            LiteralValue::Error(e) => {
1373                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
1374            }
1375            LiteralValue::Int(i) => i as f64,
1376            LiteralValue::Number(n) => n,
1377            LiteralValue::Boolean(b) => {
1378                if b {
1379                    1.0
1380                } else {
1381                    0.0
1382                }
1383            }
1384            _ => {
1385                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1386                    ExcelError::new_value(),
1387                )));
1388            }
1389        };
1390        // Excel truncates to integer first
1391        let n = n.trunc() as i64;
1392        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Boolean(
1393            n % 2 == 0,
1394        )))
1395    }
1396}
1397
1398/// ISODD(number) - Returns TRUE if number is odd
1399#[derive(Debug)]
1400pub struct IsOddFn;
1401/// Returns TRUE when a number is odd.
1402///
1403/// # Remarks
1404/// - Numeric input is truncated toward zero before parity is checked.
1405/// - Booleans are coerced (`TRUE` -> 1, `FALSE` -> 0).
1406/// - Non-numeric text returns `#VALUE!`.
1407/// - Errors propagate unchanged.
1408///
1409/// # Examples
1410///
1411/// ```yaml,sandbox
1412/// title: "Odd integer"
1413/// formula: '=ISODD(7)'
1414/// expected: true
1415/// ```
1416///
1417/// ```yaml,sandbox
1418/// title: "Boolean coercion"
1419/// formula: '=ISODD(TRUE)'
1420/// expected: true
1421/// ```
1422///
1423/// ```yaml,docs
1424/// related:
1425///   - ISEVEN
1426///   - ISNUMBER
1427///   - N
1428/// faq:
1429///   - q: "Are booleans valid inputs for ISODD?"
1430///     a: "Yes. TRUE is treated as 1 and FALSE as 0 before the odd check."
1431/// ```
1432/// [formualizer-docgen:schema:start]
1433/// Name: ISODD
1434/// Type: IsOddFn
1435/// Min args: 1
1436/// Max args: 1
1437/// Variadic: false
1438/// Signature: ISODD(arg1: any@scalar)
1439/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
1440/// Caps: PURE
1441/// [formualizer-docgen:schema:end]
1442impl Function for IsOddFn {
1443    func_caps!(PURE);
1444    fn name(&self) -> &'static str {
1445        "ISODD"
1446    }
1447    fn min_args(&self) -> usize {
1448        1
1449    }
1450    fn arg_schema(&self) -> &'static [ArgSchema] {
1451        &ARG_ANY_ONE[..]
1452    }
1453    fn eval<'a, 'b, 'c>(
1454        &self,
1455        args: &'c [ArgumentHandle<'a, 'b>],
1456        _ctx: &dyn FunctionContext<'b>,
1457    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1458        if args.len() != 1 {
1459            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1460                ExcelError::new_value(),
1461            )));
1462        }
1463        let v = args[0].value()?.into_literal();
1464        let n = match v {
1465            LiteralValue::Error(e) => {
1466                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
1467            }
1468            LiteralValue::Int(i) => i as f64,
1469            LiteralValue::Number(n) => n,
1470            LiteralValue::Boolean(b) => {
1471                if b {
1472                    1.0
1473                } else {
1474                    0.0
1475                }
1476            }
1477            _ => {
1478                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1479                    ExcelError::new_value(),
1480                )));
1481            }
1482        };
1483        let n = n.trunc() as i64;
1484        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Boolean(
1485            n % 2 != 0,
1486        )))
1487    }
1488}
1489
1490/// ERROR.TYPE(error_val) - Returns a number corresponding to an error type
1491/// Returns:
1492///   1 = #NULL!
1493///   2 = #DIV/0!
1494///   3 = #VALUE!
1495///   4 = #REF!
1496///   5 = #NAME?
1497///   6 = #NUM!
1498///   7 = #N/A
1499///   8 = #GETTING_DATA (not commonly used)
1500///   #N/A if the value is not an error
1501///
1502/// NOTE: Error codes 9-13 are non-standard extensions for internal error types.
1503#[derive(Debug)]
1504pub struct ErrorTypeFn;
1505/// Returns the numeric code for a specific error value.
1506///
1507/// # Remarks
1508/// - Standard mappings include: `#NULL!`=1, `#DIV/0!`=2, `#VALUE!`=3, `#REF!`=4, `#NAME?`=5, `#NUM!`=6, `#N/A`=7.
1509/// - Non-error inputs return `#N/A`.
1510/// - Additional internal error kinds may map to extended non-standard codes.
1511///
1512/// # Examples
1513///
1514/// ```yaml,sandbox
1515/// title: "Map DIV/0 to code"
1516/// formula: '=ERROR.TYPE(1/0)'
1517/// expected: 2
1518/// ```
1519///
1520/// ```yaml,sandbox
1521/// title: "Non-error input returns N/A"
1522/// formula: '=ERROR.TYPE(10)'
1523/// expected: "#N/A"
1524/// ```
1525///
1526/// ```yaml,docs
1527/// related:
1528///   - ISERROR
1529///   - ISNA
1530///   - IFERROR
1531/// faq:
1532///   - q: "What if the input is not an error value?"
1533///     a: "ERROR.TYPE returns #N/A when the input is not an error."
1534/// ```
1535/// [formualizer-docgen:schema:start]
1536/// Name: ERROR.TYPE
1537/// Type: ErrorTypeFn
1538/// Min args: 1
1539/// Max args: 1
1540/// Variadic: false
1541/// Signature: ERROR.TYPE(arg1: any@scalar)
1542/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
1543/// Caps: PURE
1544/// [formualizer-docgen:schema:end]
1545impl Function for ErrorTypeFn {
1546    func_caps!(PURE);
1547    fn name(&self) -> &'static str {
1548        "ERROR.TYPE"
1549    }
1550    fn min_args(&self) -> usize {
1551        1
1552    }
1553    fn arg_schema(&self) -> &'static [ArgSchema] {
1554        &ARG_ANY_ONE[..]
1555    }
1556    fn eval<'a, 'b, 'c>(
1557        &self,
1558        args: &'c [ArgumentHandle<'a, 'b>],
1559        _ctx: &dyn FunctionContext<'b>,
1560    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1561        if args.len() != 1 {
1562            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1563                ExcelError::new_value(),
1564            )));
1565        }
1566        let v = args[0].value()?.into_literal();
1567        match v {
1568            LiteralValue::Error(e) => {
1569                let code = match e.kind {
1570                    ExcelErrorKind::Null => 1,
1571                    ExcelErrorKind::Div => 2,
1572                    ExcelErrorKind::Value => 3,
1573                    ExcelErrorKind::Ref => 4,
1574                    ExcelErrorKind::Name => 5,
1575                    ExcelErrorKind::Num => 6,
1576                    ExcelErrorKind::Na => 7,
1577                    ExcelErrorKind::Error => 8,
1578                    // Non-standard extensions (codes 9-13)
1579                    ExcelErrorKind::NImpl => 9,
1580                    ExcelErrorKind::Spill => 10,
1581                    ExcelErrorKind::Calc => 11,
1582                    ExcelErrorKind::Circ => 12,
1583                    ExcelErrorKind::Cancelled => 13,
1584                };
1585                Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(code)))
1586            }
1587            _ => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1588                ExcelError::new_na(),
1589            ))),
1590        }
1591    }
1592}
1593
1594/// Returns TRUE when the value is anything other than text.
1595///
1596/// # Remarks
1597/// - Text literals return FALSE.
1598/// - Numbers, booleans, blanks, and errors return TRUE.
1599/// - This is the logical complement of `ISTEXT` in the current engine semantics.
1600///
1601/// # Examples
1602///
1603/// ```excel
1604/// =ISNONTEXT(42)
1605/// ```
1606///
1607/// ```yaml,sandbox
1608/// title: "Number is non-text"
1609/// formula: '=ISNONTEXT(42)'
1610/// expected: true
1611/// ```
1612///
1613/// ```yaml,sandbox
1614/// title: "Text is not non-text"
1615/// formula: '=ISNONTEXT("alpha")'
1616/// expected: false
1617/// ```
1618///
1619/// ```yaml,docs
1620/// related:
1621///   - ISTEXT
1622///   - TYPE
1623/// faq:
1624///   - q: "Do errors count as non-text values?"
1625///     a: "Yes. This implementation treats any non-text value, including errors, as TRUE for ISNONTEXT."
1626/// ```
1627/// [formualizer-docgen:schema:start]
1628/// Name: ISNONTEXT
1629/// Type: IsNonTextFn
1630/// Min args: 1
1631/// Max args: 1
1632/// Variadic: false
1633/// Signature: ISNONTEXT(arg1: any@scalar)
1634/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
1635/// Caps: PURE
1636/// [formualizer-docgen:schema:end]
1637#[derive(Debug)]
1638pub struct IsNonTextFn;
1639/// [formualizer-docgen:schema:start]
1640/// Name: ISNONTEXT
1641/// Type: IsNonTextFn
1642/// Min args: 1
1643/// Max args: 1
1644/// Variadic: false
1645/// Signature: ISNONTEXT(arg1: any@scalar)
1646/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
1647/// Caps: PURE
1648/// [formualizer-docgen:schema:end]
1649impl Function for IsNonTextFn {
1650    func_caps!(PURE);
1651    fn name(&self) -> &'static str {
1652        "ISNONTEXT"
1653    }
1654    fn min_args(&self) -> usize {
1655        1
1656    }
1657    fn dependency_contract(&self, arity: usize) -> Option<FunctionDependencyContract> {
1658        FunctionDependencyContract::static_scalar_all_args(arity)
1659    }
1660    fn arg_schema(&self) -> &'static [ArgSchema] {
1661        &ARG_ANY_ONE[..]
1662    }
1663    fn eval<'a, 'b, 'c>(
1664        &self,
1665        args: &'c [ArgumentHandle<'a, 'b>],
1666        _ctx: &dyn FunctionContext<'b>,
1667    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1668        if args.len() != 1 {
1669            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1670                ExcelError::new_value(),
1671            )));
1672        }
1673        let v = args[0].value()?.into_literal();
1674        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Boolean(
1675            !matches!(v, LiteralValue::Text(_)),
1676        )))
1677    }
1678}
1679
1680pub fn register_builtins() {
1681    use std::sync::Arc;
1682    crate::function_registry::register_function(Arc::new(IsNumberFn));
1683    crate::function_registry::register_function(Arc::new(IsTextFn));
1684    crate::function_registry::register_function(Arc::new(IsNonTextFn));
1685    crate::function_registry::register_function(Arc::new(IsLogicalFn));
1686    crate::function_registry::register_function(Arc::new(IsBlankFn));
1687    crate::function_registry::register_function(Arc::new(IsErrorFn));
1688    crate::function_registry::register_function(Arc::new(IsErrFn));
1689    crate::function_registry::register_function(Arc::new(IsNaFn));
1690    crate::function_registry::register_function(Arc::new(IsFormulaFn));
1691    crate::function_registry::register_function(Arc::new(IsRefFn));
1692    crate::function_registry::register_function(Arc::new(FormulaTextFn));
1693    crate::function_registry::register_function(Arc::new(SheetFn));
1694    crate::function_registry::register_function(Arc::new(SheetsFn));
1695    crate::function_registry::register_function(Arc::new(IsEvenFn));
1696    crate::function_registry::register_function(Arc::new(IsOddFn));
1697    crate::function_registry::register_function(Arc::new(ErrorTypeFn));
1698    crate::function_registry::register_function(Arc::new(TypeFn));
1699    crate::function_registry::register_function(Arc::new(NaFn));
1700    crate::function_registry::register_function(Arc::new(NFn));
1701    crate::function_registry::register_function(Arc::new(TFn));
1702}
1703
1704#[cfg(test)]
1705mod tests {
1706    use super::*;
1707    use crate::test_workbook::TestWorkbook;
1708    use formualizer_parse::parser::{ASTNode, ASTNodeType};
1709    fn interp(wb: &TestWorkbook) -> crate::interpreter::Interpreter<'_> {
1710        wb.interpreter()
1711    }
1712
1713    #[test]
1714    fn isnumber_numeric_and_date() {
1715        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(IsNumberFn));
1716        let ctx = interp(&wb);
1717        let f = ctx.context.get_function("", "ISNUMBER").unwrap();
1718        let num = ASTNode::new(
1719            ASTNodeType::Literal(LiteralValue::Number(std::f64::consts::PI)),
1720            None,
1721        );
1722        let date = ASTNode::new(
1723            ASTNodeType::Literal(LiteralValue::Date(
1724                chrono::NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1725            )),
1726            None,
1727        );
1728        let txt = ASTNode::new(ASTNodeType::Literal(LiteralValue::Text("x".into())), None);
1729        let args_num = vec![crate::traits::ArgumentHandle::new(&num, &ctx)];
1730        let args_date = vec![crate::traits::ArgumentHandle::new(&date, &ctx)];
1731        let args_txt = vec![crate::traits::ArgumentHandle::new(&txt, &ctx)];
1732        assert_eq!(
1733            f.dispatch(&args_num, &ctx.function_context(None))
1734                .unwrap()
1735                .into_literal(),
1736            LiteralValue::Boolean(true)
1737        );
1738        assert_eq!(
1739            f.dispatch(&args_date, &ctx.function_context(None))
1740                .unwrap()
1741                .into_literal(),
1742            LiteralValue::Boolean(true)
1743        );
1744        assert_eq!(
1745            f.dispatch(&args_txt, &ctx.function_context(None))
1746                .unwrap()
1747                .into_literal(),
1748            LiteralValue::Boolean(false)
1749        );
1750    }
1751
1752    #[test]
1753    fn istest_and_isblank() {
1754        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(IsTextFn));
1755        let ctx = interp(&wb);
1756        let f = ctx.context.get_function("", "ISTEXT").unwrap();
1757        let t = ASTNode::new(ASTNodeType::Literal(LiteralValue::Text("abc".into())), None);
1758        let n = ASTNode::new(ASTNodeType::Literal(LiteralValue::Int(5)), None);
1759        let args_t = vec![crate::traits::ArgumentHandle::new(&t, &ctx)];
1760        let args_n = vec![crate::traits::ArgumentHandle::new(&n, &ctx)];
1761        assert_eq!(
1762            f.dispatch(&args_t, &ctx.function_context(None))
1763                .unwrap()
1764                .into_literal(),
1765            LiteralValue::Boolean(true)
1766        );
1767        assert_eq!(
1768            f.dispatch(&args_n, &ctx.function_context(None))
1769                .unwrap()
1770                .into_literal(),
1771            LiteralValue::Boolean(false)
1772        );
1773
1774        // ISBLANK
1775        let wb2 = TestWorkbook::new().with_function(std::sync::Arc::new(IsBlankFn));
1776        let ctx2 = interp(&wb2);
1777        let f2 = ctx2.context.get_function("", "ISBLANK").unwrap();
1778        let blank = ASTNode::new(ASTNodeType::Literal(LiteralValue::Empty), None);
1779        let blank_args = vec![crate::traits::ArgumentHandle::new(&blank, &ctx2)];
1780        assert_eq!(
1781            f2.dispatch(&blank_args, &ctx2.function_context(None))
1782                .unwrap()
1783                .into_literal(),
1784            LiteralValue::Boolean(true)
1785        );
1786    }
1787
1788    #[test]
1789    fn iserror_variants() {
1790        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(IsErrorFn));
1791        let ctx = interp(&wb);
1792        let f = ctx.context.get_function("", "ISERROR").unwrap();
1793        let err = ASTNode::new(
1794            ASTNodeType::Literal(LiteralValue::Error(ExcelError::new(ExcelErrorKind::Div))),
1795            None,
1796        );
1797        let ok = ASTNode::new(ASTNodeType::Literal(LiteralValue::Int(1)), None);
1798        let a_err = vec![crate::traits::ArgumentHandle::new(&err, &ctx)];
1799        let a_ok = vec![crate::traits::ArgumentHandle::new(&ok, &ctx)];
1800        assert_eq!(
1801            f.dispatch(&a_err, &ctx.function_context(None))
1802                .unwrap()
1803                .into_literal(),
1804            LiteralValue::Boolean(true)
1805        );
1806        assert_eq!(
1807            f.dispatch(&a_ok, &ctx.function_context(None))
1808                .unwrap()
1809                .into_literal(),
1810            LiteralValue::Boolean(false)
1811        );
1812    }
1813
1814    #[test]
1815    fn type_codes_basic() {
1816        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(TypeFn));
1817        let ctx = interp(&wb);
1818        let f = ctx.context.get_function("", "TYPE").unwrap();
1819        let v_num = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(2.0)), None);
1820        let v_txt = ASTNode::new(ASTNodeType::Literal(LiteralValue::Text("hi".into())), None);
1821        let v_bool = ASTNode::new(ASTNodeType::Literal(LiteralValue::Boolean(true)), None);
1822        let v_err = ASTNode::new(
1823            ASTNodeType::Literal(LiteralValue::Error(ExcelError::new(ExcelErrorKind::Value))),
1824            None,
1825        );
1826        let v_arr = ASTNode::new(
1827            ASTNodeType::Literal(LiteralValue::Array(vec![vec![LiteralValue::Int(1)]])),
1828            None,
1829        );
1830        let a_num = vec![crate::traits::ArgumentHandle::new(&v_num, &ctx)];
1831        let a_txt = vec![crate::traits::ArgumentHandle::new(&v_txt, &ctx)];
1832        let a_bool = vec![crate::traits::ArgumentHandle::new(&v_bool, &ctx)];
1833        let a_err = vec![crate::traits::ArgumentHandle::new(&v_err, &ctx)];
1834        let a_arr = vec![crate::traits::ArgumentHandle::new(&v_arr, &ctx)];
1835        assert_eq!(
1836            f.dispatch(&a_num, &ctx.function_context(None))
1837                .unwrap()
1838                .into_literal(),
1839            LiteralValue::Int(1)
1840        );
1841        assert_eq!(
1842            f.dispatch(&a_txt, &ctx.function_context(None))
1843                .unwrap()
1844                .into_literal(),
1845            LiteralValue::Int(2)
1846        );
1847        assert_eq!(
1848            f.dispatch(&a_bool, &ctx.function_context(None))
1849                .unwrap()
1850                .into_literal(),
1851            LiteralValue::Int(4)
1852        );
1853        match f
1854            .dispatch(&a_err, &ctx.function_context(None))
1855            .unwrap()
1856            .into_literal()
1857        {
1858            LiteralValue::Error(e) => assert_eq!(e, "#VALUE!"),
1859            _ => panic!(),
1860        }
1861        assert_eq!(
1862            f.dispatch(&a_arr, &ctx.function_context(None))
1863                .unwrap()
1864                .into_literal(),
1865            LiteralValue::Int(64)
1866        );
1867    }
1868
1869    #[test]
1870    fn na_and_n_and_t() {
1871        let wb = TestWorkbook::new()
1872            .with_function(std::sync::Arc::new(NaFn))
1873            .with_function(std::sync::Arc::new(NFn))
1874            .with_function(std::sync::Arc::new(TFn));
1875        let ctx = wb.interpreter();
1876        // NA()
1877        let na_fn = ctx.context.get_function("", "NA").unwrap();
1878        match na_fn
1879            .eval(&[], &ctx.function_context(None))
1880            .unwrap()
1881            .into_literal()
1882        {
1883            LiteralValue::Error(e) => assert_eq!(e, "#N/A"),
1884            _ => panic!(),
1885        }
1886        // N()
1887        let n_fn = ctx.context.get_function("", "N").unwrap();
1888        let val = ASTNode::new(ASTNodeType::Literal(LiteralValue::Boolean(true)), None);
1889        let args = vec![crate::traits::ArgumentHandle::new(&val, &ctx)];
1890        assert_eq!(
1891            n_fn.dispatch(&args, &ctx.function_context(None))
1892                .unwrap()
1893                .into_literal(),
1894            LiteralValue::Int(1)
1895        );
1896        // T()
1897        let t_fn = ctx.context.get_function("", "T").unwrap();
1898        let txt = ASTNode::new(ASTNodeType::Literal(LiteralValue::Text("abc".into())), None);
1899        let args_t = vec![crate::traits::ArgumentHandle::new(&txt, &ctx)];
1900        assert_eq!(
1901            t_fn.dispatch(&args_t, &ctx.function_context(None))
1902                .unwrap()
1903                .into_literal(),
1904            LiteralValue::Text("abc".into())
1905        );
1906    }
1907}