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}