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