Skip to main content

formualizer_eval/builtins/math/
numeric.rs

1use super::super::utils::{
2    ARG_NUM_LENIENT_ONE, ARG_NUM_LENIENT_TWO, ARG_RANGE_NUM_LENIENT_ONE, coerce_num,
3};
4use crate::args::ArgSchema;
5use crate::function::Function;
6use crate::traits::{ArgumentHandle, FunctionContext};
7use formualizer_common::{ExcelError, LiteralValue};
8use formualizer_macros::func_caps;
9
10#[derive(Debug)]
11pub struct AbsFn;
12/// Returns the absolute value of a number.
13///
14/// # Remarks
15/// - Negative numbers are returned as positive values.
16/// - Zero and positive numbers are unchanged.
17/// - Errors are propagated.
18///
19/// # Examples
20/// ```yaml,sandbox
21/// title: "Absolute value of a negative number"
22/// formula: "=ABS(-12.5)"
23/// expected: 12.5
24/// ```
25///
26/// ```yaml,sandbox
27/// title: "Absolute value from a cell reference"
28/// grid:
29///   A1: -42
30/// formula: "=ABS(A1)"
31/// expected: 42
32/// ```
33///
34/// ```yaml,docs
35/// related:
36///   - SIGN
37///   - INT
38///   - MOD
39/// faq:
40///   - q: "How does ABS handle errors or non-numeric text?"
41///     a: "Input errors propagate, and non-coercible text returns a coercion error."
42/// ```
43/// [formualizer-docgen:schema:start]
44/// Name: ABS
45/// Type: AbsFn
46/// Min args: 1
47/// Max args: 1
48/// Variadic: false
49/// Signature: ABS(arg1: number@scalar)
50/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
51/// Caps: PURE
52/// [formualizer-docgen:schema:end]
53impl Function for AbsFn {
54    func_caps!(PURE);
55    fn name(&self) -> &'static str {
56        "ABS"
57    }
58    fn min_args(&self) -> usize {
59        1
60    }
61    fn arg_schema(&self) -> &'static [ArgSchema] {
62        &ARG_NUM_LENIENT_ONE[..]
63    }
64    fn eval<'a, 'b, 'c>(
65        &self,
66        args: &'c [ArgumentHandle<'a, 'b>],
67        _: &dyn FunctionContext<'b>,
68    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
69        let v = args[0].value()?.into_literal();
70        match v {
71            LiteralValue::Error(e) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
72            other => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
73                coerce_num(&other)?.abs(),
74            ))),
75        }
76    }
77}
78
79#[derive(Debug)]
80pub struct SignFn;
81/// Returns the sign of a number as -1, 0, or 1.
82///
83/// # Remarks
84/// - Returns `1` for positive numbers.
85/// - Returns `-1` for negative numbers.
86/// - Returns `0` when input is zero.
87///
88/// # Examples
89/// ```yaml,sandbox
90/// title: "Positive input"
91/// formula: "=SIGN(12)"
92/// expected: 1
93/// ```
94///
95/// ```yaml,sandbox
96/// title: "Negative input"
97/// formula: "=SIGN(-12)"
98/// expected: -1
99/// ```
100///
101/// ```yaml,docs
102/// related:
103///   - ABS
104///   - INT
105///   - IF
106/// faq:
107///   - q: "Can SIGN return anything other than -1, 0, or 1?"
108///     a: "No. After numeric coercion, the output is always exactly -1, 0, or 1."
109/// ```
110/// [formualizer-docgen:schema:start]
111/// Name: SIGN
112/// Type: SignFn
113/// Min args: 1
114/// Max args: 1
115/// Variadic: false
116/// Signature: SIGN(arg1: number@scalar)
117/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
118/// Caps: PURE
119/// [formualizer-docgen:schema:end]
120impl Function for SignFn {
121    func_caps!(PURE);
122    fn name(&self) -> &'static str {
123        "SIGN"
124    }
125    fn min_args(&self) -> usize {
126        1
127    }
128    fn arg_schema(&self) -> &'static [ArgSchema] {
129        &ARG_NUM_LENIENT_ONE[..]
130    }
131    fn eval<'a, 'b, 'c>(
132        &self,
133        args: &'c [ArgumentHandle<'a, 'b>],
134        _: &dyn FunctionContext<'b>,
135    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
136        let v = args[0].value()?.into_literal();
137        match v {
138            LiteralValue::Error(e) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
139            other => {
140                let n = coerce_num(&other)?;
141                Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
142                    if n > 0.0 {
143                        1.0
144                    } else if n < 0.0 {
145                        -1.0
146                    } else {
147                        0.0
148                    },
149                )))
150            }
151        }
152    }
153}
154
155#[derive(Debug)]
156pub struct IntFn; // floor toward -inf
157/// Rounds a number down to the nearest integer.
158///
159/// `INT` uses floor semantics, so negative values move farther from zero.
160///
161/// # Remarks
162/// - Equivalent to mathematical floor (`floor(x)`).
163/// - Coercion is lenient for numeric-like inputs; invalid values return an error.
164/// - Input errors are propagated.
165///
166/// # Examples
167/// ```yaml,sandbox
168/// title: "Drop decimal digits from a positive number"
169/// formula: "=INT(8.9)"
170/// expected: 8
171/// ```
172///
173/// ```yaml,sandbox
174/// title: "Floor a negative number"
175/// formula: "=INT(-8.9)"
176/// expected: -9
177/// ```
178///
179/// ```yaml,docs
180/// related:
181///   - TRUNC
182///   - ROUNDDOWN
183///   - FLOOR
184/// faq:
185///   - q: "Why is INT(-8.9) equal to -9 instead of -8?"
186///     a: "INT uses floor semantics, so negative values round toward negative infinity."
187/// ```
188/// [formualizer-docgen:schema:start]
189/// Name: INT
190/// Type: IntFn
191/// Min args: 1
192/// Max args: 1
193/// Variadic: false
194/// Signature: INT(arg1: number@scalar)
195/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
196/// Caps: PURE
197/// [formualizer-docgen:schema:end]
198impl Function for IntFn {
199    func_caps!(PURE);
200    fn name(&self) -> &'static str {
201        "INT"
202    }
203    fn min_args(&self) -> usize {
204        1
205    }
206    fn arg_schema(&self) -> &'static [ArgSchema] {
207        &ARG_NUM_LENIENT_ONE[..]
208    }
209    fn eval<'a, 'b, 'c>(
210        &self,
211        args: &'c [ArgumentHandle<'a, 'b>],
212        _: &dyn FunctionContext<'b>,
213    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
214        let v = args[0].value()?.into_literal();
215        match v {
216            LiteralValue::Error(e) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
217            other => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
218                coerce_num(&other)?.floor(),
219            ))),
220        }
221    }
222}
223
224#[derive(Debug)]
225pub struct TruncFn; // truncate toward zero
226/// Truncates a number toward zero, optionally at a specified digit position.
227///
228/// # Remarks
229/// - If `num_digits` is omitted, truncation is to an integer.
230/// - Positive `num_digits` keeps decimal places; negative values zero places to the left.
231/// - Passing more than two arguments returns `#VALUE!`.
232///
233/// # Examples
234/// ```yaml,sandbox
235/// title: "Truncate to two decimal places"
236/// formula: "=TRUNC(12.3456,2)"
237/// expected: 12.34
238/// ```
239///
240/// ```yaml,sandbox
241/// title: "Truncate toward zero at the hundreds place"
242/// formula: "=TRUNC(-987.65,-2)"
243/// expected: -900
244/// ```
245///
246/// ```yaml,docs
247/// related:
248///   - INT
249///   - ROUND
250///   - ROUNDDOWN
251/// faq:
252///   - q: "How does TRUNC differ from INT for negative numbers?"
253///     a: "TRUNC removes digits toward zero, while INT floors toward negative infinity."
254/// ```
255/// [formualizer-docgen:schema:start]
256/// Name: TRUNC
257/// Type: TruncFn
258/// Min args: 1
259/// Max args: variadic
260/// Variadic: true
261/// Signature: TRUNC(arg1: number@scalar, arg2...: number@scalar)
262/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
263/// Caps: PURE
264/// [formualizer-docgen:schema:end]
265impl Function for TruncFn {
266    func_caps!(PURE);
267    fn name(&self) -> &'static str {
268        "TRUNC"
269    }
270    fn min_args(&self) -> usize {
271        1
272    }
273    fn variadic(&self) -> bool {
274        true
275    }
276    fn arg_schema(&self) -> &'static [ArgSchema] {
277        &ARG_NUM_LENIENT_TWO[..]
278    }
279    fn eval<'a, 'b, 'c>(
280        &self,
281        args: &'c [ArgumentHandle<'a, 'b>],
282        _: &dyn FunctionContext<'b>,
283    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
284        if args.is_empty() || args.len() > 2 {
285            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
286                ExcelError::new_value(),
287            )));
288        }
289        let mut n = match args[0].value()?.into_literal() {
290            LiteralValue::Error(e) => {
291                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
292            }
293            other => coerce_num(&other)?,
294        };
295        let digits: i32 = if args.len() == 2 {
296            match args[1].value()?.into_literal() {
297                LiteralValue::Error(e) => {
298                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
299                }
300                other => coerce_num(&other)? as i32,
301            }
302        } else {
303            0
304        };
305        if digits >= 0 {
306            let f = 10f64.powi(digits);
307            n = (n * f).trunc() / f;
308        } else {
309            let f = 10f64.powi(-digits);
310            n = (n / f).trunc() * f;
311        }
312        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(n)))
313    }
314}
315
316#[derive(Debug)]
317pub struct RoundFn; // ROUND(number, digits)
318/// Rounds a number to a specified number of digits.
319///
320/// # Remarks
321/// - Positive `digits` rounds to the right of the decimal point.
322/// - Negative `digits` rounds to the left of the decimal point.
323/// - Uses standard half-up style rounding from Rust's `round` behavior.
324///
325/// # Examples
326/// ```yaml,sandbox
327/// title: "Round to two decimals"
328/// formula: "=ROUND(3.14159,2)"
329/// expected: 3.14
330/// ```
331///
332/// ```yaml,sandbox
333/// title: "Round to nearest hundred"
334/// formula: "=ROUND(1234,-2)"
335/// expected: 1200
336/// ```
337///
338/// ```yaml,docs
339/// related:
340///   - ROUNDUP
341///   - ROUNDDOWN
342///   - MROUND
343/// faq:
344///   - q: "What does a negative digits argument do in ROUND?"
345///     a: "It rounds digits to the left of the decimal point (for example, tens or hundreds)."
346/// ```
347/// [formualizer-docgen:schema:start]
348/// Name: ROUND
349/// Type: RoundFn
350/// Min args: 2
351/// Max args: 2
352/// Variadic: false
353/// Signature: ROUND(arg1: number@scalar, arg2: number@scalar)
354/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
355/// Caps: PURE
356/// [formualizer-docgen:schema:end]
357impl Function for RoundFn {
358    func_caps!(PURE);
359    fn name(&self) -> &'static str {
360        "ROUND"
361    }
362    fn min_args(&self) -> usize {
363        2
364    }
365    fn arg_schema(&self) -> &'static [ArgSchema] {
366        &ARG_NUM_LENIENT_TWO[..]
367    }
368    fn eval<'a, 'b, 'c>(
369        &self,
370        args: &'c [ArgumentHandle<'a, 'b>],
371        _: &dyn FunctionContext<'b>,
372    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
373        let n = match args[0].value()?.into_literal() {
374            LiteralValue::Error(e) => {
375                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
376            }
377            other => coerce_num(&other)?,
378        };
379        let digits = match args[1].value()?.into_literal() {
380            LiteralValue::Error(e) => {
381                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
382            }
383            other => coerce_num(&other)? as i32,
384        };
385        let f = 10f64.powi(digits.abs());
386        let out = if digits >= 0 {
387            (n * f).round() / f
388        } else {
389            (n / f).round() * f
390        };
391        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(out)))
392    }
393}
394
395#[derive(Debug)]
396pub struct RoundDownFn; // toward zero
397/// Rounds a number toward zero to a specified number of digits.
398///
399/// # Remarks
400/// - Positive `num_digits` affects decimals; negative values affect digits left of the decimal.
401/// - Always reduces magnitude toward zero (unlike `INT` for negatives).
402/// - Input errors are propagated.
403///
404/// # Examples
405/// ```yaml,sandbox
406/// title: "Trim decimals without rounding up"
407/// formula: "=ROUNDDOWN(3.14159,3)"
408/// expected: 3.141
409/// ```
410///
411/// ```yaml,sandbox
412/// title: "Round down a negative value at the hundreds place"
413/// formula: "=ROUNDDOWN(-987.65,-2)"
414/// expected: -900
415/// ```
416///
417/// ```yaml,docs
418/// related:
419///   - ROUND
420///   - ROUNDUP
421///   - TRUNC
422/// faq:
423///   - q: "Does ROUNDDOWN always move toward negative infinity?"
424///     a: "No. It moves toward zero, which is different from FLOOR-style behavior on negatives."
425/// ```
426/// [formualizer-docgen:schema:start]
427/// Name: ROUNDDOWN
428/// Type: RoundDownFn
429/// Min args: 2
430/// Max args: 2
431/// Variadic: false
432/// Signature: ROUNDDOWN(arg1: number@scalar, arg2: number@scalar)
433/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
434/// Caps: PURE
435/// [formualizer-docgen:schema:end]
436impl Function for RoundDownFn {
437    func_caps!(PURE);
438    fn name(&self) -> &'static str {
439        "ROUNDDOWN"
440    }
441    fn min_args(&self) -> usize {
442        2
443    }
444    fn arg_schema(&self) -> &'static [ArgSchema] {
445        &ARG_NUM_LENIENT_TWO[..]
446    }
447    fn eval<'a, 'b, 'c>(
448        &self,
449        args: &'c [ArgumentHandle<'a, 'b>],
450        _: &dyn FunctionContext<'b>,
451    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
452        let n = match args[0].value()?.into_literal() {
453            LiteralValue::Error(e) => {
454                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
455            }
456            other => coerce_num(&other)?,
457        };
458        let digits = match args[1].value()?.into_literal() {
459            LiteralValue::Error(e) => {
460                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
461            }
462            other => coerce_num(&other)? as i32,
463        };
464        let f = 10f64.powi(digits.abs());
465        let out = if digits >= 0 {
466            (n * f).trunc() / f
467        } else {
468            (n / f).trunc() * f
469        };
470        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(out)))
471    }
472}
473
474#[derive(Debug)]
475pub struct RoundUpFn; // away from zero
476/// Rounds a number away from zero to a specified number of digits.
477///
478/// # Remarks
479/// - Positive `num_digits` affects decimals; negative values affect digits left of the decimal.
480/// - Any discarded non-zero part increases the magnitude of the result.
481/// - Input errors are propagated.
482///
483/// # Examples
484/// ```yaml,sandbox
485/// title: "Round up decimals away from zero"
486/// formula: "=ROUNDUP(3.14159,3)"
487/// expected: 3.142
488/// ```
489///
490/// ```yaml,sandbox
491/// title: "Round up a negative value at the hundreds place"
492/// formula: "=ROUNDUP(-987.65,-2)"
493/// expected: -1000
494/// ```
495///
496/// ```yaml,docs
497/// related:
498///   - ROUND
499///   - ROUNDDOWN
500///   - CEILING
501/// faq:
502///   - q: "What does ROUNDUP do when discarded digits are already zero?"
503///     a: "It leaves the value unchanged because no non-zero discarded part remains."
504/// ```
505/// [formualizer-docgen:schema:start]
506/// Name: ROUNDUP
507/// Type: RoundUpFn
508/// Min args: 2
509/// Max args: 2
510/// Variadic: false
511/// Signature: ROUNDUP(arg1: number@scalar, arg2: number@scalar)
512/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
513/// Caps: PURE
514/// [formualizer-docgen:schema:end]
515impl Function for RoundUpFn {
516    func_caps!(PURE);
517    fn name(&self) -> &'static str {
518        "ROUNDUP"
519    }
520    fn min_args(&self) -> usize {
521        2
522    }
523    fn arg_schema(&self) -> &'static [ArgSchema] {
524        &ARG_NUM_LENIENT_TWO[..]
525    }
526    fn eval<'a, 'b, 'c>(
527        &self,
528        args: &'c [ArgumentHandle<'a, 'b>],
529        _: &dyn FunctionContext<'b>,
530    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
531        let n = match args[0].value()?.into_literal() {
532            LiteralValue::Error(e) => {
533                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
534            }
535            other => coerce_num(&other)?,
536        };
537        let digits = match args[1].value()?.into_literal() {
538            LiteralValue::Error(e) => {
539                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
540            }
541            other => coerce_num(&other)? as i32,
542        };
543        let f = 10f64.powi(digits.abs());
544        let mut scaled = if digits >= 0 { n * f } else { n / f };
545        if scaled > 0.0 {
546            scaled = scaled.ceil();
547        } else {
548            scaled = scaled.floor();
549        }
550        let out = if digits >= 0 { scaled / f } else { scaled * f };
551        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(out)))
552    }
553}
554
555#[derive(Debug)]
556pub struct ModFn; // MOD(a,b)
557/// Returns the remainder after division, with the sign of the divisor.
558///
559/// # Remarks
560/// - If divisor is `0`, returns `#DIV/0!`.
561/// - Result sign follows Excel-style MOD semantics (sign of divisor).
562/// - Errors in either argument are propagated.
563///
564/// # Examples
565/// ```yaml,sandbox
566/// title: "Positive divisor"
567/// formula: "=MOD(10,3)"
568/// expected: 1
569/// ```
570///
571/// ```yaml,sandbox
572/// title: "Negative dividend"
573/// formula: "=MOD(-3,2)"
574/// expected: 1
575/// ```
576///
577/// ```yaml,docs
578/// related:
579///   - QUOTIENT
580///   - INT
581///   - GCD
582/// faq:
583///   - q: "Why can MOD return a positive value for a negative dividend?"
584///     a: "MOD follows the sign of the divisor, matching Excel's modulo semantics."
585/// ```
586/// [formualizer-docgen:schema:start]
587/// Name: MOD
588/// Type: ModFn
589/// Min args: 2
590/// Max args: 2
591/// Variadic: false
592/// Signature: MOD(arg1: number@scalar, arg2: number@scalar)
593/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
594/// Caps: PURE
595/// [formualizer-docgen:schema:end]
596impl Function for ModFn {
597    func_caps!(PURE);
598    fn name(&self) -> &'static str {
599        "MOD"
600    }
601    fn min_args(&self) -> usize {
602        2
603    }
604    fn arg_schema(&self) -> &'static [ArgSchema] {
605        &ARG_NUM_LENIENT_TWO[..]
606    }
607    fn eval<'a, 'b, 'c>(
608        &self,
609        args: &'c [ArgumentHandle<'a, 'b>],
610        _: &dyn FunctionContext<'b>,
611    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
612        let x = match args[0].value()?.into_literal() {
613            LiteralValue::Error(e) => {
614                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
615            }
616            other => coerce_num(&other)?,
617        };
618        let y = match args[1].value()?.into_literal() {
619            LiteralValue::Error(e) => {
620                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
621            }
622            other => coerce_num(&other)?,
623        };
624        if y == 0.0 {
625            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
626                ExcelError::from_error_string("#DIV/0!"),
627            )));
628        }
629        let m = x % y;
630        let mut r = if m == 0.0 {
631            0.0
632        } else if (y > 0.0 && m < 0.0) || (y < 0.0 && m > 0.0) {
633            m + y
634        } else {
635            m
636        };
637        if r == -0.0 {
638            r = 0.0;
639        }
640        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(r)))
641    }
642}
643
644/* ───────────────────── Additional Math / Rounding ───────────────────── */
645
646#[derive(Debug)]
647pub struct CeilingFn; // CEILING(number, [significance]) legacy semantics simplified
648/// Rounds a number up to the nearest multiple of a significance.
649///
650/// This implementation defaults significance to `1` and normalizes negative significance to positive.
651///
652/// # Remarks
653/// - If `significance` is omitted, `1` is used.
654/// - `significance = 0` returns `#DIV/0!`.
655/// - Negative significance is treated as its absolute value in this fallback behavior.
656///
657/// # Examples
658/// ```yaml,sandbox
659/// title: "Round up to the nearest multiple"
660/// formula: "=CEILING(5.1,2)"
661/// expected: 6
662/// ```
663///
664/// ```yaml,sandbox
665/// title: "Round a negative number toward positive infinity"
666/// formula: "=CEILING(-5.1,2)"
667/// expected: -4
668/// ```
669///
670/// ```yaml,docs
671/// related:
672///   - CEILING.MATH
673///   - FLOOR
674///   - ROUNDUP
675/// faq:
676///   - q: "What happens if CEILING significance is 0?"
677///     a: "It returns #DIV/0! because a zero multiple is invalid."
678/// ```
679/// [formualizer-docgen:schema:start]
680/// Name: CEILING
681/// Type: CeilingFn
682/// Min args: 1
683/// Max args: variadic
684/// Variadic: true
685/// Signature: CEILING(arg1: number@scalar, arg2...: number@scalar)
686/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
687/// Caps: PURE
688/// [formualizer-docgen:schema:end]
689impl Function for CeilingFn {
690    func_caps!(PURE);
691    fn name(&self) -> &'static str {
692        "CEILING"
693    }
694    fn min_args(&self) -> usize {
695        1
696    }
697    fn variadic(&self) -> bool {
698        true
699    }
700    fn arg_schema(&self) -> &'static [ArgSchema] {
701        &ARG_NUM_LENIENT_TWO[..]
702    }
703    fn eval<'a, 'b, 'c>(
704        &self,
705        args: &'c [ArgumentHandle<'a, 'b>],
706        _: &dyn FunctionContext<'b>,
707    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
708        if args.is_empty() || args.len() > 2 {
709            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
710                ExcelError::new_value(),
711            )));
712        }
713        let n = match args[0].value()?.into_literal() {
714            LiteralValue::Error(e) => {
715                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
716            }
717            other => coerce_num(&other)?,
718        };
719        let mut sig = if args.len() == 2 {
720            match args[1].value()?.into_literal() {
721                LiteralValue::Error(e) => {
722                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
723                }
724                other => coerce_num(&other)?,
725            }
726        } else {
727            1.0
728        };
729        if sig == 0.0 {
730            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
731                ExcelError::from_error_string("#DIV/0!"),
732            )));
733        }
734        if sig < 0.0 {
735            sig = sig.abs(); /* Excel nuances: #NUM! when sign mismatch; simplified TODO */
736        }
737        let k = (n / sig).ceil();
738        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
739            k * sig,
740        )))
741    }
742}
743
744#[derive(Debug)]
745pub struct CeilingMathFn; // CEILING.MATH(number,[significance],[mode])
746/// Rounds a number up to the nearest integer or multiple using `CEILING.MATH` rules.
747///
748/// # Remarks
749/// - If `significance` is omitted (or passed as `0`), the function uses `1`.
750/// - `significance` is treated as a positive magnitude.
751/// - For negative numbers, non-zero `mode` rounds away from zero; otherwise it rounds toward positive infinity.
752///
753/// # Examples
754/// ```yaml,sandbox
755/// title: "Default behavior for a positive number"
756/// formula: "=CEILING.MATH(24.3,5)"
757/// expected: 25
758/// ```
759///
760/// ```yaml,sandbox
761/// title: "Use mode to round a negative number away from zero"
762/// formula: "=CEILING.MATH(-24.3,5,1)"
763/// expected: -25
764/// ```
765///
766/// ```yaml,docs
767/// related:
768///   - CEILING
769///   - FLOOR.MATH
770///   - ROUNDUP
771/// faq:
772///   - q: "How does mode affect negative numbers in CEILING.MATH?"
773///     a: "With non-zero mode, negatives round away from zero; otherwise they round toward +infinity."
774/// ```
775/// [formualizer-docgen:schema:start]
776/// Name: CEILING.MATH
777/// Type: CeilingMathFn
778/// Min args: 1
779/// Max args: variadic
780/// Variadic: true
781/// Signature: CEILING.MATH(arg1: number@scalar, arg2...: number@scalar)
782/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
783/// Caps: PURE
784/// [formualizer-docgen:schema:end]
785impl Function for CeilingMathFn {
786    func_caps!(PURE);
787    fn name(&self) -> &'static str {
788        "CEILING.MATH"
789    }
790    fn min_args(&self) -> usize {
791        1
792    }
793    fn variadic(&self) -> bool {
794        true
795    }
796    fn arg_schema(&self) -> &'static [ArgSchema] {
797        &ARG_NUM_LENIENT_TWO[..]
798    } // allow up to 3 handled manually
799    fn eval<'a, 'b, 'c>(
800        &self,
801        args: &'c [ArgumentHandle<'a, 'b>],
802        _: &dyn FunctionContext<'b>,
803    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
804        if args.is_empty() || args.len() > 3 {
805            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
806                ExcelError::new_value(),
807            )));
808        }
809        let n = match args[0].value()?.into_literal() {
810            LiteralValue::Error(e) => {
811                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
812            }
813            other => coerce_num(&other)?,
814        };
815        let sig = if args.len() >= 2 {
816            match args[1].value()?.into_literal() {
817                LiteralValue::Error(e) => {
818                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
819                }
820                other => {
821                    let v = coerce_num(&other)?;
822                    if v == 0.0 { 1.0 } else { v.abs() }
823                }
824            }
825        } else {
826            1.0
827        };
828        let mode_nonzero = if args.len() == 3 {
829            match args[2].value()?.into_literal() {
830                LiteralValue::Error(e) => {
831                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
832                }
833                other => coerce_num(&other)? != 0.0,
834            }
835        } else {
836            false
837        };
838        let result = if n >= 0.0 {
839            (n / sig).ceil() * sig
840        } else if mode_nonzero {
841            (n / sig).floor() * sig /* away from zero */
842        } else {
843            (n / sig).ceil() * sig /* toward +inf (less negative) */
844        };
845        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
846            result,
847        )))
848    }
849}
850
851#[derive(Debug)]
852pub struct FloorFn; // FLOOR(number,[significance])
853/// Rounds a number down to the nearest multiple of a significance.
854///
855/// This implementation defaults significance to `1` and normalizes negative significance to positive.
856///
857/// # Remarks
858/// - If `significance` is omitted, `1` is used.
859/// - `significance = 0` returns `#DIV/0!`.
860/// - Negative significance is treated as its absolute value in this fallback behavior.
861///
862/// # Examples
863/// ```yaml,sandbox
864/// title: "Round down to the nearest multiple"
865/// formula: "=FLOOR(5.9,2)"
866/// expected: 4
867/// ```
868///
869/// ```yaml,sandbox
870/// title: "Round a negative number to a lower multiple"
871/// formula: "=FLOOR(-5.9,2)"
872/// expected: -6
873/// ```
874///
875/// ```yaml,docs
876/// related:
877///   - FLOOR.MATH
878///   - CEILING
879///   - ROUNDDOWN
880/// faq:
881///   - q: "Why does FLOOR move negative values farther from zero?"
882///     a: "FLOOR rounds down to a lower multiple, which is more negative for negative inputs."
883/// ```
884/// [formualizer-docgen:schema:start]
885/// Name: FLOOR
886/// Type: FloorFn
887/// Min args: 1
888/// Max args: variadic
889/// Variadic: true
890/// Signature: FLOOR(arg1: number@scalar, arg2...: number@scalar)
891/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
892/// Caps: PURE
893/// [formualizer-docgen:schema:end]
894impl Function for FloorFn {
895    func_caps!(PURE);
896    fn name(&self) -> &'static str {
897        "FLOOR"
898    }
899    fn min_args(&self) -> usize {
900        1
901    }
902    fn variadic(&self) -> bool {
903        true
904    }
905    fn arg_schema(&self) -> &'static [ArgSchema] {
906        &ARG_NUM_LENIENT_TWO[..]
907    }
908    fn eval<'a, 'b, 'c>(
909        &self,
910        args: &'c [ArgumentHandle<'a, 'b>],
911        _: &dyn FunctionContext<'b>,
912    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
913        if args.is_empty() || args.len() > 2 {
914            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
915                ExcelError::new_value(),
916            )));
917        }
918        let n = match args[0].value()?.into_literal() {
919            LiteralValue::Error(e) => {
920                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
921            }
922            other => coerce_num(&other)?,
923        };
924        let mut sig = if args.len() == 2 {
925            match args[1].value()?.into_literal() {
926                LiteralValue::Error(e) => {
927                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
928                }
929                other => coerce_num(&other)?,
930            }
931        } else {
932            1.0
933        };
934        if sig == 0.0 {
935            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
936                ExcelError::from_error_string("#DIV/0!"),
937            )));
938        }
939        if sig < 0.0 {
940            sig = sig.abs();
941        }
942        let k = (n / sig).floor();
943        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
944            k * sig,
945        )))
946    }
947}
948
949#[derive(Debug)]
950pub struct FloorMathFn; // FLOOR.MATH(number,[significance],[mode])
951/// Rounds a number down to the nearest integer or multiple using `FLOOR.MATH` rules.
952///
953/// # Remarks
954/// - If `significance` is omitted (or passed as `0`), the function uses `1`.
955/// - `significance` is treated as a positive magnitude.
956/// - For negative numbers, non-zero `mode` rounds toward zero; otherwise it rounds away from zero.
957///
958/// # Examples
959/// ```yaml,sandbox
960/// title: "Default behavior for a positive number"
961/// formula: "=FLOOR.MATH(24.3,5)"
962/// expected: 20
963/// ```
964///
965/// ```yaml,sandbox
966/// title: "Use mode to round a negative number toward zero"
967/// formula: "=FLOOR.MATH(-24.3,5,1)"
968/// expected: -20
969/// ```
970///
971/// ```yaml,docs
972/// related:
973///   - FLOOR
974///   - CEILING.MATH
975///   - ROUNDDOWN
976/// faq:
977///   - q: "How does mode affect negative numbers in FLOOR.MATH?"
978///     a: "With non-zero mode, negatives round toward zero; otherwise they round away from zero."
979/// ```
980/// [formualizer-docgen:schema:start]
981/// Name: FLOOR.MATH
982/// Type: FloorMathFn
983/// Min args: 1
984/// Max args: variadic
985/// Variadic: true
986/// Signature: FLOOR.MATH(arg1: number@scalar, arg2...: number@scalar)
987/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
988/// Caps: PURE
989/// [formualizer-docgen:schema:end]
990impl Function for FloorMathFn {
991    func_caps!(PURE);
992    fn name(&self) -> &'static str {
993        "FLOOR.MATH"
994    }
995    fn min_args(&self) -> usize {
996        1
997    }
998    fn variadic(&self) -> bool {
999        true
1000    }
1001    fn arg_schema(&self) -> &'static [ArgSchema] {
1002        &ARG_NUM_LENIENT_TWO[..]
1003    }
1004    fn eval<'a, 'b, 'c>(
1005        &self,
1006        args: &'c [ArgumentHandle<'a, 'b>],
1007        _: &dyn FunctionContext<'b>,
1008    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1009        if args.is_empty() || args.len() > 3 {
1010            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1011                ExcelError::new_value(),
1012            )));
1013        }
1014        let n = match args[0].value()?.into_literal() {
1015            LiteralValue::Error(e) => {
1016                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
1017            }
1018            other => coerce_num(&other)?,
1019        };
1020        let sig = if args.len() >= 2 {
1021            match args[1].value()?.into_literal() {
1022                LiteralValue::Error(e) => {
1023                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
1024                }
1025                other => {
1026                    let v = coerce_num(&other)?;
1027                    if v == 0.0 { 1.0 } else { v.abs() }
1028                }
1029            }
1030        } else {
1031            1.0
1032        };
1033        let mode_nonzero = if args.len() == 3 {
1034            match args[2].value()?.into_literal() {
1035                LiteralValue::Error(e) => {
1036                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
1037                }
1038                other => coerce_num(&other)? != 0.0,
1039            }
1040        } else {
1041            false
1042        };
1043        let result = if n >= 0.0 {
1044            (n / sig).floor() * sig
1045        } else if mode_nonzero {
1046            (n / sig).ceil() * sig
1047        } else {
1048            (n / sig).floor() * sig
1049        };
1050        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
1051            result,
1052        )))
1053    }
1054}
1055
1056#[derive(Debug)]
1057pub struct SqrtFn; // SQRT(number)
1058/// Returns the positive square root of a number.
1059///
1060/// # Remarks
1061/// - Input must be greater than or equal to zero.
1062/// - Negative input returns `#NUM!`.
1063///
1064/// # Examples
1065/// ```yaml,sandbox
1066/// title: "Square root of a perfect square"
1067/// formula: "=SQRT(144)"
1068/// expected: 12
1069/// ```
1070///
1071/// ```yaml,sandbox
1072/// title: "Square root from a reference"
1073/// grid:
1074///   A1: 2
1075/// formula: "=SQRT(A1)"
1076/// expected: 1.4142135623730951
1077/// ```
1078///
1079/// ```yaml,docs
1080/// related:
1081///   - POWER
1082///   - SQRTPI
1083///   - EXP
1084/// faq:
1085///   - q: "When does SQRT return #NUM!?"
1086///     a: "It returns #NUM! for negative inputs because real square roots are undefined there."
1087/// ```
1088/// [formualizer-docgen:schema:start]
1089/// Name: SQRT
1090/// Type: SqrtFn
1091/// Min args: 1
1092/// Max args: 1
1093/// Variadic: false
1094/// Signature: SQRT(arg1: number@scalar)
1095/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1096/// Caps: PURE
1097/// [formualizer-docgen:schema:end]
1098impl Function for SqrtFn {
1099    func_caps!(PURE);
1100    fn name(&self) -> &'static str {
1101        "SQRT"
1102    }
1103    fn min_args(&self) -> usize {
1104        1
1105    }
1106    fn arg_schema(&self) -> &'static [ArgSchema] {
1107        &ARG_NUM_LENIENT_ONE[..]
1108    }
1109    fn eval<'a, 'b, 'c>(
1110        &self,
1111        args: &'c [ArgumentHandle<'a, 'b>],
1112        _: &dyn FunctionContext<'b>,
1113    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1114        let n = match args[0].value()?.into_literal() {
1115            LiteralValue::Error(e) => {
1116                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
1117            }
1118            other => coerce_num(&other)?,
1119        };
1120        if n < 0.0 {
1121            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1122                ExcelError::new_num(),
1123            )));
1124        }
1125        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
1126            n.sqrt(),
1127        )))
1128    }
1129}
1130
1131#[derive(Debug)]
1132pub struct PowerFn; // POWER(number, power)
1133/// Raises a base number to a specified power.
1134///
1135/// # Remarks
1136/// - Equivalent to exponentiation (`base^exponent`).
1137/// - Negative bases with fractional exponents return `#NUM!`.
1138/// - Errors are propagated.
1139///
1140/// # Examples
1141/// ```yaml,sandbox
1142/// title: "Integer exponent"
1143/// formula: "=POWER(2,10)"
1144/// expected: 1024
1145/// ```
1146///
1147/// ```yaml,sandbox
1148/// title: "Fractional exponent"
1149/// formula: "=POWER(9,0.5)"
1150/// expected: 3
1151/// ```
1152///
1153/// ```yaml,docs
1154/// related:
1155///   - SQRT
1156///   - EXP
1157///   - LN
1158/// faq:
1159///   - q: "Why can POWER return #NUM! for negative bases?"
1160///     a: "Negative bases with fractional exponents are rejected to avoid complex-number results."
1161/// ```
1162/// [formualizer-docgen:schema:start]
1163/// Name: POWER
1164/// Type: PowerFn
1165/// Min args: 2
1166/// Max args: 2
1167/// Variadic: false
1168/// Signature: POWER(arg1: number@scalar, arg2: number@scalar)
1169/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1170/// Caps: PURE
1171/// [formualizer-docgen:schema:end]
1172impl Function for PowerFn {
1173    func_caps!(PURE);
1174    fn name(&self) -> &'static str {
1175        "POWER"
1176    }
1177    fn min_args(&self) -> usize {
1178        2
1179    }
1180    fn arg_schema(&self) -> &'static [ArgSchema] {
1181        &ARG_NUM_LENIENT_TWO[..]
1182    }
1183    fn eval<'a, 'b, 'c>(
1184        &self,
1185        args: &'c [ArgumentHandle<'a, 'b>],
1186        _: &dyn FunctionContext<'b>,
1187    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1188        let base = match args[0].value()?.into_literal() {
1189            LiteralValue::Error(e) => {
1190                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
1191            }
1192            other => coerce_num(&other)?,
1193        };
1194        let expv = match args[1].value()?.into_literal() {
1195            LiteralValue::Error(e) => {
1196                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
1197            }
1198            other => coerce_num(&other)?,
1199        };
1200        if base < 0.0 && (expv.fract().abs() > 1e-12) {
1201            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1202                ExcelError::new_num(),
1203            )));
1204        }
1205        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
1206            base.powf(expv),
1207        )))
1208    }
1209}
1210
1211#[derive(Debug)]
1212pub struct ExpFn; // EXP(number)
1213/// Returns Euler's number `e` raised to the given power.
1214///
1215/// `EXP` is the inverse of `LN` for positive-domain values.
1216///
1217/// # Remarks
1218/// - Computes `e^x` using floating-point math.
1219/// - Very large positive inputs may overflow to infinity.
1220/// - Input errors are propagated.
1221///
1222/// # Examples
1223/// ```yaml,sandbox
1224/// title: "Compute e to the first power"
1225/// formula: "=EXP(1)"
1226/// expected: 2.718281828459045
1227/// ```
1228///
1229/// ```yaml,sandbox
1230/// title: "Invert LN"
1231/// formula: "=EXP(LN(5))"
1232/// expected: 5
1233/// ```
1234///
1235/// ```yaml,docs
1236/// related:
1237///   - LN
1238///   - LOG
1239///   - LOG10
1240/// faq:
1241///   - q: "Can EXP overflow?"
1242///     a: "Yes. Very large positive inputs can overflow floating-point range."
1243/// ```
1244/// [formualizer-docgen:schema:start]
1245/// Name: EXP
1246/// Type: ExpFn
1247/// Min args: 1
1248/// Max args: 1
1249/// Variadic: false
1250/// Signature: EXP(arg1: number@scalar)
1251/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1252/// Caps: PURE
1253/// [formualizer-docgen:schema:end]
1254impl Function for ExpFn {
1255    func_caps!(PURE);
1256    fn name(&self) -> &'static str {
1257        "EXP"
1258    }
1259    fn min_args(&self) -> usize {
1260        1
1261    }
1262    fn arg_schema(&self) -> &'static [ArgSchema] {
1263        &ARG_NUM_LENIENT_ONE[..]
1264    }
1265    fn eval<'a, 'b, 'c>(
1266        &self,
1267        args: &'c [ArgumentHandle<'a, 'b>],
1268        _: &dyn FunctionContext<'b>,
1269    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1270        let n = match args[0].value()?.into_literal() {
1271            LiteralValue::Error(e) => {
1272                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
1273            }
1274            other => coerce_num(&other)?,
1275        };
1276        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
1277            n.exp(),
1278        )))
1279    }
1280}
1281
1282#[derive(Debug)]
1283pub struct LnFn; // LN(number)
1284/// Returns the natural logarithm of a positive number.
1285///
1286/// # Remarks
1287/// - `number` must be greater than `0`; otherwise the function returns `#NUM!`.
1288/// - `LN(EXP(x))` returns `x` up to floating-point precision.
1289/// - Input errors are propagated.
1290///
1291/// # Examples
1292/// ```yaml,sandbox
1293/// title: "Natural log of e cubed"
1294/// formula: "=LN(EXP(3))"
1295/// expected: 3
1296/// ```
1297///
1298/// ```yaml,sandbox
1299/// title: "Natural log of a fraction"
1300/// formula: "=LN(0.5)"
1301/// expected: -0.6931471805599453
1302/// ```
1303///
1304/// ```yaml,docs
1305/// related:
1306///   - EXP
1307///   - LOG
1308///   - LOG10
1309/// faq:
1310///   - q: "Why does LN return #NUM! for 0 or negatives?"
1311///     a: "Natural logarithm is only defined for strictly positive inputs."
1312/// ```
1313/// [formualizer-docgen:schema:start]
1314/// Name: LN
1315/// Type: LnFn
1316/// Min args: 1
1317/// Max args: 1
1318/// Variadic: false
1319/// Signature: LN(arg1: number@scalar)
1320/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1321/// Caps: PURE
1322/// [formualizer-docgen:schema:end]
1323impl Function for LnFn {
1324    func_caps!(PURE);
1325    fn name(&self) -> &'static str {
1326        "LN"
1327    }
1328    fn min_args(&self) -> usize {
1329        1
1330    }
1331    fn arg_schema(&self) -> &'static [ArgSchema] {
1332        &ARG_NUM_LENIENT_ONE[..]
1333    }
1334    fn eval<'a, 'b, 'c>(
1335        &self,
1336        args: &'c [ArgumentHandle<'a, 'b>],
1337        _: &dyn FunctionContext<'b>,
1338    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1339        let n = match args[0].value()?.into_literal() {
1340            LiteralValue::Error(e) => {
1341                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
1342            }
1343            other => coerce_num(&other)?,
1344        };
1345        if n <= 0.0 {
1346            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1347                ExcelError::new_num(),
1348            )));
1349        }
1350        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
1351            n.ln(),
1352        )))
1353    }
1354}
1355
1356#[derive(Debug)]
1357pub struct LogFn; // LOG(number,[base]) default base 10
1358/// Returns the logarithm of a number for a specified base.
1359///
1360/// # Remarks
1361/// - If `base` is omitted, base 10 is used.
1362/// - `number` must be positive.
1363/// - `base` must be positive and not equal to 1.
1364/// - Invalid domains return `#NUM!`.
1365///
1366/// # Examples
1367/// ```yaml,sandbox
1368/// title: "Base-10 logarithm"
1369/// formula: "=LOG(1000)"
1370/// expected: 3
1371/// ```
1372///
1373/// ```yaml,sandbox
1374/// title: "Base-2 logarithm"
1375/// formula: "=LOG(8,2)"
1376/// expected: 3
1377/// ```
1378///
1379/// ```yaml,docs
1380/// related:
1381///   - LN
1382///   - LOG10
1383///   - EXP
1384/// faq:
1385///   - q: "Which base values are invalid for LOG?"
1386///     a: "Base must be positive and not equal to 1; otherwise LOG returns #NUM!."
1387/// ```
1388/// [formualizer-docgen:schema:start]
1389/// Name: LOG
1390/// Type: LogFn
1391/// Min args: 1
1392/// Max args: variadic
1393/// Variadic: true
1394/// Signature: LOG(arg1: number@scalar, arg2...: number@scalar)
1395/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1396/// Caps: PURE
1397/// [formualizer-docgen:schema:end]
1398impl Function for LogFn {
1399    func_caps!(PURE);
1400    fn name(&self) -> &'static str {
1401        "LOG"
1402    }
1403    fn min_args(&self) -> usize {
1404        1
1405    }
1406    fn variadic(&self) -> bool {
1407        true
1408    }
1409    fn arg_schema(&self) -> &'static [ArgSchema] {
1410        &ARG_NUM_LENIENT_TWO[..]
1411    }
1412    fn eval<'a, 'b, 'c>(
1413        &self,
1414        args: &'c [ArgumentHandle<'a, 'b>],
1415        _: &dyn FunctionContext<'b>,
1416    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1417        if args.is_empty() || args.len() > 2 {
1418            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1419                ExcelError::new_value(),
1420            )));
1421        }
1422        let n = match args[0].value()?.into_literal() {
1423            LiteralValue::Error(e) => {
1424                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
1425            }
1426            other => coerce_num(&other)?,
1427        };
1428        let base = if args.len() == 2 {
1429            match args[1].value()?.into_literal() {
1430                LiteralValue::Error(e) => {
1431                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
1432                }
1433                other => coerce_num(&other)?,
1434            }
1435        } else {
1436            10.0
1437        };
1438        if n <= 0.0 || base <= 0.0 || (base - 1.0).abs() < 1e-12 {
1439            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1440                ExcelError::new_num(),
1441            )));
1442        }
1443        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
1444            n.log(base),
1445        )))
1446    }
1447}
1448
1449#[derive(Debug)]
1450pub struct Log10Fn; // LOG10(number)
1451/// Returns the base-10 logarithm of a positive number.
1452///
1453/// # Remarks
1454/// - `number` must be greater than `0`; otherwise the function returns `#NUM!`.
1455/// - `LOG10(POWER(10,x))` returns `x` up to floating-point precision.
1456/// - Input errors are propagated.
1457///
1458/// # Examples
1459/// ```yaml,sandbox
1460/// title: "Power of ten to exponent"
1461/// formula: "=LOG10(1000)"
1462/// expected: 3
1463/// ```
1464///
1465/// ```yaml,sandbox
1466/// title: "Log base 10 of a decimal"
1467/// formula: "=LOG10(0.01)"
1468/// expected: -2
1469/// ```
1470///
1471/// ```yaml,docs
1472/// related:
1473///   - LOG
1474///   - LN
1475///   - EXP
1476/// faq:
1477///   - q: "When does LOG10 return #NUM!?"
1478///     a: "It returns #NUM! for non-positive input values."
1479/// ```
1480/// [formualizer-docgen:schema:start]
1481/// Name: LOG10
1482/// Type: Log10Fn
1483/// Min args: 1
1484/// Max args: 1
1485/// Variadic: false
1486/// Signature: LOG10(arg1: number@scalar)
1487/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1488/// Caps: PURE
1489/// [formualizer-docgen:schema:end]
1490impl Function for Log10Fn {
1491    func_caps!(PURE);
1492    fn name(&self) -> &'static str {
1493        "LOG10"
1494    }
1495    fn min_args(&self) -> usize {
1496        1
1497    }
1498    fn arg_schema(&self) -> &'static [ArgSchema] {
1499        &ARG_NUM_LENIENT_ONE[..]
1500    }
1501    fn eval<'a, 'b, 'c>(
1502        &self,
1503        args: &'c [ArgumentHandle<'a, 'b>],
1504        _: &dyn FunctionContext<'b>,
1505    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1506        let n = match args[0].value()?.into_literal() {
1507            LiteralValue::Error(e) => {
1508                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
1509            }
1510            other => coerce_num(&other)?,
1511        };
1512        if n <= 0.0 {
1513            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1514                ExcelError::new_num(),
1515            )));
1516        }
1517        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
1518            n.log10(),
1519        )))
1520    }
1521}
1522
1523fn factorial_checked(n: i64) -> Option<f64> {
1524    if !(0..=170).contains(&n) {
1525        return None;
1526    }
1527    let mut out = 1.0;
1528    for i in 2..=n {
1529        out *= i as f64;
1530    }
1531    Some(out)
1532}
1533
1534#[derive(Debug)]
1535pub struct QuotientFn;
1536/// Returns the integer portion of a division result, truncated toward zero.
1537///
1538/// # Remarks
1539/// - Fractional remainder is discarded without rounding.
1540/// - Dividing by `0` returns `#DIV/0!`.
1541/// - Input errors are propagated.
1542///
1543/// # Examples
1544/// ```yaml,sandbox
1545/// title: "Positive quotient"
1546/// formula: "=QUOTIENT(10,3)"
1547/// expected: 3
1548/// ```
1549///
1550/// ```yaml,sandbox
1551/// title: "Negative quotient truncates toward zero"
1552/// formula: "=QUOTIENT(-10,3)"
1553/// expected: -3
1554/// ```
1555///
1556/// ```yaml,docs
1557/// related:
1558///   - MOD
1559///   - INT
1560///   - TRUNC
1561/// faq:
1562///   - q: "How is QUOTIENT different from regular division?"
1563///     a: "It truncates the fractional part toward zero instead of returning a decimal result."
1564/// ```
1565/// [formualizer-docgen:schema:start]
1566/// Name: QUOTIENT
1567/// Type: QuotientFn
1568/// Min args: 2
1569/// Max args: 2
1570/// Variadic: false
1571/// Signature: QUOTIENT(arg1: number@scalar, arg2: number@scalar)
1572/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1573/// Caps: PURE
1574/// [formualizer-docgen:schema:end]
1575impl Function for QuotientFn {
1576    func_caps!(PURE);
1577    fn name(&self) -> &'static str {
1578        "QUOTIENT"
1579    }
1580    fn min_args(&self) -> usize {
1581        2
1582    }
1583    fn arg_schema(&self) -> &'static [ArgSchema] {
1584        &ARG_NUM_LENIENT_TWO[..]
1585    }
1586    fn eval<'a, 'b, 'c>(
1587        &self,
1588        args: &'c [ArgumentHandle<'a, 'b>],
1589        _: &dyn FunctionContext<'b>,
1590    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1591        let n = match args[0].value()?.into_literal() {
1592            LiteralValue::Error(e) => {
1593                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
1594            }
1595            other => coerce_num(&other)?,
1596        };
1597        let d = match args[1].value()?.into_literal() {
1598            LiteralValue::Error(e) => {
1599                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
1600            }
1601            other => coerce_num(&other)?,
1602        };
1603        if d == 0.0 {
1604            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1605                ExcelError::new_div(),
1606            )));
1607        }
1608        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
1609            (n / d).trunc(),
1610        )))
1611    }
1612}
1613
1614#[derive(Debug)]
1615pub struct EvenFn;
1616/// Rounds a number away from zero to the nearest even integer.
1617///
1618/// # Remarks
1619/// - Values already equal to an even integer stay unchanged.
1620/// - Positive and negative values both move away from zero.
1621/// - `0` returns `0`.
1622///
1623/// # Examples
1624/// ```yaml,sandbox
1625/// title: "Round a positive number to even"
1626/// formula: "=EVEN(3)"
1627/// expected: 4
1628/// ```
1629///
1630/// ```yaml,sandbox
1631/// title: "Round a negative number away from zero"
1632/// formula: "=EVEN(-1.1)"
1633/// expected: -2
1634/// ```
1635///
1636/// ```yaml,docs
1637/// related:
1638///   - ODD
1639///   - ROUNDUP
1640///   - MROUND
1641/// faq:
1642///   - q: "Does EVEN ever round toward zero?"
1643///     a: "No. It always rounds away from zero to the nearest even integer."
1644/// ```
1645/// [formualizer-docgen:schema:start]
1646/// Name: EVEN
1647/// Type: EvenFn
1648/// Min args: 1
1649/// Max args: 1
1650/// Variadic: false
1651/// Signature: EVEN(arg1: number@scalar)
1652/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1653/// Caps: PURE
1654/// [formualizer-docgen:schema:end]
1655impl Function for EvenFn {
1656    func_caps!(PURE);
1657    fn name(&self) -> &'static str {
1658        "EVEN"
1659    }
1660    fn min_args(&self) -> usize {
1661        1
1662    }
1663    fn arg_schema(&self) -> &'static [ArgSchema] {
1664        &ARG_NUM_LENIENT_ONE[..]
1665    }
1666    fn eval<'a, 'b, 'c>(
1667        &self,
1668        args: &'c [ArgumentHandle<'a, 'b>],
1669        _: &dyn FunctionContext<'b>,
1670    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1671        let number = match args[0].value()?.into_literal() {
1672            LiteralValue::Error(e) => {
1673                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
1674            }
1675            other => coerce_num(&other)?,
1676        };
1677        if number == 0.0 {
1678            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0)));
1679        }
1680
1681        let sign = number.signum();
1682        let mut v = number.abs().ceil() as i64;
1683        if v % 2 != 0 {
1684            v += 1;
1685        }
1686        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
1687            sign * v as f64,
1688        )))
1689    }
1690}
1691
1692#[derive(Debug)]
1693pub struct OddFn;
1694/// Rounds a number away from zero to the nearest odd integer.
1695///
1696/// # Remarks
1697/// - Values already equal to an odd integer stay unchanged.
1698/// - Positive and negative values both move away from zero.
1699/// - `0` returns `1`.
1700///
1701/// # Examples
1702/// ```yaml,sandbox
1703/// title: "Round a positive number to odd"
1704/// formula: "=ODD(2)"
1705/// expected: 3
1706/// ```
1707///
1708/// ```yaml,sandbox
1709/// title: "Round a negative number away from zero"
1710/// formula: "=ODD(-1.1)"
1711/// expected: -3
1712/// ```
1713///
1714/// ```yaml,docs
1715/// related:
1716///   - EVEN
1717///   - ROUNDUP
1718///   - INT
1719/// faq:
1720///   - q: "Why does ODD(0) return 1?"
1721///     a: "ODD rounds away from zero to the nearest odd integer, so zero maps to positive one."
1722/// ```
1723/// [formualizer-docgen:schema:start]
1724/// Name: ODD
1725/// Type: OddFn
1726/// Min args: 1
1727/// Max args: 1
1728/// Variadic: false
1729/// Signature: ODD(arg1: number@scalar)
1730/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1731/// Caps: PURE
1732/// [formualizer-docgen:schema:end]
1733impl Function for OddFn {
1734    func_caps!(PURE);
1735    fn name(&self) -> &'static str {
1736        "ODD"
1737    }
1738    fn min_args(&self) -> usize {
1739        1
1740    }
1741    fn arg_schema(&self) -> &'static [ArgSchema] {
1742        &ARG_NUM_LENIENT_ONE[..]
1743    }
1744    fn eval<'a, 'b, 'c>(
1745        &self,
1746        args: &'c [ArgumentHandle<'a, 'b>],
1747        _: &dyn FunctionContext<'b>,
1748    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1749        let number = match args[0].value()?.into_literal() {
1750            LiteralValue::Error(e) => {
1751                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
1752            }
1753            other => coerce_num(&other)?,
1754        };
1755
1756        let sign = if number < 0.0 { -1.0 } else { 1.0 };
1757        let mut v = number.abs().ceil() as i64;
1758        if v % 2 == 0 {
1759            v += 1;
1760        }
1761        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
1762            sign * v as f64,
1763        )))
1764    }
1765}
1766
1767#[derive(Debug)]
1768pub struct SqrtPiFn;
1769/// Returns the square root of a number multiplied by pi.
1770///
1771/// # Remarks
1772/// - Computes `SQRT(number * PI())`.
1773/// - `number` must be greater than or equal to `0`; otherwise returns `#NUM!`.
1774/// - Input errors are propagated.
1775///
1776/// # Examples
1777/// ```yaml,sandbox
1778/// title: "Square root of pi"
1779/// formula: "=SQRTPI(1)"
1780/// expected: 1.772453850905516
1781/// ```
1782///
1783/// ```yaml,sandbox
1784/// title: "Scale before taking square root"
1785/// formula: "=SQRTPI(4)"
1786/// expected: 3.544907701811032
1787/// ```
1788///
1789/// ```yaml,docs
1790/// related:
1791///   - SQRT
1792///   - PI
1793///   - POWER
1794/// faq:
1795///   - q: "When does SQRTPI return #NUM!?"
1796///     a: "It returns #NUM! when the input is negative, because number*PI must be non-negative."
1797/// ```
1798/// [formualizer-docgen:schema:start]
1799/// Name: SQRTPI
1800/// Type: SqrtPiFn
1801/// Min args: 1
1802/// Max args: 1
1803/// Variadic: false
1804/// Signature: SQRTPI(arg1: number@scalar)
1805/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1806/// Caps: PURE
1807/// [formualizer-docgen:schema:end]
1808impl Function for SqrtPiFn {
1809    func_caps!(PURE);
1810    fn name(&self) -> &'static str {
1811        "SQRTPI"
1812    }
1813    fn min_args(&self) -> usize {
1814        1
1815    }
1816    fn arg_schema(&self) -> &'static [ArgSchema] {
1817        &ARG_NUM_LENIENT_ONE[..]
1818    }
1819    fn eval<'a, 'b, 'c>(
1820        &self,
1821        args: &'c [ArgumentHandle<'a, 'b>],
1822        _: &dyn FunctionContext<'b>,
1823    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1824        let n = match args[0].value()?.into_literal() {
1825            LiteralValue::Error(e) => {
1826                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
1827            }
1828            other => coerce_num(&other)?,
1829        };
1830        if n < 0.0 {
1831            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1832                ExcelError::new_num(),
1833            )));
1834        }
1835        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
1836            (n * std::f64::consts::PI).sqrt(),
1837        )))
1838    }
1839}
1840
1841#[derive(Debug)]
1842pub struct MultinomialFn;
1843/// Returns the multinomial coefficient for one or more values.
1844///
1845/// # Remarks
1846/// - Each input is truncated toward zero before factorial is applied.
1847/// - Any negative term returns `#NUM!`.
1848/// - Values that require factorials outside `0..=170` return `#NUM!`.
1849///
1850/// # Examples
1851/// ```yaml,sandbox
1852/// title: "Compute a standard multinomial coefficient"
1853/// formula: "=MULTINOMIAL(2,3,4)"
1854/// expected: 1260
1855/// ```
1856///
1857/// ```yaml,sandbox
1858/// title: "Non-integers are truncated first"
1859/// formula: "=MULTINOMIAL(1.9,2.2)"
1860/// expected: 3
1861/// ```
1862///
1863/// ```yaml,docs
1864/// related:
1865///   - FACT
1866///   - COMBIN
1867///   - PERMUT
1868/// faq:
1869///   - q: "Why does MULTINOMIAL return #NUM! for large terms?"
1870///     a: "If any required factorial falls outside 0..=170, the function returns #NUM!."
1871/// ```
1872/// [formualizer-docgen:schema:start]
1873/// Name: MULTINOMIAL
1874/// Type: MultinomialFn
1875/// Min args: 1
1876/// Max args: variadic
1877/// Variadic: true
1878/// Signature: MULTINOMIAL(arg1...: number@scalar)
1879/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1880/// Caps: PURE
1881/// [formualizer-docgen:schema:end]
1882impl Function for MultinomialFn {
1883    func_caps!(PURE);
1884    fn name(&self) -> &'static str {
1885        "MULTINOMIAL"
1886    }
1887    fn min_args(&self) -> usize {
1888        1
1889    }
1890    fn variadic(&self) -> bool {
1891        true
1892    }
1893    fn arg_schema(&self) -> &'static [ArgSchema] {
1894        &ARG_NUM_LENIENT_ONE[..]
1895    }
1896    fn eval<'a, 'b, 'c>(
1897        &self,
1898        args: &'c [ArgumentHandle<'a, 'b>],
1899        _ctx: &dyn FunctionContext<'b>,
1900    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1901        let mut values: Vec<i64> = Vec::new();
1902        for arg in args {
1903            for value in arg.lazy_values_owned()? {
1904                let n = match value {
1905                    LiteralValue::Error(e) => {
1906                        return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
1907                    }
1908                    other => coerce_num(&other)?.trunc() as i64,
1909                };
1910                if n < 0 {
1911                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1912                        ExcelError::new_num(),
1913                    )));
1914                }
1915                values.push(n);
1916            }
1917        }
1918
1919        let sum: i64 = values.iter().sum();
1920        let num = match factorial_checked(sum) {
1921            Some(v) => v,
1922            None => {
1923                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1924                    ExcelError::new_num(),
1925                )));
1926            }
1927        };
1928
1929        let mut den = 1.0;
1930        for n in values {
1931            let fact = match factorial_checked(n) {
1932                Some(v) => v,
1933                None => {
1934                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1935                        ExcelError::new_num(),
1936                    )));
1937                }
1938            };
1939            den *= fact;
1940        }
1941
1942        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
1943            (num / den).round(),
1944        )))
1945    }
1946}
1947
1948#[derive(Debug)]
1949pub struct SeriesSumFn;
1950/// Evaluates a power series from coefficients, start power, and step.
1951///
1952/// # Remarks
1953/// - Computes `sum(c_i * x^(n + i*m))` in coefficient order.
1954/// - Coefficients may be supplied as a scalar, array literal, or range.
1955/// - Errors in `x`, `n`, `m`, or coefficient values are propagated.
1956///
1957/// # Examples
1958/// ```yaml,sandbox
1959/// title: "Series from an array literal"
1960/// formula: "=SERIESSUM(2,0,1,{1,2,3})"
1961/// expected: 17
1962/// ```
1963///
1964/// ```yaml,sandbox
1965/// title: "Series from worksheet coefficients"
1966/// grid:
1967///   A1: 1
1968///   A2: -1
1969///   A3: 0.5
1970/// formula: "=SERIESSUM(0.5,1,2,A1:A3)"
1971/// expected: 0.390625
1972/// ```
1973///
1974/// ```yaml,docs
1975/// related:
1976///   - SUMPRODUCT
1977///   - POWER
1978///   - EXP
1979/// faq:
1980///   - q: "In what order are SERIESSUM coefficients applied?"
1981///     a: "Coefficients are consumed in input order as c_i*x^(n+i*m)."
1982/// ```
1983/// [formualizer-docgen:schema:start]
1984/// Name: SERIESSUM
1985/// Type: SeriesSumFn
1986/// Min args: 4
1987/// Max args: 4
1988/// Variadic: false
1989/// Signature: SERIESSUM(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: any@scalar)
1990/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg4{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
1991/// Caps: PURE
1992/// [formualizer-docgen:schema:end]
1993impl Function for SeriesSumFn {
1994    func_caps!(PURE);
1995    fn name(&self) -> &'static str {
1996        "SERIESSUM"
1997    }
1998    fn min_args(&self) -> usize {
1999        4
2000    }
2001    fn arg_schema(&self) -> &'static [ArgSchema] {
2002        use std::sync::LazyLock;
2003        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
2004            vec![
2005                ArgSchema::number_lenient_scalar(),
2006                ArgSchema::number_lenient_scalar(),
2007                ArgSchema::number_lenient_scalar(),
2008                ArgSchema::any(),
2009            ]
2010        });
2011        &SCHEMA[..]
2012    }
2013    fn eval<'a, 'b, 'c>(
2014        &self,
2015        args: &'c [ArgumentHandle<'a, 'b>],
2016        _ctx: &dyn FunctionContext<'b>,
2017    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2018        let x = match args[0].value()?.into_literal() {
2019            LiteralValue::Error(e) => {
2020                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
2021            }
2022            other => coerce_num(&other)?,
2023        };
2024        let n = match args[1].value()?.into_literal() {
2025            LiteralValue::Error(e) => {
2026                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
2027            }
2028            other => coerce_num(&other)?,
2029        };
2030        let m = match args[2].value()?.into_literal() {
2031            LiteralValue::Error(e) => {
2032                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
2033            }
2034            other => coerce_num(&other)?,
2035        };
2036
2037        let mut coeffs: Vec<f64> = Vec::new();
2038        if let Ok(view) = args[3].range_view() {
2039            view.for_each_cell(&mut |cell| {
2040                match cell {
2041                    LiteralValue::Error(e) => return Err(e.clone()),
2042                    other => coeffs.push(coerce_num(other)?),
2043                }
2044                Ok(())
2045            })?;
2046        } else {
2047            match args[3].value()?.into_literal() {
2048                LiteralValue::Array(rows) => {
2049                    for row in rows {
2050                        for cell in row {
2051                            match cell {
2052                                LiteralValue::Error(e) => {
2053                                    return Ok(crate::traits::CalcValue::Scalar(
2054                                        LiteralValue::Error(e),
2055                                    ));
2056                                }
2057                                other => coeffs.push(coerce_num(&other)?),
2058                            }
2059                        }
2060                    }
2061                }
2062                LiteralValue::Error(e) => {
2063                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
2064                }
2065                other => coeffs.push(coerce_num(&other)?),
2066            }
2067        }
2068
2069        let mut sum = 0.0;
2070        for (i, c) in coeffs.into_iter().enumerate() {
2071            sum += c * x.powf(n + (i as f64) * m);
2072        }
2073
2074        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(sum)))
2075    }
2076}
2077
2078#[derive(Debug)]
2079pub struct SumsqFn;
2080/// Returns the sum of squares of supplied numbers.
2081///
2082/// # Remarks
2083/// - Accepts one or more scalar values, arrays, or ranges.
2084/// - For ranges, non-numeric cells are ignored while errors are propagated.
2085/// - Date/time-like values in ranges are converted to numeric serial values before squaring.
2086///
2087/// # Examples
2088/// ```yaml,sandbox
2089/// title: "Sum squares of scalar arguments"
2090/// formula: "=SUMSQ(3,4)"
2091/// expected: 25
2092/// ```
2093///
2094/// ```yaml,sandbox
2095/// title: "Ignore text cells in a range"
2096/// grid:
2097///   A1: 1
2098///   A2: "x"
2099///   A3: 2
2100/// formula: "=SUMSQ(A1:A3)"
2101/// expected: 5
2102/// ```
2103///
2104/// ```yaml,docs
2105/// related:
2106///   - SUM
2107///   - PRODUCT
2108///   - SUMPRODUCT
2109/// faq:
2110///   - q: "How does SUMSQ treat text cells in ranges?"
2111///     a: "Non-numeric range cells are ignored, while explicit errors are propagated."
2112/// ```
2113/// [formualizer-docgen:schema:start]
2114/// Name: SUMSQ
2115/// Type: SumsqFn
2116/// Min args: 1
2117/// Max args: variadic
2118/// Variadic: true
2119/// Signature: SUMSQ(arg1...: number@range)
2120/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
2121/// Caps: PURE, REDUCTION, NUMERIC_ONLY
2122/// [formualizer-docgen:schema:end]
2123impl Function for SumsqFn {
2124    func_caps!(PURE, REDUCTION, NUMERIC_ONLY);
2125    fn name(&self) -> &'static str {
2126        "SUMSQ"
2127    }
2128    fn min_args(&self) -> usize {
2129        1
2130    }
2131    fn variadic(&self) -> bool {
2132        true
2133    }
2134    fn arg_schema(&self) -> &'static [ArgSchema] {
2135        &ARG_RANGE_NUM_LENIENT_ONE[..]
2136    }
2137    fn eval<'a, 'b, 'c>(
2138        &self,
2139        args: &'c [ArgumentHandle<'a, 'b>],
2140        _ctx: &dyn FunctionContext<'b>,
2141    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2142        let mut total = 0.0;
2143        for arg in args {
2144            if let Ok(view) = arg.range_view() {
2145                view.for_each_cell(&mut |cell| {
2146                    match cell {
2147                        LiteralValue::Error(e) => return Err(e.clone()),
2148                        LiteralValue::Number(n) => total += n * n,
2149                        LiteralValue::Int(i) => {
2150                            let n = *i as f64;
2151                            total += n * n;
2152                        }
2153                        LiteralValue::Date(d) => {
2154                            let n = crate::builtins::datetime::date_to_serial(d);
2155                            total += n * n;
2156                        }
2157                        LiteralValue::DateTime(dt) => {
2158                            let n = crate::builtins::datetime::datetime_to_serial(dt);
2159                            total += n * n;
2160                        }
2161                        LiteralValue::Time(t) => {
2162                            let n = crate::builtins::datetime::time_to_fraction(t);
2163                            total += n * n;
2164                        }
2165                        LiteralValue::Duration(d) => {
2166                            let n = d.num_seconds() as f64 / 86_400.0;
2167                            total += n * n;
2168                        }
2169                        _ => {}
2170                    }
2171                    Ok(())
2172                })?;
2173            } else {
2174                let v = arg.value()?.into_literal();
2175                match v {
2176                    LiteralValue::Error(e) => {
2177                        return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
2178                    }
2179                    other => {
2180                        let n = coerce_num(&other)?;
2181                        total += n * n;
2182                    }
2183                }
2184            }
2185        }
2186        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
2187            total,
2188        )))
2189    }
2190}
2191
2192#[derive(Debug)]
2193pub struct MroundFn;
2194/// Rounds a number to the nearest multiple.
2195///
2196/// # Remarks
2197/// - Returns `0` when `multiple` is `0`.
2198/// - If `number` and `multiple` have different signs, returns `#NUM!`.
2199/// - Midpoints are rounded away from zero.
2200///
2201/// # Examples
2202/// ```yaml,sandbox
2203/// title: "Round to nearest 5"
2204/// formula: "=MROUND(17,5)"
2205/// expected: 15
2206/// ```
2207///
2208/// ```yaml,sandbox
2209/// title: "Round negative value"
2210/// formula: "=MROUND(-17,-5)"
2211/// expected: -15
2212/// ```
2213///
2214/// ```yaml,docs
2215/// related:
2216///   - ROUND
2217///   - CEILING
2218///   - FLOOR
2219/// faq:
2220///   - q: "Why does MROUND return #NUM! for mixed signs?"
2221///     a: "If number and multiple have different signs (excluding zero), MROUND returns #NUM!."
2222/// ```
2223/// [formualizer-docgen:schema:start]
2224/// Name: MROUND
2225/// Type: MroundFn
2226/// Min args: 2
2227/// Max args: 2
2228/// Variadic: false
2229/// Signature: MROUND(arg1: number@scalar, arg2: number@scalar)
2230/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
2231/// Caps: PURE
2232/// [formualizer-docgen:schema:end]
2233impl Function for MroundFn {
2234    func_caps!(PURE);
2235    fn name(&self) -> &'static str {
2236        "MROUND"
2237    }
2238    fn min_args(&self) -> usize {
2239        2
2240    }
2241    fn arg_schema(&self) -> &'static [ArgSchema] {
2242        &ARG_NUM_LENIENT_TWO[..]
2243    }
2244    fn eval<'a, 'b, 'c>(
2245        &self,
2246        args: &'c [ArgumentHandle<'a, 'b>],
2247        _ctx: &dyn FunctionContext<'b>,
2248    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2249        let number = match args[0].value()?.into_literal() {
2250            LiteralValue::Error(e) => {
2251                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
2252            }
2253            other => coerce_num(&other)?,
2254        };
2255        let multiple = match args[1].value()?.into_literal() {
2256            LiteralValue::Error(e) => {
2257                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
2258            }
2259            other => coerce_num(&other)?,
2260        };
2261
2262        if multiple == 0.0 {
2263            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0)));
2264        }
2265        if number != 0.0 && number.signum() != multiple.signum() {
2266            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2267                ExcelError::new_num(),
2268            )));
2269        }
2270
2271        let m = multiple.abs();
2272        let scaled = number.abs() / m;
2273        let rounded = (scaled + 0.5 + 1e-12).floor();
2274        let out = rounded * m * number.signum();
2275        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(out)))
2276    }
2277}
2278
2279fn roman_classic(mut n: u32) -> String {
2280    let table = [
2281        (1000, "M"),
2282        (900, "CM"),
2283        (500, "D"),
2284        (400, "CD"),
2285        (100, "C"),
2286        (90, "XC"),
2287        (50, "L"),
2288        (40, "XL"),
2289        (10, "X"),
2290        (9, "IX"),
2291        (5, "V"),
2292        (4, "IV"),
2293        (1, "I"),
2294    ];
2295
2296    let mut out = String::new();
2297    for (value, glyph) in table {
2298        while n >= value {
2299            n -= value;
2300            out.push_str(glyph);
2301        }
2302    }
2303    out
2304}
2305
2306fn roman_apply_form(classic: String, form: i64) -> String {
2307    match form {
2308        0 => classic,
2309        1 => classic
2310            .replace("CM", "LM")
2311            .replace("CD", "LD")
2312            .replace("XC", "VL")
2313            .replace("XL", "VL")
2314            .replace("IX", "IV"),
2315        2 => roman_apply_form(classic, 1)
2316            .replace("LD", "XD")
2317            .replace("LM", "XM")
2318            .replace("VLIV", "IX"),
2319        3 => roman_apply_form(classic, 2)
2320            .replace("XD", "VD")
2321            .replace("XM", "VM")
2322            .replace("IX", "IV"),
2323        4 => roman_apply_form(classic, 3)
2324            .replace("VDIV", "ID")
2325            .replace("VMIV", "IM"),
2326        _ => classic,
2327    }
2328}
2329
2330#[derive(Debug)]
2331pub struct RomanFn;
2332/// Converts an Arabic number to a Roman numeral string.
2333///
2334/// # Remarks
2335/// - Accepts integer values in the range `0..=3999`.
2336/// - `0` returns an empty string.
2337/// - Optional `form` controls output compactness (`0` classic through `4` simplified).
2338/// - Out-of-range values return `#VALUE!`.
2339///
2340/// # Examples
2341/// ```yaml,sandbox
2342/// title: "Classic Roman numeral"
2343/// formula: "=ROMAN(1999)"
2344/// expected: "MCMXCIX"
2345/// ```
2346///
2347/// ```yaml,sandbox
2348/// title: "Another conversion"
2349/// formula: "=ROMAN(44)"
2350/// expected: "XLIV"
2351/// ```
2352///
2353/// ```yaml,docs
2354/// related:
2355///   - ARABIC
2356///   - TEXT
2357/// faq:
2358///   - q: "What input range does ROMAN support?"
2359///     a: "ROMAN accepts truncated integers from 0 through 3999; outside that range it returns #VALUE!."
2360/// ```
2361/// [formualizer-docgen:schema:start]
2362/// Name: ROMAN
2363/// Type: RomanFn
2364/// Min args: 1
2365/// Max args: variadic
2366/// Variadic: true
2367/// Signature: ROMAN(arg1: number@scalar, arg2...: number@scalar)
2368/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
2369/// Caps: PURE
2370/// [formualizer-docgen:schema:end]
2371impl Function for RomanFn {
2372    func_caps!(PURE);
2373    fn name(&self) -> &'static str {
2374        "ROMAN"
2375    }
2376    fn min_args(&self) -> usize {
2377        1
2378    }
2379    fn variadic(&self) -> bool {
2380        true
2381    }
2382    fn arg_schema(&self) -> &'static [ArgSchema] {
2383        &ARG_NUM_LENIENT_TWO[..]
2384    }
2385    fn eval<'a, 'b, 'c>(
2386        &self,
2387        args: &'c [ArgumentHandle<'a, 'b>],
2388        _ctx: &dyn FunctionContext<'b>,
2389    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2390        if args.len() > 2 {
2391            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2392                ExcelError::new_value(),
2393            )));
2394        }
2395
2396        let number = match args[0].value()?.into_literal() {
2397            LiteralValue::Error(e) => {
2398                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
2399            }
2400            other => coerce_num(&other)?.trunc() as i64,
2401        };
2402
2403        if !(0..=3999).contains(&number) {
2404            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2405                ExcelError::new_value(),
2406            )));
2407        }
2408        if number == 0 {
2409            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Text(
2410                "".to_string(),
2411            )));
2412        }
2413
2414        let form = if args.len() >= 2 {
2415            match args[1].value()?.into_literal() {
2416                LiteralValue::Boolean(b) => {
2417                    if b {
2418                        0
2419                    } else {
2420                        4
2421                    }
2422                }
2423                LiteralValue::Number(n) => n.trunc() as i64,
2424                LiteralValue::Int(i) => i,
2425                LiteralValue::Empty => 0,
2426                LiteralValue::Error(e) => {
2427                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
2428                }
2429                _ => {
2430                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2431                        ExcelError::new_value(),
2432                    )));
2433                }
2434            }
2435        } else {
2436            0
2437        };
2438
2439        if !(0..=4).contains(&form) {
2440            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2441                ExcelError::new_value(),
2442            )));
2443        }
2444
2445        let classic = roman_classic(number as u32);
2446        let text = roman_apply_form(classic, form);
2447        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Text(text)))
2448    }
2449}
2450
2451fn roman_digit_value(ch: char) -> Option<i64> {
2452    match ch {
2453        'I' => Some(1),
2454        'V' => Some(5),
2455        'X' => Some(10),
2456        'L' => Some(50),
2457        'C' => Some(100),
2458        'D' => Some(500),
2459        'M' => Some(1000),
2460        _ => None,
2461    }
2462}
2463
2464#[derive(Debug)]
2465pub struct ArabicFn;
2466/// Converts a Roman numeral string to its Arabic numeric value.
2467///
2468/// # Remarks
2469/// - Accepts text input containing Roman symbols (`I,V,X,L,C,D,M`).
2470/// - Surrounding whitespace is trimmed.
2471/// - Empty text returns `0`.
2472/// - Invalid Roman syntax returns `#VALUE!`.
2473///
2474/// # Examples
2475/// ```yaml,sandbox
2476/// title: "Roman to Arabic"
2477/// formula: "=ARABIC(\"MCMXCIX\")"
2478/// expected: 1999
2479/// ```
2480///
2481/// ```yaml,sandbox
2482/// title: "Trimmed input"
2483/// formula: "=ARABIC(\"  XLIV  \")"
2484/// expected: 44
2485/// ```
2486///
2487/// ```yaml,docs
2488/// related:
2489///   - ROMAN
2490///   - VALUE
2491/// faq:
2492///   - q: "What causes ARABIC to return #VALUE!?"
2493///     a: "Invalid Roman symbols/syntax, non-text input, or overlength text produce #VALUE!."
2494/// ```
2495/// [formualizer-docgen:schema:start]
2496/// Name: ARABIC
2497/// Type: ArabicFn
2498/// Min args: 1
2499/// Max args: 1
2500/// Variadic: false
2501/// Signature: ARABIC(arg1: any@scalar)
2502/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
2503/// Caps: PURE
2504/// [formualizer-docgen:schema:end]
2505impl Function for ArabicFn {
2506    func_caps!(PURE);
2507    fn name(&self) -> &'static str {
2508        "ARABIC"
2509    }
2510    fn min_args(&self) -> usize {
2511        1
2512    }
2513    fn arg_schema(&self) -> &'static [ArgSchema] {
2514        use std::sync::LazyLock;
2515        static ONE: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| vec![ArgSchema::any()]);
2516        &ONE[..]
2517    }
2518    fn eval<'a, 'b, 'c>(
2519        &self,
2520        args: &'c [ArgumentHandle<'a, 'b>],
2521        _ctx: &dyn FunctionContext<'b>,
2522    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2523        let raw = match args[0].value()?.into_literal() {
2524            LiteralValue::Text(s) => s,
2525            LiteralValue::Empty => String::new(),
2526            LiteralValue::Error(e) => {
2527                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
2528            }
2529            _ => {
2530                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2531                    ExcelError::new_value(),
2532                )));
2533            }
2534        };
2535
2536        let mut text = raw.trim().to_uppercase();
2537        if text.len() > 255 {
2538            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2539                ExcelError::new_value(),
2540            )));
2541        }
2542        if text.is_empty() {
2543            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0)));
2544        }
2545
2546        let sign = if text.starts_with('-') {
2547            text.remove(0);
2548            -1.0
2549        } else {
2550            1.0
2551        };
2552
2553        if text.is_empty() {
2554            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2555                ExcelError::new_value(),
2556            )));
2557        }
2558
2559        let mut total = 0i64;
2560        let mut prev = 0i64;
2561        for ch in text.chars().rev() {
2562            let v = match roman_digit_value(ch) {
2563                Some(v) => v,
2564                None => {
2565                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2566                        ExcelError::new_value(),
2567                    )));
2568                }
2569            };
2570            if v < prev {
2571                total -= v;
2572            } else {
2573                total += v;
2574                prev = v;
2575            }
2576        }
2577
2578        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
2579            sign * total as f64,
2580        )))
2581    }
2582}
2583
2584/* ─────────────────── BASE / DECIMAL / CEILING.PRECISE / FLOOR.PRECISE / ISO.CEILING ─────────────────── */
2585
2586#[derive(Debug)]
2587pub struct BaseFn;
2588/// Converts a non-negative integer to text in the requested radix.
2589///
2590/// # Examples
2591///
2592/// ```excel
2593/// =BASE(31,16)
2594/// ```
2595///
2596/// ```yaml,sandbox
2597/// title: "Convert decimal to hexadecimal"
2598/// formula: '=BASE(31,16)'
2599/// expected: "1F"
2600/// ```
2601///
2602/// ```yaml,docs
2603/// related:
2604///   - DECIMAL
2605/// faq:
2606///   - q: "What bases are supported?"
2607///     a: "BASE accepts radix values from 2 through 36."
2608/// ```
2609///
2610/// [formualizer-docgen:schema:start]
2611/// Name: BASE
2612/// Type: BaseFn
2613/// Min args: 2
2614/// Max args: variadic
2615/// Variadic: true
2616/// Signature: BASE(arg1: number@scalar, arg2: number@scalar, arg3...: number@scalar)
2617/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
2618/// Caps: PURE
2619/// [formualizer-docgen:schema:end]
2620impl Function for BaseFn {
2621    func_caps!(PURE);
2622    fn name(&self) -> &'static str {
2623        "BASE"
2624    }
2625    fn min_args(&self) -> usize {
2626        2
2627    }
2628    fn variadic(&self) -> bool {
2629        true
2630    }
2631    fn arg_schema(&self) -> &'static [ArgSchema] {
2632        use std::sync::LazyLock;
2633        static THREE: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
2634            vec![
2635                ArgSchema::number_lenient_scalar(),
2636                ArgSchema::number_lenient_scalar(),
2637                ArgSchema::number_lenient_scalar(),
2638            ]
2639        });
2640        &THREE[..]
2641    }
2642    fn eval<'a, 'b, 'c>(
2643        &self,
2644        args: &'c [ArgumentHandle<'a, 'b>],
2645        _: &dyn FunctionContext<'b>,
2646    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2647        if args.len() < 2 || args.len() > 3 {
2648            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2649                ExcelError::new_value(),
2650            )));
2651        }
2652        let number = match args[0].value()?.into_literal() {
2653            LiteralValue::Error(e) => {
2654                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
2655            }
2656            other => coerce_num(&other)?.trunc() as i64,
2657        };
2658        let radix = match args[1].value()?.into_literal() {
2659            LiteralValue::Error(e) => {
2660                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
2661            }
2662            other => coerce_num(&other)?.trunc() as i64,
2663        };
2664        let min_len = if args.len() == 3 {
2665            match args[2].value()?.into_literal() {
2666                LiteralValue::Error(e) => {
2667                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
2668                }
2669                other => coerce_num(&other)?.trunc() as usize,
2670            }
2671        } else {
2672            0
2673        };
2674        if !(2..=36).contains(&radix) || number < 0 {
2675            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2676                ExcelError::new_num(),
2677            )));
2678        }
2679        let mut digits = Vec::new();
2680        let mut n = number as u64;
2681        if n == 0 {
2682            digits.push('0');
2683        } else {
2684            while n > 0 {
2685                let d = (n % radix as u64) as u32;
2686                digits.push(
2687                    char::from_digit(d, radix as u32)
2688                        .unwrap()
2689                        .to_ascii_uppercase(),
2690                );
2691                n /= radix as u64;
2692            }
2693            digits.reverse();
2694        }
2695        while digits.len() < min_len {
2696            digits.insert(0, '0');
2697        }
2698        let text: String = digits.into_iter().collect();
2699        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Text(text)))
2700    }
2701}
2702
2703#[derive(Debug)]
2704pub struct DecimalFn;
2705/// Converts text in a given radix back to a decimal number.
2706///
2707/// # Examples
2708///
2709/// ```excel
2710/// =DECIMAL("1F",16)
2711/// ```
2712///
2713/// ```yaml,sandbox
2714/// title: "Convert hexadecimal to decimal"
2715/// formula: '=DECIMAL("1F",16)'
2716/// expected: 31
2717/// ```
2718///
2719/// ```yaml,docs
2720/// related:
2721///   - BASE
2722/// faq:
2723///   - q: "Is DECIMAL case-sensitive for alphabetic digits?"
2724///     a: "No. Inputs like \"ff\" and \"FF\" both work for hexadecimal conversions."
2725/// ```
2726///
2727/// [formualizer-docgen:schema:start]
2728/// Name: DECIMAL
2729/// Type: DecimalFn
2730/// Min args: 2
2731/// Max args: 2
2732/// Variadic: false
2733/// Signature: DECIMAL(arg1: any@scalar, arg2: number@scalar)
2734/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
2735/// Caps: PURE
2736/// [formualizer-docgen:schema:end]
2737impl Function for DecimalFn {
2738    func_caps!(PURE);
2739    fn name(&self) -> &'static str {
2740        "DECIMAL"
2741    }
2742    fn min_args(&self) -> usize {
2743        2
2744    }
2745    fn arg_schema(&self) -> &'static [ArgSchema] {
2746        use std::sync::LazyLock;
2747        static SCHEMA: LazyLock<Vec<ArgSchema>> =
2748            LazyLock::new(|| vec![ArgSchema::any(), ArgSchema::number_lenient_scalar()]);
2749        &SCHEMA[..]
2750    }
2751    fn eval<'a, 'b, 'c>(
2752        &self,
2753        args: &'c [ArgumentHandle<'a, 'b>],
2754        _: &dyn FunctionContext<'b>,
2755    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2756        if args.len() != 2 {
2757            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2758                ExcelError::new_value(),
2759            )));
2760        }
2761        let text = match args[0].value()?.into_literal() {
2762            LiteralValue::Text(s) => s,
2763            LiteralValue::Number(n) => format!("{}", n.trunc() as i64),
2764            LiteralValue::Int(i) => i.to_string(),
2765            LiteralValue::Error(e) => {
2766                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
2767            }
2768            _ => {
2769                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2770                    ExcelError::new_value(),
2771                )));
2772            }
2773        };
2774        let radix = match args[1].value()?.into_literal() {
2775            LiteralValue::Error(e) => {
2776                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
2777            }
2778            other => coerce_num(&other)?.trunc() as u32,
2779        };
2780        if !(2..=36).contains(&radix) {
2781            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2782                ExcelError::new_num(),
2783            )));
2784        }
2785        let trimmed = text.trim();
2786        match i64::from_str_radix(trimmed, radix) {
2787            Ok(v) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
2788                v as f64,
2789            ))),
2790            Err(_) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2791                ExcelError::new_num(),
2792            ))),
2793        }
2794    }
2795}
2796
2797#[derive(Debug)]
2798pub struct CeilingPreciseFn;
2799/// Rounds a number up toward positive infinity using absolute significance.
2800///
2801/// # Examples
2802///
2803/// ```excel
2804/// =CEILING.PRECISE(-4.3)
2805/// ```
2806///
2807/// ```yaml,sandbox
2808/// title: "Round a negative number upward"
2809/// formula: '=CEILING.PRECISE(-4.3)'
2810/// expected: -4
2811/// ```
2812///
2813/// ```yaml,docs
2814/// related:
2815///   - FLOOR.PRECISE
2816///   - ISO.CEILING
2817/// faq:
2818///   - q: "Does the sign of significance matter?"
2819///     a: "No. CEILING.PRECISE uses the absolute value of significance."
2820/// ```
2821///
2822/// [formualizer-docgen:schema:start]
2823/// Name: CEILING.PRECISE
2824/// Type: CeilingPreciseFn
2825/// Min args: 1
2826/// Max args: variadic
2827/// Variadic: true
2828/// Signature: CEILING.PRECISE(arg1: number@scalar, arg2...: number@scalar)
2829/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
2830/// Caps: PURE
2831/// [formualizer-docgen:schema:end]
2832impl Function for CeilingPreciseFn {
2833    func_caps!(PURE);
2834    fn name(&self) -> &'static str {
2835        "CEILING.PRECISE"
2836    }
2837    fn min_args(&self) -> usize {
2838        1
2839    }
2840    fn variadic(&self) -> bool {
2841        true
2842    }
2843    fn arg_schema(&self) -> &'static [ArgSchema] {
2844        &ARG_NUM_LENIENT_TWO[..]
2845    }
2846    fn eval<'a, 'b, 'c>(
2847        &self,
2848        args: &'c [ArgumentHandle<'a, 'b>],
2849        _: &dyn FunctionContext<'b>,
2850    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2851        if args.is_empty() || args.len() > 2 {
2852            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2853                ExcelError::new_value(),
2854            )));
2855        }
2856        let n = match args[0].value()?.into_literal() {
2857            LiteralValue::Error(e) => {
2858                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
2859            }
2860            other => coerce_num(&other)?,
2861        };
2862        let sig = if args.len() == 2 {
2863            match args[1].value()?.into_literal() {
2864                LiteralValue::Error(e) => {
2865                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
2866                }
2867                other => {
2868                    let v = coerce_num(&other)?;
2869                    if v == 0.0 {
2870                        return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0)));
2871                    }
2872                    v.abs()
2873                }
2874            }
2875        } else {
2876            1.0
2877        };
2878        let result = (n / sig).ceil() * sig;
2879        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
2880            result,
2881        )))
2882    }
2883}
2884
2885#[derive(Debug)]
2886pub struct FloorPreciseFn;
2887/// Rounds a number down toward negative infinity using absolute significance.
2888///
2889/// # Examples
2890///
2891/// ```excel
2892/// =FLOOR.PRECISE(-4.3)
2893/// ```
2894///
2895/// ```yaml,sandbox
2896/// title: "Round a negative number downward"
2897/// formula: '=FLOOR.PRECISE(-4.3)'
2898/// expected: -5
2899/// ```
2900///
2901/// ```yaml,docs
2902/// related:
2903///   - CEILING.PRECISE
2904/// faq:
2905///   - q: "Does FLOOR.PRECISE keep negative significance?"
2906///     a: "No. Like Excel, this implementation uses the absolute value of significance."
2907/// ```
2908///
2909/// [formualizer-docgen:schema:start]
2910/// Name: FLOOR.PRECISE
2911/// Type: FloorPreciseFn
2912/// Min args: 1
2913/// Max args: variadic
2914/// Variadic: true
2915/// Signature: FLOOR.PRECISE(arg1: number@scalar, arg2...: number@scalar)
2916/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
2917/// Caps: PURE
2918/// [formualizer-docgen:schema:end]
2919impl Function for FloorPreciseFn {
2920    func_caps!(PURE);
2921    fn name(&self) -> &'static str {
2922        "FLOOR.PRECISE"
2923    }
2924    fn min_args(&self) -> usize {
2925        1
2926    }
2927    fn variadic(&self) -> bool {
2928        true
2929    }
2930    fn arg_schema(&self) -> &'static [ArgSchema] {
2931        &ARG_NUM_LENIENT_TWO[..]
2932    }
2933    fn eval<'a, 'b, 'c>(
2934        &self,
2935        args: &'c [ArgumentHandle<'a, 'b>],
2936        _: &dyn FunctionContext<'b>,
2937    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2938        if args.is_empty() || args.len() > 2 {
2939            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2940                ExcelError::new_value(),
2941            )));
2942        }
2943        let n = match args[0].value()?.into_literal() {
2944            LiteralValue::Error(e) => {
2945                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
2946            }
2947            other => coerce_num(&other)?,
2948        };
2949        let sig = if args.len() == 2 {
2950            match args[1].value()?.into_literal() {
2951                LiteralValue::Error(e) => {
2952                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
2953                }
2954                other => {
2955                    let v = coerce_num(&other)?;
2956                    if v == 0.0 {
2957                        return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0)));
2958                    }
2959                    v.abs()
2960                }
2961            }
2962        } else {
2963            1.0
2964        };
2965        let result = (n / sig).floor() * sig;
2966        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
2967            result,
2968        )))
2969    }
2970}
2971
2972#[derive(Debug)]
2973pub struct IsoCeilingFn;
2974/// Rounds a number up using ISO ceiling semantics.
2975///
2976/// # Examples
2977///
2978/// ```excel
2979/// =ISO.CEILING(-4.3)
2980/// ```
2981///
2982/// ```yaml,sandbox
2983/// title: "ISO ceiling on a negative value"
2984/// formula: '=ISO.CEILING(-4.3)'
2985/// expected: -4
2986/// ```
2987///
2988/// ```yaml,docs
2989/// related:
2990///   - CEILING.PRECISE
2991/// faq:
2992///   - q: "How does ISO.CEILING differ from legacy CEILING?"
2993///     a: "ISO.CEILING always uses a positive significance and rounds toward positive infinity."
2994/// ```
2995///
2996/// [formualizer-docgen:schema:start]
2997/// Name: ISO.CEILING
2998/// Type: IsoCeilingFn
2999/// Min args: 1
3000/// Max args: variadic
3001/// Variadic: true
3002/// Signature: ISO.CEILING(arg1: number@scalar, arg2...: number@scalar)
3003/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
3004/// Caps: PURE
3005/// [formualizer-docgen:schema:end]
3006impl Function for IsoCeilingFn {
3007    func_caps!(PURE);
3008    fn name(&self) -> &'static str {
3009        "ISO.CEILING"
3010    }
3011    fn min_args(&self) -> usize {
3012        1
3013    }
3014    fn variadic(&self) -> bool {
3015        true
3016    }
3017    fn arg_schema(&self) -> &'static [ArgSchema] {
3018        &ARG_NUM_LENIENT_TWO[..]
3019    }
3020    fn eval<'a, 'b, 'c>(
3021        &self,
3022        args: &'c [ArgumentHandle<'a, 'b>],
3023        _: &dyn FunctionContext<'b>,
3024    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3025        if args.is_empty() || args.len() > 2 {
3026            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3027                ExcelError::new_value(),
3028            )));
3029        }
3030        let n = match args[0].value()?.into_literal() {
3031            LiteralValue::Error(e) => {
3032                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
3033            }
3034            other => coerce_num(&other)?,
3035        };
3036        let sig = if args.len() == 2 {
3037            match args[1].value()?.into_literal() {
3038                LiteralValue::Error(e) => {
3039                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
3040                }
3041                other => {
3042                    let v = coerce_num(&other)?;
3043                    if v == 0.0 {
3044                        return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0)));
3045                    }
3046                    v.abs()
3047                }
3048            }
3049        } else {
3050            1.0
3051        };
3052        let result = (n / sig).ceil() * sig;
3053        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
3054            result,
3055        )))
3056    }
3057}
3058
3059/* ─────────────────── SUMX2MY2, SUMX2PY2, SUMXMY2 ─────────────────── */
3060
3061fn collect_nums_from_arg<'a, 'b>(
3062    arg: &'a crate::traits::ArgumentHandle<'a, 'b>,
3063) -> Result<Vec<f64>, ExcelError> {
3064    let mut out = Vec::new();
3065    if let Ok(view) = arg.range_view() {
3066        view.for_each_cell(&mut |cell| {
3067            match cell {
3068                LiteralValue::Error(e) => return Err(e.clone()),
3069                LiteralValue::Number(n) => out.push(*n),
3070                LiteralValue::Int(i) => out.push(*i as f64),
3071                LiteralValue::Boolean(b) => out.push(if *b { 1.0 } else { 0.0 }),
3072                _ => out.push(0.0),
3073            }
3074            Ok(())
3075        })?;
3076    } else {
3077        match arg.value()?.into_literal() {
3078            LiteralValue::Error(e) => return Err(e),
3079            LiteralValue::Array(rows) => {
3080                for row in rows {
3081                    for cell in row {
3082                        match cell {
3083                            LiteralValue::Error(e) => return Err(e),
3084                            LiteralValue::Number(n) => out.push(n),
3085                            LiteralValue::Int(i) => out.push(i as f64),
3086                            LiteralValue::Boolean(b) => out.push(if b { 1.0 } else { 0.0 }),
3087                            _ => out.push(0.0),
3088                        }
3089                    }
3090                }
3091            }
3092            other => {
3093                out.push(coerce_num(&other)?);
3094            }
3095        }
3096    }
3097    Ok(out)
3098}
3099
3100#[derive(Debug)]
3101pub struct SumX2MY2Fn;
3102/// Returns the sum of squares of values in one array minus the squares in another.
3103///
3104/// # Examples
3105///
3106/// ```excel
3107/// =SUMX2MY2({2,3},{1,4})
3108/// ```
3109///
3110/// ```yaml,sandbox
3111/// title: "Pairwise squared difference of squares"
3112/// formula: '=SUMX2MY2({2,3},{1,4})'
3113/// expected: -4
3114/// ```
3115///
3116/// ```yaml,docs
3117/// related:
3118///   - SUMX2PY2
3119///   - SUMXMY2
3120/// faq:
3121///   - q: "What happens when the arrays have different sizes?"
3122///     a: "SUMX2MY2 returns #N/A when the two inputs do not contain the same number of values."
3123/// ```
3124///
3125/// [formualizer-docgen:schema:start]
3126/// Name: SUMX2MY2
3127/// Type: SumX2MY2Fn
3128/// Min args: 2
3129/// Max args: 1
3130/// Variadic: false
3131/// Signature: SUMX2MY2(arg1: number@range)
3132/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
3133/// Caps: PURE
3134/// [formualizer-docgen:schema:end]
3135impl Function for SumX2MY2Fn {
3136    func_caps!(PURE);
3137    fn name(&self) -> &'static str {
3138        "SUMX2MY2"
3139    }
3140    fn min_args(&self) -> usize {
3141        2
3142    }
3143    fn arg_schema(&self) -> &'static [ArgSchema] {
3144        &ARG_RANGE_NUM_LENIENT_ONE[..]
3145    }
3146    fn eval<'a, 'b, 'c>(
3147        &self,
3148        args: &'c [ArgumentHandle<'a, 'b>],
3149        _: &dyn FunctionContext<'b>,
3150    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3151        if args.len() != 2 {
3152            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3153                ExcelError::new_value(),
3154            )));
3155        }
3156        let xs = collect_nums_from_arg(&args[0])?;
3157        let ys = collect_nums_from_arg(&args[1])?;
3158        if xs.len() != ys.len() {
3159            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3160                ExcelError::new_na(),
3161            )));
3162        }
3163        let total: f64 = xs.iter().zip(ys.iter()).map(|(x, y)| x * x - y * y).sum();
3164        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
3165            total,
3166        )))
3167    }
3168}
3169
3170#[derive(Debug)]
3171pub struct SumX2PY2Fn;
3172/// Returns the sum of squares of corresponding values from two arrays.
3173///
3174/// # Examples
3175///
3176/// ```excel
3177/// =SUMX2PY2({2,3},{1,4})
3178/// ```
3179///
3180/// ```yaml,sandbox
3181/// title: "Pairwise squared sums"
3182/// formula: '=SUMX2PY2({2,3},{1,4})'
3183/// expected: 30
3184/// ```
3185///
3186/// ```yaml,docs
3187/// related:
3188///   - SUMX2MY2
3189///   - SUMXMY2
3190/// faq:
3191///   - q: "Does SUMX2PY2 coerce booleans and blanks?"
3192///     a: "It follows the same collection rules as the engine's paired-array helpers, coercing booleans and treating blanks/text as zero."
3193/// ```
3194///
3195/// [formualizer-docgen:schema:start]
3196/// Name: SUMX2PY2
3197/// Type: SumX2PY2Fn
3198/// Min args: 2
3199/// Max args: 1
3200/// Variadic: false
3201/// Signature: SUMX2PY2(arg1: number@range)
3202/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
3203/// Caps: PURE
3204/// [formualizer-docgen:schema:end]
3205impl Function for SumX2PY2Fn {
3206    func_caps!(PURE);
3207    fn name(&self) -> &'static str {
3208        "SUMX2PY2"
3209    }
3210    fn min_args(&self) -> usize {
3211        2
3212    }
3213    fn arg_schema(&self) -> &'static [ArgSchema] {
3214        &ARG_RANGE_NUM_LENIENT_ONE[..]
3215    }
3216    fn eval<'a, 'b, 'c>(
3217        &self,
3218        args: &'c [ArgumentHandle<'a, 'b>],
3219        _: &dyn FunctionContext<'b>,
3220    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3221        if args.len() != 2 {
3222            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3223                ExcelError::new_value(),
3224            )));
3225        }
3226        let xs = collect_nums_from_arg(&args[0])?;
3227        let ys = collect_nums_from_arg(&args[1])?;
3228        if xs.len() != ys.len() {
3229            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3230                ExcelError::new_na(),
3231            )));
3232        }
3233        let total: f64 = xs.iter().zip(ys.iter()).map(|(x, y)| x * x + y * y).sum();
3234        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
3235            total,
3236        )))
3237    }
3238}
3239
3240#[derive(Debug)]
3241pub struct SumXMY2Fn;
3242/// Returns the sum of squared differences between corresponding values.
3243///
3244/// # Examples
3245///
3246/// ```excel
3247/// =SUMXMY2({2,3},{1,4})
3248/// ```
3249///
3250/// ```yaml,sandbox
3251/// title: "Pairwise squared differences"
3252/// formula: '=SUMXMY2({2,3},{1,4})'
3253/// expected: 2
3254/// ```
3255///
3256/// ```yaml,docs
3257/// related:
3258///   - SUMX2MY2
3259///   - SUMX2PY2
3260/// faq:
3261///   - q: "What shape must the inputs have?"
3262///     a: "Both inputs must flatten to the same number of values or the function returns #N/A."
3263/// ```
3264///
3265/// [formualizer-docgen:schema:start]
3266/// Name: SUMXMY2
3267/// Type: SumXMY2Fn
3268/// Min args: 2
3269/// Max args: 1
3270/// Variadic: false
3271/// Signature: SUMXMY2(arg1: number@range)
3272/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
3273/// Caps: PURE
3274/// [formualizer-docgen:schema:end]
3275impl Function for SumXMY2Fn {
3276    func_caps!(PURE);
3277    fn name(&self) -> &'static str {
3278        "SUMXMY2"
3279    }
3280    fn min_args(&self) -> usize {
3281        2
3282    }
3283    fn arg_schema(&self) -> &'static [ArgSchema] {
3284        &ARG_RANGE_NUM_LENIENT_ONE[..]
3285    }
3286    fn eval<'a, 'b, 'c>(
3287        &self,
3288        args: &'c [ArgumentHandle<'a, 'b>],
3289        _: &dyn FunctionContext<'b>,
3290    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3291        if args.len() != 2 {
3292            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3293                ExcelError::new_value(),
3294            )));
3295        }
3296        let xs = collect_nums_from_arg(&args[0])?;
3297        let ys = collect_nums_from_arg(&args[1])?;
3298        if xs.len() != ys.len() {
3299            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3300                ExcelError::new_na(),
3301            )));
3302        }
3303        let total: f64 = xs.iter().zip(ys.iter()).map(|(x, y)| (x - y).powi(2)).sum();
3304        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
3305            total,
3306        )))
3307    }
3308}
3309
3310pub fn register_builtins() {
3311    use std::sync::Arc;
3312    crate::function_registry::register_function(Arc::new(AbsFn));
3313    crate::function_registry::register_function(Arc::new(SignFn));
3314    crate::function_registry::register_function(Arc::new(IntFn));
3315    crate::function_registry::register_function(Arc::new(TruncFn));
3316    crate::function_registry::register_function(Arc::new(RoundFn));
3317    crate::function_registry::register_function(Arc::new(RoundDownFn));
3318    crate::function_registry::register_function(Arc::new(RoundUpFn));
3319    crate::function_registry::register_function(Arc::new(ModFn));
3320    crate::function_registry::register_function(Arc::new(CeilingFn));
3321    crate::function_registry::register_function(Arc::new(CeilingMathFn));
3322    crate::function_registry::register_function(Arc::new(CeilingPreciseFn));
3323    crate::function_registry::register_function(Arc::new(IsoCeilingFn));
3324    crate::function_registry::register_function(Arc::new(FloorFn));
3325    crate::function_registry::register_function(Arc::new(FloorMathFn));
3326    crate::function_registry::register_function(Arc::new(FloorPreciseFn));
3327    crate::function_registry::register_function(Arc::new(SqrtFn));
3328    crate::function_registry::register_function(Arc::new(PowerFn));
3329    crate::function_registry::register_function(Arc::new(ExpFn));
3330    crate::function_registry::register_function(Arc::new(LnFn));
3331    crate::function_registry::register_function(Arc::new(LogFn));
3332    crate::function_registry::register_function(Arc::new(Log10Fn));
3333    crate::function_registry::register_function(Arc::new(QuotientFn));
3334    crate::function_registry::register_function(Arc::new(EvenFn));
3335    crate::function_registry::register_function(Arc::new(OddFn));
3336    crate::function_registry::register_function(Arc::new(SqrtPiFn));
3337    crate::function_registry::register_function(Arc::new(MultinomialFn));
3338    crate::function_registry::register_function(Arc::new(SeriesSumFn));
3339    crate::function_registry::register_function(Arc::new(SumsqFn));
3340    crate::function_registry::register_function(Arc::new(MroundFn));
3341    crate::function_registry::register_function(Arc::new(RomanFn));
3342    crate::function_registry::register_function(Arc::new(ArabicFn));
3343    crate::function_registry::register_function(Arc::new(BaseFn));
3344    crate::function_registry::register_function(Arc::new(DecimalFn));
3345    crate::function_registry::register_function(Arc::new(SumX2MY2Fn));
3346    crate::function_registry::register_function(Arc::new(SumX2PY2Fn));
3347    crate::function_registry::register_function(Arc::new(SumXMY2Fn));
3348}
3349
3350#[cfg(test)]
3351mod tests_numeric {
3352    use super::*;
3353    use crate::test_workbook::TestWorkbook;
3354    use crate::traits::ArgumentHandle;
3355    use formualizer_common::LiteralValue;
3356    use formualizer_parse::parser::{ASTNode, ASTNodeType};
3357
3358    fn interp(wb: &TestWorkbook) -> crate::interpreter::Interpreter<'_> {
3359        wb.interpreter()
3360    }
3361    fn lit(v: LiteralValue) -> ASTNode {
3362        ASTNode::new(ASTNodeType::Literal(v), None)
3363    }
3364
3365    // ABS
3366    #[test]
3367    fn abs_basic() {
3368        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(AbsFn));
3369        let ctx = interp(&wb);
3370        let n = lit(LiteralValue::Number(-5.5));
3371        let f = ctx.context.get_function("", "ABS").unwrap();
3372        assert_eq!(
3373            f.dispatch(
3374                &[ArgumentHandle::new(&n, &ctx)],
3375                &ctx.function_context(None)
3376            )
3377            .unwrap()
3378            .into_literal(),
3379            LiteralValue::Number(5.5)
3380        );
3381    }
3382    #[test]
3383    fn abs_error_passthrough() {
3384        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(AbsFn));
3385        let ctx = interp(&wb);
3386        let e = lit(LiteralValue::Error(ExcelError::from_error_string(
3387            "#VALUE!",
3388        )));
3389        let f = ctx.context.get_function("", "ABS").unwrap();
3390        match f
3391            .dispatch(
3392                &[ArgumentHandle::new(&e, &ctx)],
3393                &ctx.function_context(None),
3394            )
3395            .unwrap()
3396            .into_literal()
3397        {
3398            LiteralValue::Error(er) => assert_eq!(er, "#VALUE!"),
3399            _ => panic!(),
3400        }
3401    }
3402
3403    // SIGN
3404    #[test]
3405    fn sign_neg_zero_pos() {
3406        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(SignFn));
3407        let ctx = interp(&wb);
3408        let f = ctx.context.get_function("", "SIGN").unwrap();
3409        let neg = lit(LiteralValue::Number(-3.2));
3410        let zero = lit(LiteralValue::Int(0));
3411        let pos = lit(LiteralValue::Int(9));
3412        assert_eq!(
3413            f.dispatch(
3414                &[ArgumentHandle::new(&neg, &ctx)],
3415                &ctx.function_context(None)
3416            )
3417            .unwrap()
3418            .into_literal(),
3419            LiteralValue::Number(-1.0)
3420        );
3421        assert_eq!(
3422            f.dispatch(
3423                &[ArgumentHandle::new(&zero, &ctx)],
3424                &ctx.function_context(None)
3425            )
3426            .unwrap()
3427            .into_literal(),
3428            LiteralValue::Number(0.0)
3429        );
3430        assert_eq!(
3431            f.dispatch(
3432                &[ArgumentHandle::new(&pos, &ctx)],
3433                &ctx.function_context(None)
3434            )
3435            .unwrap()
3436            .into_literal(),
3437            LiteralValue::Number(1.0)
3438        );
3439    }
3440    #[test]
3441    fn sign_error_passthrough() {
3442        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(SignFn));
3443        let ctx = interp(&wb);
3444        let e = lit(LiteralValue::Error(ExcelError::from_error_string(
3445            "#DIV/0!",
3446        )));
3447        let f = ctx.context.get_function("", "SIGN").unwrap();
3448        match f
3449            .dispatch(
3450                &[ArgumentHandle::new(&e, &ctx)],
3451                &ctx.function_context(None),
3452            )
3453            .unwrap()
3454            .into_literal()
3455        {
3456            LiteralValue::Error(er) => assert_eq!(er, "#DIV/0!"),
3457            _ => panic!(),
3458        }
3459    }
3460
3461    // INT
3462    #[test]
3463    fn int_floor_negative() {
3464        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(IntFn));
3465        let ctx = interp(&wb);
3466        let f = ctx.context.get_function("", "INT").unwrap();
3467        let n = lit(LiteralValue::Number(-3.2));
3468        assert_eq!(
3469            f.dispatch(
3470                &[ArgumentHandle::new(&n, &ctx)],
3471                &ctx.function_context(None)
3472            )
3473            .unwrap()
3474            .into_literal(),
3475            LiteralValue::Number(-4.0)
3476        );
3477    }
3478    #[test]
3479    fn int_floor_positive() {
3480        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(IntFn));
3481        let ctx = interp(&wb);
3482        let f = ctx.context.get_function("", "INT").unwrap();
3483        let n = lit(LiteralValue::Number(3.7));
3484        assert_eq!(
3485            f.dispatch(
3486                &[ArgumentHandle::new(&n, &ctx)],
3487                &ctx.function_context(None)
3488            )
3489            .unwrap()
3490            .into_literal(),
3491            LiteralValue::Number(3.0)
3492        );
3493    }
3494
3495    // TRUNC
3496    #[test]
3497    fn trunc_digits_positive_and_negative() {
3498        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(TruncFn));
3499        let ctx = interp(&wb);
3500        let f = ctx.context.get_function("", "TRUNC").unwrap();
3501        let n = lit(LiteralValue::Number(12.3456));
3502        let d2 = lit(LiteralValue::Int(2));
3503        let dneg1 = lit(LiteralValue::Int(-1));
3504        assert_eq!(
3505            f.dispatch(
3506                &[
3507                    ArgumentHandle::new(&n, &ctx),
3508                    ArgumentHandle::new(&d2, &ctx)
3509                ],
3510                &ctx.function_context(None)
3511            )
3512            .unwrap()
3513            .into_literal(),
3514            LiteralValue::Number(12.34)
3515        );
3516        assert_eq!(
3517            f.dispatch(
3518                &[
3519                    ArgumentHandle::new(&n, &ctx),
3520                    ArgumentHandle::new(&dneg1, &ctx)
3521                ],
3522                &ctx.function_context(None)
3523            )
3524            .unwrap()
3525            .into_literal(),
3526            LiteralValue::Number(10.0)
3527        );
3528    }
3529    #[test]
3530    fn trunc_default_zero_digits() {
3531        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(TruncFn));
3532        let ctx = interp(&wb);
3533        let f = ctx.context.get_function("", "TRUNC").unwrap();
3534        let n = lit(LiteralValue::Number(-12.999));
3535        assert_eq!(
3536            f.dispatch(
3537                &[ArgumentHandle::new(&n, &ctx)],
3538                &ctx.function_context(None)
3539            )
3540            .unwrap()
3541            .into_literal(),
3542            LiteralValue::Number(-12.0)
3543        );
3544    }
3545
3546    // ROUND
3547    #[test]
3548    fn round_half_away_positive_and_negative() {
3549        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(RoundFn));
3550        let ctx = interp(&wb);
3551        let f = ctx.context.get_function("", "ROUND").unwrap();
3552        let p = lit(LiteralValue::Number(2.5));
3553        let n = lit(LiteralValue::Number(-2.5));
3554        let d0 = lit(LiteralValue::Int(0));
3555        assert_eq!(
3556            f.dispatch(
3557                &[
3558                    ArgumentHandle::new(&p, &ctx),
3559                    ArgumentHandle::new(&d0, &ctx)
3560                ],
3561                &ctx.function_context(None)
3562            )
3563            .unwrap()
3564            .into_literal(),
3565            LiteralValue::Number(3.0)
3566        );
3567        assert_eq!(
3568            f.dispatch(
3569                &[
3570                    ArgumentHandle::new(&n, &ctx),
3571                    ArgumentHandle::new(&d0, &ctx)
3572                ],
3573                &ctx.function_context(None)
3574            )
3575            .unwrap()
3576            .into_literal(),
3577            LiteralValue::Number(-3.0)
3578        );
3579    }
3580    #[test]
3581    fn round_digits_positive() {
3582        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(RoundFn));
3583        let ctx = interp(&wb);
3584        let f = ctx.context.get_function("", "ROUND").unwrap();
3585        let n = lit(LiteralValue::Number(1.2345));
3586        let d = lit(LiteralValue::Int(3));
3587        assert_eq!(
3588            f.dispatch(
3589                &[ArgumentHandle::new(&n, &ctx), ArgumentHandle::new(&d, &ctx)],
3590                &ctx.function_context(None)
3591            )
3592            .unwrap()
3593            .into_literal(),
3594            LiteralValue::Number(1.235)
3595        );
3596    }
3597
3598    // ROUNDDOWN
3599    #[test]
3600    fn rounddown_truncates() {
3601        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(RoundDownFn));
3602        let ctx = interp(&wb);
3603        let f = ctx.context.get_function("", "ROUNDDOWN").unwrap();
3604        let n = lit(LiteralValue::Number(1.299));
3605        let d = lit(LiteralValue::Int(2));
3606        assert_eq!(
3607            f.dispatch(
3608                &[ArgumentHandle::new(&n, &ctx), ArgumentHandle::new(&d, &ctx)],
3609                &ctx.function_context(None)
3610            )
3611            .unwrap()
3612            .into_literal(),
3613            LiteralValue::Number(1.29)
3614        );
3615    }
3616    #[test]
3617    fn rounddown_negative_number() {
3618        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(RoundDownFn));
3619        let ctx = interp(&wb);
3620        let f = ctx.context.get_function("", "ROUNDDOWN").unwrap();
3621        let n = lit(LiteralValue::Number(-1.299));
3622        let d = lit(LiteralValue::Int(2));
3623        assert_eq!(
3624            f.dispatch(
3625                &[ArgumentHandle::new(&n, &ctx), ArgumentHandle::new(&d, &ctx)],
3626                &ctx.function_context(None)
3627            )
3628            .unwrap()
3629            .into_literal(),
3630            LiteralValue::Number(-1.29)
3631        );
3632    }
3633
3634    // ROUNDUP
3635    #[test]
3636    fn roundup_away_from_zero() {
3637        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(RoundUpFn));
3638        let ctx = interp(&wb);
3639        let f = ctx.context.get_function("", "ROUNDUP").unwrap();
3640        let n = lit(LiteralValue::Number(1.001));
3641        let d = lit(LiteralValue::Int(2));
3642        assert_eq!(
3643            f.dispatch(
3644                &[ArgumentHandle::new(&n, &ctx), ArgumentHandle::new(&d, &ctx)],
3645                &ctx.function_context(None)
3646            )
3647            .unwrap()
3648            .into_literal(),
3649            LiteralValue::Number(1.01)
3650        );
3651    }
3652    #[test]
3653    fn roundup_negative() {
3654        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(RoundUpFn));
3655        let ctx = interp(&wb);
3656        let f = ctx.context.get_function("", "ROUNDUP").unwrap();
3657        let n = lit(LiteralValue::Number(-1.001));
3658        let d = lit(LiteralValue::Int(2));
3659        assert_eq!(
3660            f.dispatch(
3661                &[ArgumentHandle::new(&n, &ctx), ArgumentHandle::new(&d, &ctx)],
3662                &ctx.function_context(None)
3663            )
3664            .unwrap()
3665            .into_literal(),
3666            LiteralValue::Number(-1.01)
3667        );
3668    }
3669
3670    // MOD
3671    #[test]
3672    fn mod_positive_negative_cases() {
3673        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(ModFn));
3674        let ctx = interp(&wb);
3675        let f = ctx.context.get_function("", "MOD").unwrap();
3676        let a = lit(LiteralValue::Int(-3));
3677        let b = lit(LiteralValue::Int(2));
3678        let out = f
3679            .dispatch(
3680                &[ArgumentHandle::new(&a, &ctx), ArgumentHandle::new(&b, &ctx)],
3681                &ctx.function_context(None),
3682            )
3683            .unwrap();
3684        assert_eq!(out, LiteralValue::Number(1.0));
3685        let a2 = lit(LiteralValue::Int(3));
3686        let b2 = lit(LiteralValue::Int(-2));
3687        let out2 = f
3688            .dispatch(
3689                &[
3690                    ArgumentHandle::new(&a2, &ctx),
3691                    ArgumentHandle::new(&b2, &ctx),
3692                ],
3693                &ctx.function_context(None),
3694            )
3695            .unwrap();
3696        assert_eq!(out2, LiteralValue::Number(-1.0));
3697    }
3698    #[test]
3699    fn mod_div_by_zero_error() {
3700        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(ModFn));
3701        let ctx = interp(&wb);
3702        let f = ctx.context.get_function("", "MOD").unwrap();
3703        let a = lit(LiteralValue::Int(5));
3704        let zero = lit(LiteralValue::Int(0));
3705        match f
3706            .dispatch(
3707                &[
3708                    ArgumentHandle::new(&a, &ctx),
3709                    ArgumentHandle::new(&zero, &ctx),
3710                ],
3711                &ctx.function_context(None),
3712            )
3713            .unwrap()
3714            .into_literal()
3715        {
3716            LiteralValue::Error(e) => assert_eq!(e, "#DIV/0!"),
3717            _ => panic!(),
3718        }
3719    }
3720
3721    // SQRT domain
3722    #[test]
3723    fn sqrt_basic_and_domain() {
3724        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(SqrtFn));
3725        let ctx = interp(&wb);
3726        let f = ctx.context.get_function("", "SQRT").unwrap();
3727        let n = lit(LiteralValue::Number(9.0));
3728        let out = f
3729            .dispatch(
3730                &[ArgumentHandle::new(&n, &ctx)],
3731                &ctx.function_context(None),
3732            )
3733            .unwrap();
3734        assert_eq!(out, LiteralValue::Number(3.0));
3735        let neg = lit(LiteralValue::Number(-1.0));
3736        let out2 = f
3737            .dispatch(
3738                &[ArgumentHandle::new(&neg, &ctx)],
3739                &ctx.function_context(None),
3740            )
3741            .unwrap();
3742        assert!(matches!(out2.into_literal(), LiteralValue::Error(_)));
3743    }
3744
3745    #[test]
3746    fn power_fractional_negative_domain() {
3747        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(PowerFn));
3748        let ctx = interp(&wb);
3749        let f = ctx.context.get_function("", "POWER").unwrap();
3750        let a = lit(LiteralValue::Number(-4.0));
3751        let half = lit(LiteralValue::Number(0.5));
3752        let out = f
3753            .dispatch(
3754                &[
3755                    ArgumentHandle::new(&a, &ctx),
3756                    ArgumentHandle::new(&half, &ctx),
3757                ],
3758                &ctx.function_context(None),
3759            )
3760            .unwrap();
3761        assert!(matches!(out.into_literal(), LiteralValue::Error(_))); // complex -> #NUM!
3762    }
3763
3764    #[test]
3765    fn log_variants() {
3766        let wb = TestWorkbook::new()
3767            .with_function(std::sync::Arc::new(LogFn))
3768            .with_function(std::sync::Arc::new(Log10Fn))
3769            .with_function(std::sync::Arc::new(LnFn));
3770        let ctx = interp(&wb);
3771        let logf = ctx.context.get_function("", "LOG").unwrap();
3772        let log10f = ctx.context.get_function("", "LOG10").unwrap();
3773        let lnf = ctx.context.get_function("", "LN").unwrap();
3774        let n = lit(LiteralValue::Number(100.0));
3775        let base = lit(LiteralValue::Number(10.0));
3776        assert_eq!(
3777            logf.dispatch(
3778                &[
3779                    ArgumentHandle::new(&n, &ctx),
3780                    ArgumentHandle::new(&base, &ctx)
3781                ],
3782                &ctx.function_context(None)
3783            )
3784            .unwrap()
3785            .into_literal(),
3786            LiteralValue::Number(2.0)
3787        );
3788        assert_eq!(
3789            log10f
3790                .dispatch(
3791                    &[ArgumentHandle::new(&n, &ctx)],
3792                    &ctx.function_context(None)
3793                )
3794                .unwrap()
3795                .into_literal(),
3796            LiteralValue::Number(2.0)
3797        );
3798        assert_eq!(
3799            lnf.dispatch(
3800                &[ArgumentHandle::new(&n, &ctx)],
3801                &ctx.function_context(None)
3802            )
3803            .unwrap()
3804            .into_literal(),
3805            LiteralValue::Number(100.0f64.ln())
3806        );
3807    }
3808    #[test]
3809    fn ceiling_floor_basic() {
3810        let wb = TestWorkbook::new()
3811            .with_function(std::sync::Arc::new(CeilingFn))
3812            .with_function(std::sync::Arc::new(FloorFn))
3813            .with_function(std::sync::Arc::new(CeilingMathFn))
3814            .with_function(std::sync::Arc::new(FloorMathFn));
3815        let ctx = interp(&wb);
3816        let c = ctx.context.get_function("", "CEILING").unwrap();
3817        let f = ctx.context.get_function("", "FLOOR").unwrap();
3818        let n = lit(LiteralValue::Number(5.1));
3819        let sig = lit(LiteralValue::Number(2.0));
3820        assert_eq!(
3821            c.dispatch(
3822                &[
3823                    ArgumentHandle::new(&n, &ctx),
3824                    ArgumentHandle::new(&sig, &ctx)
3825                ],
3826                &ctx.function_context(None)
3827            )
3828            .unwrap()
3829            .into_literal(),
3830            LiteralValue::Number(6.0)
3831        );
3832        assert_eq!(
3833            f.dispatch(
3834                &[
3835                    ArgumentHandle::new(&n, &ctx),
3836                    ArgumentHandle::new(&sig, &ctx)
3837                ],
3838                &ctx.function_context(None)
3839            )
3840            .unwrap()
3841            .into_literal(),
3842            LiteralValue::Number(4.0)
3843        );
3844    }
3845
3846    #[test]
3847    fn quotient_basic_and_div_zero() {
3848        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(QuotientFn));
3849        let ctx = interp(&wb);
3850        let f = ctx.context.get_function("", "QUOTIENT").unwrap();
3851
3852        let ten = lit(LiteralValue::Int(10));
3853        let three = lit(LiteralValue::Int(3));
3854        assert_eq!(
3855            f.dispatch(
3856                &[
3857                    ArgumentHandle::new(&ten, &ctx),
3858                    ArgumentHandle::new(&three, &ctx),
3859                ],
3860                &ctx.function_context(None),
3861            )
3862            .unwrap()
3863            .into_literal(),
3864            LiteralValue::Number(3.0)
3865        );
3866
3867        let neg_ten = lit(LiteralValue::Int(-10));
3868        assert_eq!(
3869            f.dispatch(
3870                &[
3871                    ArgumentHandle::new(&neg_ten, &ctx),
3872                    ArgumentHandle::new(&three, &ctx),
3873                ],
3874                &ctx.function_context(None),
3875            )
3876            .unwrap()
3877            .into_literal(),
3878            LiteralValue::Number(-3.0)
3879        );
3880
3881        let zero = lit(LiteralValue::Int(0));
3882        match f
3883            .dispatch(
3884                &[
3885                    ArgumentHandle::new(&ten, &ctx),
3886                    ArgumentHandle::new(&zero, &ctx),
3887                ],
3888                &ctx.function_context(None),
3889            )
3890            .unwrap()
3891            .into_literal()
3892        {
3893            LiteralValue::Error(e) => assert_eq!(e, "#DIV/0!"),
3894            other => panic!("expected #DIV/0!, got {other:?}"),
3895        }
3896    }
3897
3898    #[test]
3899    fn even_odd_examples() {
3900        let wb = TestWorkbook::new()
3901            .with_function(std::sync::Arc::new(EvenFn))
3902            .with_function(std::sync::Arc::new(OddFn));
3903        let ctx = interp(&wb);
3904
3905        let even = ctx.context.get_function("", "EVEN").unwrap();
3906        let odd = ctx.context.get_function("", "ODD").unwrap();
3907
3908        let one_half = lit(LiteralValue::Number(1.5));
3909        let three = lit(LiteralValue::Int(3));
3910        let neg_one = lit(LiteralValue::Int(-1));
3911        let two = lit(LiteralValue::Int(2));
3912        let zero = lit(LiteralValue::Int(0));
3913
3914        assert_eq!(
3915            even.dispatch(
3916                &[ArgumentHandle::new(&one_half, &ctx)],
3917                &ctx.function_context(None),
3918            )
3919            .unwrap()
3920            .into_literal(),
3921            LiteralValue::Number(2.0)
3922        );
3923        assert_eq!(
3924            even.dispatch(
3925                &[ArgumentHandle::new(&three, &ctx)],
3926                &ctx.function_context(None),
3927            )
3928            .unwrap()
3929            .into_literal(),
3930            LiteralValue::Number(4.0)
3931        );
3932        assert_eq!(
3933            even.dispatch(
3934                &[ArgumentHandle::new(&neg_one, &ctx)],
3935                &ctx.function_context(None),
3936            )
3937            .unwrap()
3938            .into_literal(),
3939            LiteralValue::Number(-2.0)
3940        );
3941        assert_eq!(
3942            even.dispatch(
3943                &[ArgumentHandle::new(&two, &ctx)],
3944                &ctx.function_context(None),
3945            )
3946            .unwrap()
3947            .into_literal(),
3948            LiteralValue::Number(2.0)
3949        );
3950
3951        assert_eq!(
3952            odd.dispatch(
3953                &[ArgumentHandle::new(&one_half, &ctx)],
3954                &ctx.function_context(None),
3955            )
3956            .unwrap()
3957            .into_literal(),
3958            LiteralValue::Number(3.0)
3959        );
3960        assert_eq!(
3961            odd.dispatch(
3962                &[ArgumentHandle::new(&two, &ctx)],
3963                &ctx.function_context(None),
3964            )
3965            .unwrap()
3966            .into_literal(),
3967            LiteralValue::Number(3.0)
3968        );
3969        assert_eq!(
3970            odd.dispatch(
3971                &[ArgumentHandle::new(&neg_one, &ctx)],
3972                &ctx.function_context(None),
3973            )
3974            .unwrap()
3975            .into_literal(),
3976            LiteralValue::Number(-1.0)
3977        );
3978        assert_eq!(
3979            odd.dispatch(
3980                &[ArgumentHandle::new(&zero, &ctx)],
3981                &ctx.function_context(None),
3982            )
3983            .unwrap()
3984            .into_literal(),
3985            LiteralValue::Number(1.0)
3986        );
3987    }
3988
3989    #[test]
3990    fn sqrtpi_multinomial_and_seriessum_examples() {
3991        let wb = TestWorkbook::new()
3992            .with_function(std::sync::Arc::new(SqrtPiFn))
3993            .with_function(std::sync::Arc::new(MultinomialFn))
3994            .with_function(std::sync::Arc::new(SeriesSumFn));
3995        let ctx = interp(&wb);
3996
3997        let sqrtpi = ctx.context.get_function("", "SQRTPI").unwrap();
3998        let one = lit(LiteralValue::Int(1));
3999        match sqrtpi
4000            .dispatch(
4001                &[ArgumentHandle::new(&one, &ctx)],
4002                &ctx.function_context(None),
4003            )
4004            .unwrap()
4005            .into_literal()
4006        {
4007            LiteralValue::Number(v) => assert!((v - std::f64::consts::PI.sqrt()).abs() < 1e-12),
4008            other => panic!("expected numeric SQRTPI, got {other:?}"),
4009        }
4010
4011        let multinomial = ctx.context.get_function("", "MULTINOMIAL").unwrap();
4012        let two = lit(LiteralValue::Int(2));
4013        let three = lit(LiteralValue::Int(3));
4014        let four = lit(LiteralValue::Int(4));
4015        assert_eq!(
4016            multinomial
4017                .dispatch(
4018                    &[
4019                        ArgumentHandle::new(&two, &ctx),
4020                        ArgumentHandle::new(&three, &ctx),
4021                        ArgumentHandle::new(&four, &ctx),
4022                    ],
4023                    &ctx.function_context(None),
4024                )
4025                .unwrap()
4026                .into_literal(),
4027            LiteralValue::Number(1260.0)
4028        );
4029
4030        let seriessum = ctx.context.get_function("", "SERIESSUM").unwrap();
4031        let x = lit(LiteralValue::Int(2));
4032        let n0 = lit(LiteralValue::Int(0));
4033        let m1 = lit(LiteralValue::Int(1));
4034        let coeffs = ASTNode::new(
4035            ASTNodeType::Literal(LiteralValue::Array(vec![vec![
4036                LiteralValue::Int(1),
4037                LiteralValue::Int(2),
4038                LiteralValue::Int(3),
4039            ]])),
4040            None,
4041        );
4042        assert_eq!(
4043            seriessum
4044                .dispatch(
4045                    &[
4046                        ArgumentHandle::new(&x, &ctx),
4047                        ArgumentHandle::new(&n0, &ctx),
4048                        ArgumentHandle::new(&m1, &ctx),
4049                        ArgumentHandle::new(&coeffs, &ctx),
4050                    ],
4051                    &ctx.function_context(None),
4052                )
4053                .unwrap()
4054                .into_literal(),
4055            LiteralValue::Number(17.0)
4056        );
4057    }
4058
4059    #[test]
4060    fn sumsq_basic() {
4061        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(SumsqFn));
4062        let ctx = interp(&wb);
4063        let f = ctx.context.get_function("", "SUMSQ").unwrap();
4064        let a = lit(LiteralValue::Int(3));
4065        let b = lit(LiteralValue::Int(4));
4066        assert_eq!(
4067            f.dispatch(
4068                &[ArgumentHandle::new(&a, &ctx), ArgumentHandle::new(&b, &ctx)],
4069                &ctx.function_context(None)
4070            )
4071            .unwrap()
4072            .into_literal(),
4073            LiteralValue::Number(25.0)
4074        );
4075    }
4076
4077    #[test]
4078    fn mround_sign_and_midpoint() {
4079        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(MroundFn));
4080        let ctx = interp(&wb);
4081        let f = ctx.context.get_function("", "MROUND").unwrap();
4082
4083        let n = lit(LiteralValue::Number(1.3));
4084        let m = lit(LiteralValue::Number(0.2));
4085        match f
4086            .dispatch(
4087                &[ArgumentHandle::new(&n, &ctx), ArgumentHandle::new(&m, &ctx)],
4088                &ctx.function_context(None),
4089            )
4090            .unwrap()
4091            .into_literal()
4092        {
4093            LiteralValue::Number(v) => assert!((v - 1.4).abs() < 1e-12),
4094            other => panic!("expected numeric result, got {other:?}"),
4095        }
4096
4097        let bad_m = lit(LiteralValue::Number(-2.0));
4098        let five = lit(LiteralValue::Number(5.0));
4099        match f
4100            .dispatch(
4101                &[
4102                    ArgumentHandle::new(&five, &ctx),
4103                    ArgumentHandle::new(&bad_m, &ctx),
4104                ],
4105                &ctx.function_context(None),
4106            )
4107            .unwrap()
4108            .into_literal()
4109        {
4110            LiteralValue::Error(e) => assert_eq!(e, "#NUM!"),
4111            other => panic!("expected #NUM!, got {other:?}"),
4112        }
4113    }
4114
4115    #[test]
4116    fn roman_and_arabic_examples() {
4117        let wb = TestWorkbook::new()
4118            .with_function(std::sync::Arc::new(RomanFn))
4119            .with_function(std::sync::Arc::new(ArabicFn));
4120        let ctx = interp(&wb);
4121
4122        let roman = ctx.context.get_function("", "ROMAN").unwrap();
4123        let n499 = lit(LiteralValue::Int(499));
4124        let out = roman
4125            .dispatch(
4126                &[ArgumentHandle::new(&n499, &ctx)],
4127                &ctx.function_context(None),
4128            )
4129            .unwrap()
4130            .into_literal();
4131        assert_eq!(out, LiteralValue::Text("CDXCIX".to_string()));
4132
4133        let form4 = lit(LiteralValue::Int(4));
4134        let out_form4 = roman
4135            .dispatch(
4136                &[
4137                    ArgumentHandle::new(&n499, &ctx),
4138                    ArgumentHandle::new(&form4, &ctx),
4139                ],
4140                &ctx.function_context(None),
4141            )
4142            .unwrap()
4143            .into_literal();
4144        assert_eq!(out_form4, LiteralValue::Text("ID".to_string()));
4145
4146        let arabic = ctx.context.get_function("", "ARABIC").unwrap();
4147        let roman_text = lit(LiteralValue::Text("CDXCIX".to_string()));
4148        let out_arabic = arabic
4149            .dispatch(
4150                &[ArgumentHandle::new(&roman_text, &ctx)],
4151                &ctx.function_context(None),
4152            )
4153            .unwrap()
4154            .into_literal();
4155        assert_eq!(out_arabic, LiteralValue::Number(499.0));
4156    }
4157
4158    // ── Too-few-arguments: must not panic ────────────────────────────────
4159
4160    #[test]
4161    fn round_one_arg_returns_error_not_panic() {
4162        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(RoundFn));
4163        let ctx = interp(&wb);
4164        let f = ctx.context.get_function("", "ROUND").unwrap();
4165        let n = lit(LiteralValue::Number(2.5));
4166        let result = f
4167            .dispatch(
4168                &[ArgumentHandle::new(&n, &ctx)],
4169                &ctx.function_context(None),
4170            )
4171            .unwrap()
4172            .into_literal();
4173        assert!(
4174            matches!(result, LiteralValue::Error(_)),
4175            "Expected an error, got {result:?}"
4176        );
4177    }
4178
4179    #[test]
4180    fn rounddown_one_arg_returns_error_not_panic() {
4181        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(RoundDownFn));
4182        let ctx = interp(&wb);
4183        let f = ctx.context.get_function("", "ROUNDDOWN").unwrap();
4184        let n = lit(LiteralValue::Number(1.9));
4185        let result = f
4186            .dispatch(
4187                &[ArgumentHandle::new(&n, &ctx)],
4188                &ctx.function_context(None),
4189            )
4190            .unwrap()
4191            .into_literal();
4192        assert!(
4193            matches!(result, LiteralValue::Error(_)),
4194            "Expected an error, got {result:?}"
4195        );
4196    }
4197
4198    #[test]
4199    fn abs_zero_args_returns_error_not_panic() {
4200        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(AbsFn));
4201        let ctx = interp(&wb);
4202        let f = ctx.context.get_function("", "ABS").unwrap();
4203        let result = f
4204            .dispatch(&[], &ctx.function_context(None))
4205            .unwrap()
4206            .into_literal();
4207        assert!(
4208            matches!(result, LiteralValue::Error(_)),
4209            "Expected an error, got {result:?}"
4210        );
4211    }
4212
4213    #[test]
4214    fn mod_one_arg_returns_error_not_panic() {
4215        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(ModFn));
4216        let ctx = interp(&wb);
4217        let f = ctx.context.get_function("", "MOD").unwrap();
4218        let n = lit(LiteralValue::Number(10.0));
4219        let result = f
4220            .dispatch(
4221                &[ArgumentHandle::new(&n, &ctx)],
4222                &ctx.function_context(None),
4223            )
4224            .unwrap()
4225            .into_literal();
4226        assert!(
4227            matches!(result, LiteralValue::Error(_)),
4228            "Expected an error, got {result:?}"
4229        );
4230    }
4231}