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
2584pub fn register_builtins() {
2585    use std::sync::Arc;
2586    crate::function_registry::register_function(Arc::new(AbsFn));
2587    crate::function_registry::register_function(Arc::new(SignFn));
2588    crate::function_registry::register_function(Arc::new(IntFn));
2589    crate::function_registry::register_function(Arc::new(TruncFn));
2590    crate::function_registry::register_function(Arc::new(RoundFn));
2591    crate::function_registry::register_function(Arc::new(RoundDownFn));
2592    crate::function_registry::register_function(Arc::new(RoundUpFn));
2593    crate::function_registry::register_function(Arc::new(ModFn));
2594    crate::function_registry::register_function(Arc::new(CeilingFn));
2595    crate::function_registry::register_function(Arc::new(CeilingMathFn));
2596    crate::function_registry::register_function(Arc::new(FloorFn));
2597    crate::function_registry::register_function(Arc::new(FloorMathFn));
2598    crate::function_registry::register_function(Arc::new(SqrtFn));
2599    crate::function_registry::register_function(Arc::new(PowerFn));
2600    crate::function_registry::register_function(Arc::new(ExpFn));
2601    crate::function_registry::register_function(Arc::new(LnFn));
2602    crate::function_registry::register_function(Arc::new(LogFn));
2603    crate::function_registry::register_function(Arc::new(Log10Fn));
2604    crate::function_registry::register_function(Arc::new(QuotientFn));
2605    crate::function_registry::register_function(Arc::new(EvenFn));
2606    crate::function_registry::register_function(Arc::new(OddFn));
2607    crate::function_registry::register_function(Arc::new(SqrtPiFn));
2608    crate::function_registry::register_function(Arc::new(MultinomialFn));
2609    crate::function_registry::register_function(Arc::new(SeriesSumFn));
2610    crate::function_registry::register_function(Arc::new(SumsqFn));
2611    crate::function_registry::register_function(Arc::new(MroundFn));
2612    crate::function_registry::register_function(Arc::new(RomanFn));
2613    crate::function_registry::register_function(Arc::new(ArabicFn));
2614}
2615
2616#[cfg(test)]
2617mod tests_numeric {
2618    use super::*;
2619    use crate::test_workbook::TestWorkbook;
2620    use crate::traits::ArgumentHandle;
2621    use formualizer_common::LiteralValue;
2622    use formualizer_parse::parser::{ASTNode, ASTNodeType};
2623
2624    fn interp(wb: &TestWorkbook) -> crate::interpreter::Interpreter<'_> {
2625        wb.interpreter()
2626    }
2627    fn lit(v: LiteralValue) -> ASTNode {
2628        ASTNode::new(ASTNodeType::Literal(v), None)
2629    }
2630
2631    // ABS
2632    #[test]
2633    fn abs_basic() {
2634        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(AbsFn));
2635        let ctx = interp(&wb);
2636        let n = lit(LiteralValue::Number(-5.5));
2637        let f = ctx.context.get_function("", "ABS").unwrap();
2638        assert_eq!(
2639            f.dispatch(
2640                &[ArgumentHandle::new(&n, &ctx)],
2641                &ctx.function_context(None)
2642            )
2643            .unwrap()
2644            .into_literal(),
2645            LiteralValue::Number(5.5)
2646        );
2647    }
2648    #[test]
2649    fn abs_error_passthrough() {
2650        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(AbsFn));
2651        let ctx = interp(&wb);
2652        let e = lit(LiteralValue::Error(ExcelError::from_error_string(
2653            "#VALUE!",
2654        )));
2655        let f = ctx.context.get_function("", "ABS").unwrap();
2656        match f
2657            .dispatch(
2658                &[ArgumentHandle::new(&e, &ctx)],
2659                &ctx.function_context(None),
2660            )
2661            .unwrap()
2662            .into_literal()
2663        {
2664            LiteralValue::Error(er) => assert_eq!(er, "#VALUE!"),
2665            _ => panic!(),
2666        }
2667    }
2668
2669    // SIGN
2670    #[test]
2671    fn sign_neg_zero_pos() {
2672        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(SignFn));
2673        let ctx = interp(&wb);
2674        let f = ctx.context.get_function("", "SIGN").unwrap();
2675        let neg = lit(LiteralValue::Number(-3.2));
2676        let zero = lit(LiteralValue::Int(0));
2677        let pos = lit(LiteralValue::Int(9));
2678        assert_eq!(
2679            f.dispatch(
2680                &[ArgumentHandle::new(&neg, &ctx)],
2681                &ctx.function_context(None)
2682            )
2683            .unwrap()
2684            .into_literal(),
2685            LiteralValue::Number(-1.0)
2686        );
2687        assert_eq!(
2688            f.dispatch(
2689                &[ArgumentHandle::new(&zero, &ctx)],
2690                &ctx.function_context(None)
2691            )
2692            .unwrap()
2693            .into_literal(),
2694            LiteralValue::Number(0.0)
2695        );
2696        assert_eq!(
2697            f.dispatch(
2698                &[ArgumentHandle::new(&pos, &ctx)],
2699                &ctx.function_context(None)
2700            )
2701            .unwrap()
2702            .into_literal(),
2703            LiteralValue::Number(1.0)
2704        );
2705    }
2706    #[test]
2707    fn sign_error_passthrough() {
2708        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(SignFn));
2709        let ctx = interp(&wb);
2710        let e = lit(LiteralValue::Error(ExcelError::from_error_string(
2711            "#DIV/0!",
2712        )));
2713        let f = ctx.context.get_function("", "SIGN").unwrap();
2714        match f
2715            .dispatch(
2716                &[ArgumentHandle::new(&e, &ctx)],
2717                &ctx.function_context(None),
2718            )
2719            .unwrap()
2720            .into_literal()
2721        {
2722            LiteralValue::Error(er) => assert_eq!(er, "#DIV/0!"),
2723            _ => panic!(),
2724        }
2725    }
2726
2727    // INT
2728    #[test]
2729    fn int_floor_negative() {
2730        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(IntFn));
2731        let ctx = interp(&wb);
2732        let f = ctx.context.get_function("", "INT").unwrap();
2733        let n = lit(LiteralValue::Number(-3.2));
2734        assert_eq!(
2735            f.dispatch(
2736                &[ArgumentHandle::new(&n, &ctx)],
2737                &ctx.function_context(None)
2738            )
2739            .unwrap()
2740            .into_literal(),
2741            LiteralValue::Number(-4.0)
2742        );
2743    }
2744    #[test]
2745    fn int_floor_positive() {
2746        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(IntFn));
2747        let ctx = interp(&wb);
2748        let f = ctx.context.get_function("", "INT").unwrap();
2749        let n = lit(LiteralValue::Number(3.7));
2750        assert_eq!(
2751            f.dispatch(
2752                &[ArgumentHandle::new(&n, &ctx)],
2753                &ctx.function_context(None)
2754            )
2755            .unwrap()
2756            .into_literal(),
2757            LiteralValue::Number(3.0)
2758        );
2759    }
2760
2761    // TRUNC
2762    #[test]
2763    fn trunc_digits_positive_and_negative() {
2764        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(TruncFn));
2765        let ctx = interp(&wb);
2766        let f = ctx.context.get_function("", "TRUNC").unwrap();
2767        let n = lit(LiteralValue::Number(12.3456));
2768        let d2 = lit(LiteralValue::Int(2));
2769        let dneg1 = lit(LiteralValue::Int(-1));
2770        assert_eq!(
2771            f.dispatch(
2772                &[
2773                    ArgumentHandle::new(&n, &ctx),
2774                    ArgumentHandle::new(&d2, &ctx)
2775                ],
2776                &ctx.function_context(None)
2777            )
2778            .unwrap()
2779            .into_literal(),
2780            LiteralValue::Number(12.34)
2781        );
2782        assert_eq!(
2783            f.dispatch(
2784                &[
2785                    ArgumentHandle::new(&n, &ctx),
2786                    ArgumentHandle::new(&dneg1, &ctx)
2787                ],
2788                &ctx.function_context(None)
2789            )
2790            .unwrap()
2791            .into_literal(),
2792            LiteralValue::Number(10.0)
2793        );
2794    }
2795    #[test]
2796    fn trunc_default_zero_digits() {
2797        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(TruncFn));
2798        let ctx = interp(&wb);
2799        let f = ctx.context.get_function("", "TRUNC").unwrap();
2800        let n = lit(LiteralValue::Number(-12.999));
2801        assert_eq!(
2802            f.dispatch(
2803                &[ArgumentHandle::new(&n, &ctx)],
2804                &ctx.function_context(None)
2805            )
2806            .unwrap()
2807            .into_literal(),
2808            LiteralValue::Number(-12.0)
2809        );
2810    }
2811
2812    // ROUND
2813    #[test]
2814    fn round_half_away_positive_and_negative() {
2815        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(RoundFn));
2816        let ctx = interp(&wb);
2817        let f = ctx.context.get_function("", "ROUND").unwrap();
2818        let p = lit(LiteralValue::Number(2.5));
2819        let n = lit(LiteralValue::Number(-2.5));
2820        let d0 = lit(LiteralValue::Int(0));
2821        assert_eq!(
2822            f.dispatch(
2823                &[
2824                    ArgumentHandle::new(&p, &ctx),
2825                    ArgumentHandle::new(&d0, &ctx)
2826                ],
2827                &ctx.function_context(None)
2828            )
2829            .unwrap()
2830            .into_literal(),
2831            LiteralValue::Number(3.0)
2832        );
2833        assert_eq!(
2834            f.dispatch(
2835                &[
2836                    ArgumentHandle::new(&n, &ctx),
2837                    ArgumentHandle::new(&d0, &ctx)
2838                ],
2839                &ctx.function_context(None)
2840            )
2841            .unwrap()
2842            .into_literal(),
2843            LiteralValue::Number(-3.0)
2844        );
2845    }
2846    #[test]
2847    fn round_digits_positive() {
2848        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(RoundFn));
2849        let ctx = interp(&wb);
2850        let f = ctx.context.get_function("", "ROUND").unwrap();
2851        let n = lit(LiteralValue::Number(1.2345));
2852        let d = lit(LiteralValue::Int(3));
2853        assert_eq!(
2854            f.dispatch(
2855                &[ArgumentHandle::new(&n, &ctx), ArgumentHandle::new(&d, &ctx)],
2856                &ctx.function_context(None)
2857            )
2858            .unwrap()
2859            .into_literal(),
2860            LiteralValue::Number(1.235)
2861        );
2862    }
2863
2864    // ROUNDDOWN
2865    #[test]
2866    fn rounddown_truncates() {
2867        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(RoundDownFn));
2868        let ctx = interp(&wb);
2869        let f = ctx.context.get_function("", "ROUNDDOWN").unwrap();
2870        let n = lit(LiteralValue::Number(1.299));
2871        let d = lit(LiteralValue::Int(2));
2872        assert_eq!(
2873            f.dispatch(
2874                &[ArgumentHandle::new(&n, &ctx), ArgumentHandle::new(&d, &ctx)],
2875                &ctx.function_context(None)
2876            )
2877            .unwrap()
2878            .into_literal(),
2879            LiteralValue::Number(1.29)
2880        );
2881    }
2882    #[test]
2883    fn rounddown_negative_number() {
2884        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(RoundDownFn));
2885        let ctx = interp(&wb);
2886        let f = ctx.context.get_function("", "ROUNDDOWN").unwrap();
2887        let n = lit(LiteralValue::Number(-1.299));
2888        let d = lit(LiteralValue::Int(2));
2889        assert_eq!(
2890            f.dispatch(
2891                &[ArgumentHandle::new(&n, &ctx), ArgumentHandle::new(&d, &ctx)],
2892                &ctx.function_context(None)
2893            )
2894            .unwrap()
2895            .into_literal(),
2896            LiteralValue::Number(-1.29)
2897        );
2898    }
2899
2900    // ROUNDUP
2901    #[test]
2902    fn roundup_away_from_zero() {
2903        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(RoundUpFn));
2904        let ctx = interp(&wb);
2905        let f = ctx.context.get_function("", "ROUNDUP").unwrap();
2906        let n = lit(LiteralValue::Number(1.001));
2907        let d = lit(LiteralValue::Int(2));
2908        assert_eq!(
2909            f.dispatch(
2910                &[ArgumentHandle::new(&n, &ctx), ArgumentHandle::new(&d, &ctx)],
2911                &ctx.function_context(None)
2912            )
2913            .unwrap()
2914            .into_literal(),
2915            LiteralValue::Number(1.01)
2916        );
2917    }
2918    #[test]
2919    fn roundup_negative() {
2920        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(RoundUpFn));
2921        let ctx = interp(&wb);
2922        let f = ctx.context.get_function("", "ROUNDUP").unwrap();
2923        let n = lit(LiteralValue::Number(-1.001));
2924        let d = lit(LiteralValue::Int(2));
2925        assert_eq!(
2926            f.dispatch(
2927                &[ArgumentHandle::new(&n, &ctx), ArgumentHandle::new(&d, &ctx)],
2928                &ctx.function_context(None)
2929            )
2930            .unwrap()
2931            .into_literal(),
2932            LiteralValue::Number(-1.01)
2933        );
2934    }
2935
2936    // MOD
2937    #[test]
2938    fn mod_positive_negative_cases() {
2939        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(ModFn));
2940        let ctx = interp(&wb);
2941        let f = ctx.context.get_function("", "MOD").unwrap();
2942        let a = lit(LiteralValue::Int(-3));
2943        let b = lit(LiteralValue::Int(2));
2944        let out = f
2945            .dispatch(
2946                &[ArgumentHandle::new(&a, &ctx), ArgumentHandle::new(&b, &ctx)],
2947                &ctx.function_context(None),
2948            )
2949            .unwrap();
2950        assert_eq!(out, LiteralValue::Number(1.0));
2951        let a2 = lit(LiteralValue::Int(3));
2952        let b2 = lit(LiteralValue::Int(-2));
2953        let out2 = f
2954            .dispatch(
2955                &[
2956                    ArgumentHandle::new(&a2, &ctx),
2957                    ArgumentHandle::new(&b2, &ctx),
2958                ],
2959                &ctx.function_context(None),
2960            )
2961            .unwrap();
2962        assert_eq!(out2, LiteralValue::Number(-1.0));
2963    }
2964    #[test]
2965    fn mod_div_by_zero_error() {
2966        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(ModFn));
2967        let ctx = interp(&wb);
2968        let f = ctx.context.get_function("", "MOD").unwrap();
2969        let a = lit(LiteralValue::Int(5));
2970        let zero = lit(LiteralValue::Int(0));
2971        match f
2972            .dispatch(
2973                &[
2974                    ArgumentHandle::new(&a, &ctx),
2975                    ArgumentHandle::new(&zero, &ctx),
2976                ],
2977                &ctx.function_context(None),
2978            )
2979            .unwrap()
2980            .into_literal()
2981        {
2982            LiteralValue::Error(e) => assert_eq!(e, "#DIV/0!"),
2983            _ => panic!(),
2984        }
2985    }
2986
2987    // SQRT domain
2988    #[test]
2989    fn sqrt_basic_and_domain() {
2990        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(SqrtFn));
2991        let ctx = interp(&wb);
2992        let f = ctx.context.get_function("", "SQRT").unwrap();
2993        let n = lit(LiteralValue::Number(9.0));
2994        let out = f
2995            .dispatch(
2996                &[ArgumentHandle::new(&n, &ctx)],
2997                &ctx.function_context(None),
2998            )
2999            .unwrap();
3000        assert_eq!(out, LiteralValue::Number(3.0));
3001        let neg = lit(LiteralValue::Number(-1.0));
3002        let out2 = f
3003            .dispatch(
3004                &[ArgumentHandle::new(&neg, &ctx)],
3005                &ctx.function_context(None),
3006            )
3007            .unwrap();
3008        assert!(matches!(out2.into_literal(), LiteralValue::Error(_)));
3009    }
3010
3011    #[test]
3012    fn power_fractional_negative_domain() {
3013        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(PowerFn));
3014        let ctx = interp(&wb);
3015        let f = ctx.context.get_function("", "POWER").unwrap();
3016        let a = lit(LiteralValue::Number(-4.0));
3017        let half = lit(LiteralValue::Number(0.5));
3018        let out = f
3019            .dispatch(
3020                &[
3021                    ArgumentHandle::new(&a, &ctx),
3022                    ArgumentHandle::new(&half, &ctx),
3023                ],
3024                &ctx.function_context(None),
3025            )
3026            .unwrap();
3027        assert!(matches!(out.into_literal(), LiteralValue::Error(_))); // complex -> #NUM!
3028    }
3029
3030    #[test]
3031    fn log_variants() {
3032        let wb = TestWorkbook::new()
3033            .with_function(std::sync::Arc::new(LogFn))
3034            .with_function(std::sync::Arc::new(Log10Fn))
3035            .with_function(std::sync::Arc::new(LnFn));
3036        let ctx = interp(&wb);
3037        let logf = ctx.context.get_function("", "LOG").unwrap();
3038        let log10f = ctx.context.get_function("", "LOG10").unwrap();
3039        let lnf = ctx.context.get_function("", "LN").unwrap();
3040        let n = lit(LiteralValue::Number(100.0));
3041        let base = lit(LiteralValue::Number(10.0));
3042        assert_eq!(
3043            logf.dispatch(
3044                &[
3045                    ArgumentHandle::new(&n, &ctx),
3046                    ArgumentHandle::new(&base, &ctx)
3047                ],
3048                &ctx.function_context(None)
3049            )
3050            .unwrap()
3051            .into_literal(),
3052            LiteralValue::Number(2.0)
3053        );
3054        assert_eq!(
3055            log10f
3056                .dispatch(
3057                    &[ArgumentHandle::new(&n, &ctx)],
3058                    &ctx.function_context(None)
3059                )
3060                .unwrap()
3061                .into_literal(),
3062            LiteralValue::Number(2.0)
3063        );
3064        assert_eq!(
3065            lnf.dispatch(
3066                &[ArgumentHandle::new(&n, &ctx)],
3067                &ctx.function_context(None)
3068            )
3069            .unwrap()
3070            .into_literal(),
3071            LiteralValue::Number(100.0f64.ln())
3072        );
3073    }
3074    #[test]
3075    fn ceiling_floor_basic() {
3076        let wb = TestWorkbook::new()
3077            .with_function(std::sync::Arc::new(CeilingFn))
3078            .with_function(std::sync::Arc::new(FloorFn))
3079            .with_function(std::sync::Arc::new(CeilingMathFn))
3080            .with_function(std::sync::Arc::new(FloorMathFn));
3081        let ctx = interp(&wb);
3082        let c = ctx.context.get_function("", "CEILING").unwrap();
3083        let f = ctx.context.get_function("", "FLOOR").unwrap();
3084        let n = lit(LiteralValue::Number(5.1));
3085        let sig = lit(LiteralValue::Number(2.0));
3086        assert_eq!(
3087            c.dispatch(
3088                &[
3089                    ArgumentHandle::new(&n, &ctx),
3090                    ArgumentHandle::new(&sig, &ctx)
3091                ],
3092                &ctx.function_context(None)
3093            )
3094            .unwrap()
3095            .into_literal(),
3096            LiteralValue::Number(6.0)
3097        );
3098        assert_eq!(
3099            f.dispatch(
3100                &[
3101                    ArgumentHandle::new(&n, &ctx),
3102                    ArgumentHandle::new(&sig, &ctx)
3103                ],
3104                &ctx.function_context(None)
3105            )
3106            .unwrap()
3107            .into_literal(),
3108            LiteralValue::Number(4.0)
3109        );
3110    }
3111
3112    #[test]
3113    fn quotient_basic_and_div_zero() {
3114        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(QuotientFn));
3115        let ctx = interp(&wb);
3116        let f = ctx.context.get_function("", "QUOTIENT").unwrap();
3117
3118        let ten = lit(LiteralValue::Int(10));
3119        let three = lit(LiteralValue::Int(3));
3120        assert_eq!(
3121            f.dispatch(
3122                &[
3123                    ArgumentHandle::new(&ten, &ctx),
3124                    ArgumentHandle::new(&three, &ctx),
3125                ],
3126                &ctx.function_context(None),
3127            )
3128            .unwrap()
3129            .into_literal(),
3130            LiteralValue::Number(3.0)
3131        );
3132
3133        let neg_ten = lit(LiteralValue::Int(-10));
3134        assert_eq!(
3135            f.dispatch(
3136                &[
3137                    ArgumentHandle::new(&neg_ten, &ctx),
3138                    ArgumentHandle::new(&three, &ctx),
3139                ],
3140                &ctx.function_context(None),
3141            )
3142            .unwrap()
3143            .into_literal(),
3144            LiteralValue::Number(-3.0)
3145        );
3146
3147        let zero = lit(LiteralValue::Int(0));
3148        match f
3149            .dispatch(
3150                &[
3151                    ArgumentHandle::new(&ten, &ctx),
3152                    ArgumentHandle::new(&zero, &ctx),
3153                ],
3154                &ctx.function_context(None),
3155            )
3156            .unwrap()
3157            .into_literal()
3158        {
3159            LiteralValue::Error(e) => assert_eq!(e, "#DIV/0!"),
3160            other => panic!("expected #DIV/0!, got {other:?}"),
3161        }
3162    }
3163
3164    #[test]
3165    fn even_odd_examples() {
3166        let wb = TestWorkbook::new()
3167            .with_function(std::sync::Arc::new(EvenFn))
3168            .with_function(std::sync::Arc::new(OddFn));
3169        let ctx = interp(&wb);
3170
3171        let even = ctx.context.get_function("", "EVEN").unwrap();
3172        let odd = ctx.context.get_function("", "ODD").unwrap();
3173
3174        let one_half = lit(LiteralValue::Number(1.5));
3175        let three = lit(LiteralValue::Int(3));
3176        let neg_one = lit(LiteralValue::Int(-1));
3177        let two = lit(LiteralValue::Int(2));
3178        let zero = lit(LiteralValue::Int(0));
3179
3180        assert_eq!(
3181            even.dispatch(
3182                &[ArgumentHandle::new(&one_half, &ctx)],
3183                &ctx.function_context(None),
3184            )
3185            .unwrap()
3186            .into_literal(),
3187            LiteralValue::Number(2.0)
3188        );
3189        assert_eq!(
3190            even.dispatch(
3191                &[ArgumentHandle::new(&three, &ctx)],
3192                &ctx.function_context(None),
3193            )
3194            .unwrap()
3195            .into_literal(),
3196            LiteralValue::Number(4.0)
3197        );
3198        assert_eq!(
3199            even.dispatch(
3200                &[ArgumentHandle::new(&neg_one, &ctx)],
3201                &ctx.function_context(None),
3202            )
3203            .unwrap()
3204            .into_literal(),
3205            LiteralValue::Number(-2.0)
3206        );
3207        assert_eq!(
3208            even.dispatch(
3209                &[ArgumentHandle::new(&two, &ctx)],
3210                &ctx.function_context(None),
3211            )
3212            .unwrap()
3213            .into_literal(),
3214            LiteralValue::Number(2.0)
3215        );
3216
3217        assert_eq!(
3218            odd.dispatch(
3219                &[ArgumentHandle::new(&one_half, &ctx)],
3220                &ctx.function_context(None),
3221            )
3222            .unwrap()
3223            .into_literal(),
3224            LiteralValue::Number(3.0)
3225        );
3226        assert_eq!(
3227            odd.dispatch(
3228                &[ArgumentHandle::new(&two, &ctx)],
3229                &ctx.function_context(None),
3230            )
3231            .unwrap()
3232            .into_literal(),
3233            LiteralValue::Number(3.0)
3234        );
3235        assert_eq!(
3236            odd.dispatch(
3237                &[ArgumentHandle::new(&neg_one, &ctx)],
3238                &ctx.function_context(None),
3239            )
3240            .unwrap()
3241            .into_literal(),
3242            LiteralValue::Number(-1.0)
3243        );
3244        assert_eq!(
3245            odd.dispatch(
3246                &[ArgumentHandle::new(&zero, &ctx)],
3247                &ctx.function_context(None),
3248            )
3249            .unwrap()
3250            .into_literal(),
3251            LiteralValue::Number(1.0)
3252        );
3253    }
3254
3255    #[test]
3256    fn sqrtpi_multinomial_and_seriessum_examples() {
3257        let wb = TestWorkbook::new()
3258            .with_function(std::sync::Arc::new(SqrtPiFn))
3259            .with_function(std::sync::Arc::new(MultinomialFn))
3260            .with_function(std::sync::Arc::new(SeriesSumFn));
3261        let ctx = interp(&wb);
3262
3263        let sqrtpi = ctx.context.get_function("", "SQRTPI").unwrap();
3264        let one = lit(LiteralValue::Int(1));
3265        match sqrtpi
3266            .dispatch(
3267                &[ArgumentHandle::new(&one, &ctx)],
3268                &ctx.function_context(None),
3269            )
3270            .unwrap()
3271            .into_literal()
3272        {
3273            LiteralValue::Number(v) => assert!((v - std::f64::consts::PI.sqrt()).abs() < 1e-12),
3274            other => panic!("expected numeric SQRTPI, got {other:?}"),
3275        }
3276
3277        let multinomial = ctx.context.get_function("", "MULTINOMIAL").unwrap();
3278        let two = lit(LiteralValue::Int(2));
3279        let three = lit(LiteralValue::Int(3));
3280        let four = lit(LiteralValue::Int(4));
3281        assert_eq!(
3282            multinomial
3283                .dispatch(
3284                    &[
3285                        ArgumentHandle::new(&two, &ctx),
3286                        ArgumentHandle::new(&three, &ctx),
3287                        ArgumentHandle::new(&four, &ctx),
3288                    ],
3289                    &ctx.function_context(None),
3290                )
3291                .unwrap()
3292                .into_literal(),
3293            LiteralValue::Number(1260.0)
3294        );
3295
3296        let seriessum = ctx.context.get_function("", "SERIESSUM").unwrap();
3297        let x = lit(LiteralValue::Int(2));
3298        let n0 = lit(LiteralValue::Int(0));
3299        let m1 = lit(LiteralValue::Int(1));
3300        let coeffs = ASTNode::new(
3301            ASTNodeType::Literal(LiteralValue::Array(vec![vec![
3302                LiteralValue::Int(1),
3303                LiteralValue::Int(2),
3304                LiteralValue::Int(3),
3305            ]])),
3306            None,
3307        );
3308        assert_eq!(
3309            seriessum
3310                .dispatch(
3311                    &[
3312                        ArgumentHandle::new(&x, &ctx),
3313                        ArgumentHandle::new(&n0, &ctx),
3314                        ArgumentHandle::new(&m1, &ctx),
3315                        ArgumentHandle::new(&coeffs, &ctx),
3316                    ],
3317                    &ctx.function_context(None),
3318                )
3319                .unwrap()
3320                .into_literal(),
3321            LiteralValue::Number(17.0)
3322        );
3323    }
3324
3325    #[test]
3326    fn sumsq_basic() {
3327        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(SumsqFn));
3328        let ctx = interp(&wb);
3329        let f = ctx.context.get_function("", "SUMSQ").unwrap();
3330        let a = lit(LiteralValue::Int(3));
3331        let b = lit(LiteralValue::Int(4));
3332        assert_eq!(
3333            f.dispatch(
3334                &[ArgumentHandle::new(&a, &ctx), ArgumentHandle::new(&b, &ctx)],
3335                &ctx.function_context(None)
3336            )
3337            .unwrap()
3338            .into_literal(),
3339            LiteralValue::Number(25.0)
3340        );
3341    }
3342
3343    #[test]
3344    fn mround_sign_and_midpoint() {
3345        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(MroundFn));
3346        let ctx = interp(&wb);
3347        let f = ctx.context.get_function("", "MROUND").unwrap();
3348
3349        let n = lit(LiteralValue::Number(1.3));
3350        let m = lit(LiteralValue::Number(0.2));
3351        match f
3352            .dispatch(
3353                &[ArgumentHandle::new(&n, &ctx), ArgumentHandle::new(&m, &ctx)],
3354                &ctx.function_context(None),
3355            )
3356            .unwrap()
3357            .into_literal()
3358        {
3359            LiteralValue::Number(v) => assert!((v - 1.4).abs() < 1e-12),
3360            other => panic!("expected numeric result, got {other:?}"),
3361        }
3362
3363        let bad_m = lit(LiteralValue::Number(-2.0));
3364        let five = lit(LiteralValue::Number(5.0));
3365        match f
3366            .dispatch(
3367                &[
3368                    ArgumentHandle::new(&five, &ctx),
3369                    ArgumentHandle::new(&bad_m, &ctx),
3370                ],
3371                &ctx.function_context(None),
3372            )
3373            .unwrap()
3374            .into_literal()
3375        {
3376            LiteralValue::Error(e) => assert_eq!(e, "#NUM!"),
3377            other => panic!("expected #NUM!, got {other:?}"),
3378        }
3379    }
3380
3381    #[test]
3382    fn roman_and_arabic_examples() {
3383        let wb = TestWorkbook::new()
3384            .with_function(std::sync::Arc::new(RomanFn))
3385            .with_function(std::sync::Arc::new(ArabicFn));
3386        let ctx = interp(&wb);
3387
3388        let roman = ctx.context.get_function("", "ROMAN").unwrap();
3389        let n499 = lit(LiteralValue::Int(499));
3390        let out = roman
3391            .dispatch(
3392                &[ArgumentHandle::new(&n499, &ctx)],
3393                &ctx.function_context(None),
3394            )
3395            .unwrap()
3396            .into_literal();
3397        assert_eq!(out, LiteralValue::Text("CDXCIX".to_string()));
3398
3399        let form4 = lit(LiteralValue::Int(4));
3400        let out_form4 = roman
3401            .dispatch(
3402                &[
3403                    ArgumentHandle::new(&n499, &ctx),
3404                    ArgumentHandle::new(&form4, &ctx),
3405                ],
3406                &ctx.function_context(None),
3407            )
3408            .unwrap()
3409            .into_literal();
3410        assert_eq!(out_form4, LiteralValue::Text("ID".to_string()));
3411
3412        let arabic = ctx.context.get_function("", "ARABIC").unwrap();
3413        let roman_text = lit(LiteralValue::Text("CDXCIX".to_string()));
3414        let out_arabic = arabic
3415            .dispatch(
3416                &[ArgumentHandle::new(&roman_text, &ctx)],
3417                &ctx.function_context(None),
3418            )
3419            .unwrap()
3420            .into_literal();
3421        assert_eq!(out_arabic, LiteralValue::Number(499.0));
3422    }
3423}