Skip to main content

formualizer_eval/builtins/stats/
mod.rs

1//! Statistical basic functions (Sprint 6)
2//!
3//! Implementations target Excel semantic parity for:
4//! LARGE, SMALL, RANK.EQ, RANK.AVG, MEDIAN, STDEV.S, STDEV.P, VAR.S, VAR.P,
5//! PERCENTILE.INC, PERCENTILE.EXC, QUARTILE.INC, QUARTILE.EXC.
6//!
7//! Notes:
8//! - We currently materialize numeric values into a Vec<f64>. For large ranges this could be
9//!   optimized with streaming selection algorithms (nth_element / partial sort). TODO(perf).
10//! - Text/boolean coercion nuance: For Excel statistical functions, values coming from range
11//!   references should ignore text and logical values (they are skipped), while direct scalar
12//!   arguments still coerce (e.g. =STDEV(1,TRUE) treats TRUE as 1). This file now implements that
13//!   distinction. TODO(excel-nuance): refine numeric text literal vs non‑numeric text handling.
14//! - Errors encountered in any argument propagate immediately.
15//! - Empty numeric sets produce Excel-specific errors (#NUM! for LARGE/SMALL, #N/A for rank target
16//!   out of range, #DIV/0! for STDEV/VAR sample with n < 2, etc.).
17
18use super::super::builtins::utils::{ARG_RANGE_NUM_LENIENT_ONE, coerce_num};
19use crate::args::ArgSchema;
20use crate::function::Function;
21use crate::function_contract::FunctionDependencyContract;
22use crate::traits::{ArgumentHandle, FunctionContext};
23use formualizer_common::{ExcelError, LiteralValue};
24// use std::collections::BTreeMap; // removed unused import
25use formualizer_macros::func_caps;
26
27fn scalar_like_value(arg: &ArgumentHandle<'_, '_>) -> Result<LiteralValue, ExcelError> {
28    Ok(match arg.value()? {
29        crate::traits::CalcValue::Scalar(v) => v,
30        crate::traits::CalcValue::Range(rv) => rv.get_cell(0, 0),
31        crate::traits::CalcValue::Callable(_) => LiteralValue::Error(
32            ExcelError::new(formualizer_common::ExcelErrorKind::Calc)
33                .with_message("LAMBDA value must be invoked"),
34        ),
35    })
36}
37
38/// Collect numeric inputs applying Excel statistical semantics:
39/// - Range references: include only numeric cells; skip text, logical, blank. Errors propagate.
40/// - Direct scalar arguments: attempt numeric coercion (so TRUE/FALSE, numeric text are included if
41///   coerce_num succeeds). Non-numeric text is ignored (Excel would treat a direct non-numeric text
42///   argument as #VALUE! in some contexts; covered by TODO for finer parity).
43fn collect_numeric_stats(args: &[ArgumentHandle]) -> Result<Vec<f64>, ExcelError> {
44    let mut out = Vec::new();
45    for a in args {
46        // Special-case: inline array literal argument should be treated like a list of direct scalar
47        // arguments (not a by-ref range). This allows boolean/text coercion per element akin to
48        // passing multiple scalars to the function.
49        if let Some(arr) = a.inline_array_literal()? {
50            for row in arr.into_iter() {
51                for cell in row.into_iter() {
52                    match cell {
53                        LiteralValue::Error(e) => return Err(e),
54                        other => {
55                            if let Ok(n) = coerce_num(&other) {
56                                out.push(n);
57                            }
58                        }
59                    }
60                }
61            }
62            continue;
63        }
64
65        if let Ok(view) = a.range_view() {
66            view.for_each_cell(&mut |v| {
67                match v {
68                    LiteralValue::Error(e) => return Err(e.clone()),
69                    LiteralValue::Number(n) => out.push(*n),
70                    LiteralValue::Int(i) => out.push(*i as f64),
71                    _ => {}
72                }
73                Ok(())
74            })?;
75        } else {
76            let v = scalar_like_value(a)?;
77            match v {
78                LiteralValue::Error(e) => return Err(e),
79                other => {
80                    if let Ok(n) = coerce_num(&other) {
81                        out.push(n);
82                    }
83                }
84            }
85        }
86    }
87    Ok(out)
88}
89
90fn percentile_inc(sorted: &[f64], p: f64) -> Result<f64, ExcelError> {
91    if sorted.is_empty() {
92        return Err(ExcelError::new_num());
93    }
94    if !(0.0..=1.0).contains(&p) {
95        return Err(ExcelError::new_num());
96    }
97    if sorted.len() == 1 {
98        return Ok(sorted[0]);
99    }
100    let n = sorted.len() as f64;
101    let rank = p * (n - 1.0); // 0-based rank
102    let lo = rank.floor() as usize;
103    let hi = rank.ceil() as usize;
104    if lo == hi {
105        return Ok(sorted[lo]);
106    }
107    let frac = rank - (lo as f64);
108    Ok(sorted[lo] + (sorted[hi] - sorted[lo]) * frac)
109}
110
111fn percentile_exc(sorted: &[f64], p: f64) -> Result<f64, ExcelError> {
112    // Excel PERCENTILE.EXC requires 0 < p < 1 and uses (n+1) basis; invalid if rank<1 or >n
113    if sorted.is_empty() {
114        return Err(ExcelError::new_num());
115    }
116    if !(0.0..=1.0).contains(&p) || p <= 0.0 || p >= 1.0 {
117        return Err(ExcelError::new_num());
118    }
119    let n = sorted.len() as f64;
120    let rank = p * (n + 1.0); // 1..n domain
121    if rank < 1.0 || rank > n {
122        return Err(ExcelError::new_num());
123    }
124    let lo = rank.floor();
125    let hi = rank.ceil();
126    if (lo - hi).abs() < f64::EPSILON {
127        return Ok(sorted[(lo as usize) - 1]);
128    }
129    let frac = rank - lo;
130    let lo_idx = (lo as usize) - 1;
131    let hi_idx = (hi as usize) - 1;
132    Ok(sorted[lo_idx] + (sorted[hi_idx] - sorted[lo_idx]) * frac)
133}
134
135/// Returns the rank position of a number within a data set, with ties sharing the same rank.
136///
137/// `RANK.EQ` defaults to descending order (largest value is rank 1), and can switch to ascending
138/// order when `order` is non-zero.
139///
140/// # Remarks
141/// - Omitting `order`, or setting `order` to `0`, ranks values in descending order.
142/// - Any non-zero `order` ranks values in ascending order.
143/// - Tied values receive the same rank (the first matching position in the sorted list).
144/// - Returns `#N/A` if `number` is not found in `ref`.
145///
146/// # Examples
147///
148/// ```yaml,sandbox
149/// title: "Descending rank with direct values"
150/// formula: "=RANK.EQ(7,{10,7,4,2})"
151/// expected: 2
152/// ```
153///
154/// ```yaml,sandbox
155/// title: "Ascending rank with ties in a range"
156/// grid:
157///   A1: 50
158///   A2: 20
159///   A3: 20
160///   A4: 10
161///   A5: 5
162/// formula: "=RANK.EQ(A2,A1:A5,1)"
163/// expected: 3
164/// ```
165///
166/// ```yaml,docs
167/// related:
168///   - RANK.AVG
169///   - LARGE
170///   - SMALL
171/// faq:
172///   - q: "When does RANK.EQ return #N/A?"
173///     a: "It returns #N/A when the target number does not appear in the reference set."
174/// ```
175#[derive(Debug)]
176pub struct RankEqFn;
177/// [formualizer-docgen:schema:start]
178/// Name: RANK.EQ
179/// Type: RankEqFn
180/// Min args: 2
181/// Max args: variadic
182/// Variadic: true
183/// Signature: RANK.EQ(arg1...: number@range)
184/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
185/// Caps: PURE, NUMERIC_ONLY
186/// [formualizer-docgen:schema:end]
187impl Function for RankEqFn {
188    func_caps!(PURE, NUMERIC_ONLY);
189    fn name(&self) -> &'static str {
190        "RANK.EQ"
191    }
192    fn aliases(&self) -> &'static [&'static str] {
193        &["RANK"]
194    }
195    fn min_args(&self) -> usize {
196        2
197    }
198    fn variadic(&self) -> bool {
199        true
200    } // allow optional order
201    fn arg_schema(&self) -> &'static [ArgSchema] {
202        &ARG_RANGE_NUM_LENIENT_ONE[..]
203    }
204    fn eval<'a, 'b, 'c>(
205        &self,
206        args: &'c [ArgumentHandle<'a, 'b>],
207        _ctx: &dyn FunctionContext<'b>,
208    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
209        if args.len() < 2 {
210            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
211                ExcelError::new_na(),
212            )));
213        }
214        let target = match coerce_num(&args[0].value()?.into_literal()) {
215            Ok(n) => n,
216            Err(_) => {
217                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
218                    ExcelError::new_na(),
219                )));
220            }
221        };
222        // optional order arg at end if 3 args
223        let order = if args.len() >= 3 {
224            coerce_num(&args[2].value()?.into_literal()).unwrap_or(0.0)
225        } else {
226            0.0
227        };
228        let nums = collect_numeric_stats(&args[1..2])?; // only one ref range per Excel spec
229        if nums.is_empty() {
230            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
231                ExcelError::new_na(),
232            )));
233        }
234        let mut sorted = nums; // copy
235        if order.abs() < 1e-12 {
236            // descending
237            sorted.sort_by(|a, b| b.partial_cmp(a).unwrap());
238        } else {
239            // ascending
240            sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
241        }
242        for (i, &v) in sorted.iter().enumerate() {
243            if (v - target).abs() < 1e-12 {
244                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
245                    (i + 1) as f64,
246                )));
247            }
248        }
249        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
250            ExcelError::new_na(),
251        )))
252    }
253}
254
255/// Returns the rank position of a number, averaging the rank positions for ties.
256///
257/// Use `RANK.AVG` when tied values should share the average of their occupied rank positions.
258///
259/// # Remarks
260/// - Omitting `order`, or setting `order` to `0`, ranks values in descending order.
261/// - Any non-zero `order` ranks values in ascending order.
262/// - If `number` appears multiple times, the function returns the mean of those rank positions.
263/// - Returns `#N/A` if `number` is not found in `ref`.
264///
265/// # Examples
266///
267/// ```yaml,sandbox
268/// title: "Average rank for tied values"
269/// formula: "=RANK.AVG(20,{30,20,20,10})"
270/// expected: 2.5
271/// ```
272///
273/// ```yaml,sandbox
274/// title: "Ascending average rank from a range"
275/// grid:
276///   A1: 50
277///   A2: 20
278///   A3: 20
279///   A4: 10
280///   A5: 5
281/// formula: "=RANK.AVG(A2,A1:A5,1)"
282/// expected: 3.5
283/// ```
284///
285/// ```yaml,docs
286/// related:
287///   - RANK.EQ
288///   - LARGE
289///   - SMALL
290/// faq:
291///   - q: "How are ties handled by RANK.AVG?"
292///     a: "All tied occurrences share the average of their rank positions."
293/// ```
294#[derive(Debug)]
295pub struct RankAvgFn;
296/// [formualizer-docgen:schema:start]
297/// Name: RANK.AVG
298/// Type: RankAvgFn
299/// Min args: 2
300/// Max args: variadic
301/// Variadic: true
302/// Signature: RANK.AVG(arg1...: number@range)
303/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
304/// Caps: PURE, NUMERIC_ONLY
305/// [formualizer-docgen:schema:end]
306impl Function for RankAvgFn {
307    func_caps!(PURE, NUMERIC_ONLY);
308    fn name(&self) -> &'static str {
309        "RANK.AVG"
310    }
311    fn min_args(&self) -> usize {
312        2
313    }
314    fn variadic(&self) -> bool {
315        true
316    }
317    fn arg_schema(&self) -> &'static [ArgSchema] {
318        &ARG_RANGE_NUM_LENIENT_ONE[..]
319    }
320    fn eval<'a, 'b, 'c>(
321        &self,
322        args: &'c [ArgumentHandle<'a, 'b>],
323        _ctx: &dyn FunctionContext<'b>,
324    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
325        if args.len() < 2 {
326            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
327                ExcelError::new_na(),
328            )));
329        }
330        let t0 = scalar_like_value(&args[0])?;
331        let target = match coerce_num(&t0) {
332            Ok(n) => n,
333            Err(_) => {
334                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
335                    ExcelError::new_na(),
336                )));
337            }
338        };
339        let order = if args.len() >= 3 {
340            let ord = scalar_like_value(&args[2])?;
341            coerce_num(&ord).unwrap_or(0.0)
342        } else {
343            0.0
344        };
345        let nums = collect_numeric_stats(&args[1..2])?;
346        if nums.is_empty() {
347            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
348                ExcelError::new_na(),
349            )));
350        }
351        let mut sorted = nums;
352        if order.abs() < 1e-12 {
353            sorted.sort_by(|a, b| b.partial_cmp(a).unwrap());
354        } else {
355            sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
356        }
357        let mut positions = Vec::new();
358        for (i, &v) in sorted.iter().enumerate() {
359            if (v - target).abs() < 1e-12 {
360                positions.push(i + 1);
361            }
362        }
363        if positions.is_empty() {
364            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
365                ExcelError::new_na(),
366            )));
367        }
368        let avg = positions.iter().copied().sum::<usize>() as f64 / positions.len() as f64;
369        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(avg)))
370    }
371}
372
373/// Returns the k-th largest value in a data set.
374///
375/// `LARGE` is useful for top-N analysis, such as highest score, second-highest sale, or third-best
376/// result.
377///
378/// # Remarks
379/// - `k` must be at least `1`.
380/// - Returns `#NUM!` if `k` is greater than the count of numeric values.
381/// - Non-numeric values in referenced ranges are ignored.
382///
383/// # Examples
384///
385/// ```yaml,sandbox
386/// title: "Second-largest from direct values"
387/// formula: "=LARGE({4,9,1,7},2)"
388/// expected: 7
389/// ```
390///
391/// ```yaml,sandbox
392/// title: "Third-largest from a range"
393/// grid:
394///   A1: 3
395///   A2: 12
396///   A3: 8
397///   A4: 5
398/// formula: "=LARGE(A1:A4,3)"
399/// expected: 5
400/// ```
401///
402/// ```yaml,docs
403/// related:
404///   - SMALL
405///   - MAX
406///   - RANK.EQ
407/// faq:
408///   - q: "When does LARGE return #NUM!?"
409///     a: "It returns #NUM! when k < 1, k exceeds numeric count, or no numeric values exist."
410/// ```
411#[derive(Debug)]
412pub struct LARGE;
413/// [formualizer-docgen:schema:start]
414/// Name: LARGE
415/// Type: LARGE
416/// Min args: 2
417/// Max args: variadic
418/// Variadic: true
419/// Signature: LARGE(arg1...: number@range)
420/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
421/// Caps: PURE, REDUCTION, NUMERIC_ONLY
422/// [formualizer-docgen:schema:end]
423impl Function for LARGE {
424    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
425    fn name(&self) -> &'static str {
426        "LARGE"
427    }
428    fn min_args(&self) -> usize {
429        2
430    }
431    fn variadic(&self) -> bool {
432        true
433    }
434    fn arg_schema(&self) -> &'static [ArgSchema] {
435        &ARG_RANGE_NUM_LENIENT_ONE[..]
436    }
437    fn eval<'a, 'b, 'c>(
438        &self,
439        args: &'c [ArgumentHandle<'a, 'b>],
440        _ctx: &dyn FunctionContext<'b>,
441    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
442        if args.len() < 2 {
443            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
444                ExcelError::new_num(),
445            )));
446        }
447        let k = match coerce_num(&args.last().unwrap().value()?.into_literal()) {
448            Ok(n) => n,
449            Err(_) => {
450                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
451                    ExcelError::new_num(),
452                )));
453            }
454        };
455        let k = k as i64;
456        if k < 1 {
457            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
458                ExcelError::new_num(),
459            )));
460        }
461        let mut nums = collect_numeric_stats(&args[..args.len() - 1])?;
462        if nums.is_empty() || k as usize > nums.len() {
463            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
464                ExcelError::new_num(),
465            )));
466        }
467        nums.sort_by(|a, b| b.partial_cmp(a).unwrap());
468        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
469            nums[(k as usize) - 1],
470        )))
471    }
472}
473
474/// Returns the k-th smallest value in a data set.
475///
476/// `SMALL` is often used to find low outliers, minimum thresholds, or bottom-N values.
477///
478/// # Remarks
479/// - `k` must be at least `1`.
480/// - Returns `#NUM!` if `k` is greater than the count of numeric values.
481/// - Non-numeric values in referenced ranges are ignored.
482///
483/// # Examples
484///
485/// ```yaml,sandbox
486/// title: "Second-smallest from direct values"
487/// formula: "=SMALL({4,9,1,7},2)"
488/// expected: 4
489/// ```
490///
491/// ```yaml,sandbox
492/// title: "Third-smallest from a range"
493/// grid:
494///   A1: 3
495///   A2: 12
496///   A3: 8
497///   A4: 5
498/// formula: "=SMALL(A1:A4,3)"
499/// expected: 8
500/// ```
501///
502/// ```yaml,docs
503/// related:
504///   - LARGE
505///   - MIN
506///   - RANK.EQ
507/// faq:
508///   - q: "Does SMALL include text in referenced ranges?"
509///     a: "No. Non-numeric range values are ignored when selecting the k-th smallest value."
510/// ```
511#[derive(Debug)]
512pub struct SMALL;
513/// [formualizer-docgen:schema:start]
514/// Name: SMALL
515/// Type: SMALL
516/// Min args: 2
517/// Max args: variadic
518/// Variadic: true
519/// Signature: SMALL(arg1...: number@range)
520/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
521/// Caps: PURE, REDUCTION, NUMERIC_ONLY
522/// [formualizer-docgen:schema:end]
523impl Function for SMALL {
524    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
525    fn name(&self) -> &'static str {
526        "SMALL"
527    }
528    fn min_args(&self) -> usize {
529        2
530    }
531    fn variadic(&self) -> bool {
532        true
533    }
534    fn arg_schema(&self) -> &'static [ArgSchema] {
535        &ARG_RANGE_NUM_LENIENT_ONE[..]
536    }
537    fn eval<'a, 'b, 'c>(
538        &self,
539        args: &'c [ArgumentHandle<'a, 'b>],
540        _ctx: &dyn FunctionContext<'b>,
541    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
542        if args.len() < 2 {
543            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
544                ExcelError::new_num(),
545            )));
546        }
547        let k = match coerce_num(&args.last().unwrap().value()?.into_literal()) {
548            Ok(n) => n,
549            Err(_) => {
550                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
551                    ExcelError::new_num(),
552                )));
553            }
554        };
555        let k = k as i64;
556        if k < 1 {
557            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
558                ExcelError::new_num(),
559            )));
560        }
561        let mut nums = collect_numeric_stats(&args[..args.len() - 1])?;
562        if nums.is_empty() || k as usize > nums.len() {
563            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
564                ExcelError::new_num(),
565            )));
566        }
567        nums.sort_by(|a, b| a.partial_cmp(b).unwrap());
568        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
569            nums[(k as usize) - 1],
570        )))
571    }
572}
573
574/// Returns the middle value of a numeric data set.
575///
576/// For an even number of values, `MEDIAN` returns the average of the two center values.
577///
578/// # Remarks
579/// - Ignores non-numeric values in referenced ranges.
580/// - Returns `#NUM!` when no numeric values are available.
581/// - Supports both scalar arguments and range inputs.
582///
583/// # Examples
584///
585/// ```yaml,sandbox
586/// title: "Median of an odd-sized set"
587/// formula: "=MEDIAN(1,3,8)"
588/// expected: 3
589/// ```
590///
591/// ```yaml,sandbox
592/// title: "Median of an even-sized range"
593/// grid:
594///   A1: 1
595///   A2: 2
596///   A3: 10
597///   A4: 12
598/// formula: "=MEDIAN(A1:A4)"
599/// expected: 6
600/// ```
601///
602/// ```yaml,docs
603/// related:
604///   - AVERAGE
605///   - MODE.SNGL
606///   - QUARTILE.INC
607/// faq:
608///   - q: "When does MEDIAN return #NUM!?"
609///     a: "MEDIAN returns #NUM! when no numeric values are available after filtering/coercion."
610/// ```
611#[derive(Debug)]
612pub struct MEDIAN;
613/// [formualizer-docgen:schema:start]
614/// Name: MEDIAN
615/// Type: MEDIAN
616/// Min args: 1
617/// Max args: variadic
618/// Variadic: true
619/// Signature: MEDIAN(arg1...: number@range)
620/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
621/// Caps: PURE, REDUCTION, NUMERIC_ONLY
622/// [formualizer-docgen:schema:end]
623impl Function for MEDIAN {
624    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
625    fn name(&self) -> &'static str {
626        "MEDIAN"
627    }
628    fn min_args(&self) -> usize {
629        1
630    }
631    fn variadic(&self) -> bool {
632        true
633    }
634    fn arg_schema(&self) -> &'static [ArgSchema] {
635        &ARG_RANGE_NUM_LENIENT_ONE[..]
636    }
637    fn eval<'a, 'b, 'c>(
638        &self,
639        args: &'c [ArgumentHandle<'a, 'b>],
640        _ctx: &dyn FunctionContext<'b>,
641    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
642        let mut nums = collect_numeric_stats(args)?;
643        if nums.is_empty() {
644            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
645                ExcelError::new_num(),
646            )));
647        }
648        nums.sort_by(|a, b| a.partial_cmp(b).unwrap());
649        let n = nums.len();
650        let mid = n / 2;
651        let med = if n % 2 == 1 {
652            nums[mid]
653        } else {
654            (nums[mid - 1] + nums[mid]) / 2.0
655        };
656        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(med)))
657    }
658}
659
660/// Estimates sample standard deviation using `n-1` in the denominator.
661///
662/// `STDEV.S` measures spread when your values represent a sample of a larger population.
663///
664/// # Remarks
665/// - Requires at least two numeric values.
666/// - Returns `#DIV/0!` when fewer than two numeric values are provided.
667/// - Non-numeric values in referenced ranges are ignored.
668///
669/// # Examples
670///
671/// ```yaml,sandbox
672/// title: "Sample standard deviation from scalar arguments"
673/// formula: "=STDEV.S(2,4,6)"
674/// expected: 2
675/// ```
676///
677/// ```yaml,sandbox
678/// title: "Sample standard deviation from a range"
679/// grid:
680///   A1: 5
681///   A2: 7
682///   A3: 9
683/// formula: "=STDEV.S(A1:A3)"
684/// expected: 2
685/// ```
686///
687/// ```yaml,docs
688/// related:
689///   - STDEV.P
690///   - VAR.S
691///   - VAR.P
692/// faq:
693///   - q: "Why does STDEV.S return #DIV/0!?"
694///     a: "Sample standard deviation needs at least two numeric values."
695/// ```
696#[derive(Debug)]
697pub struct StdevSample; // sample
698/// [formualizer-docgen:schema:start]
699/// Name: STDEV.S
700/// Type: StdevSample
701/// Min args: 1
702/// Max args: variadic
703/// Variadic: true
704/// Signature: STDEV.S(arg1...: number@range)
705/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
706/// Caps: PURE, REDUCTION, NUMERIC_ONLY, STREAM_OK
707/// [formualizer-docgen:schema:end]
708impl Function for StdevSample {
709    func_caps!(PURE, NUMERIC_ONLY, REDUCTION, STREAM_OK);
710    fn name(&self) -> &'static str {
711        "STDEV.S"
712    }
713    fn aliases(&self) -> &'static [&'static str] {
714        &["STDEV"]
715    }
716    fn min_args(&self) -> usize {
717        1
718    }
719    fn variadic(&self) -> bool {
720        true
721    }
722    fn arg_schema(&self) -> &'static [ArgSchema] {
723        &ARG_RANGE_NUM_LENIENT_ONE[..]
724    }
725    fn eval<'a, 'b, 'c>(
726        &self,
727        args: &'c [ArgumentHandle<'a, 'b>],
728        _ctx: &dyn FunctionContext<'b>,
729    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
730        let nums = collect_numeric_stats(args)?;
731        let n = nums.len();
732        if n < 2 {
733            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
734                ExcelError::from_error_string("#DIV/0!"),
735            )));
736        }
737        let mean = nums.iter().sum::<f64>() / (n as f64);
738        let mut ss = 0.0;
739        for &v in &nums {
740            let d = v - mean;
741            ss += d * d;
742        }
743        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
744            (ss / ((n - 1) as f64)).sqrt(),
745        )))
746    }
747}
748
749/// Returns population standard deviation using `n` in the denominator.
750///
751/// Use `STDEV.P` when your values represent the entire population, not a sample.
752///
753/// # Remarks
754/// - Requires at least one numeric value.
755/// - Returns `#DIV/0!` when no numeric values are provided.
756/// - Non-numeric values in referenced ranges are ignored.
757///
758/// # Examples
759///
760/// ```yaml,sandbox
761/// title: "Population standard deviation from scalar arguments"
762/// formula: "=STDEV.P(2,4,6)"
763/// expected: 1.632993161855452
764/// ```
765///
766/// ```yaml,sandbox
767/// title: "Population standard deviation from a range"
768/// grid:
769///   A1: 1
770///   A2: 2
771///   A3: 3
772/// formula: "=STDEV.P(A1:A3)"
773/// expected: 0.816496580927726
774/// ```
775///
776/// ```yaml,docs
777/// related:
778///   - STDEV.S
779///   - VAR.P
780///   - VAR.S
781/// faq:
782///   - q: "When does STDEV.P return #DIV/0!?"
783///     a: "It returns #DIV/0! when no numeric values are provided."
784/// ```
785#[derive(Debug)]
786pub struct StdevPop; // population
787/// [formualizer-docgen:schema:start]
788/// Name: STDEV.P
789/// Type: StdevPop
790/// Min args: 1
791/// Max args: variadic
792/// Variadic: true
793/// Signature: STDEV.P(arg1...: number@range)
794/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
795/// Caps: PURE, REDUCTION, NUMERIC_ONLY, STREAM_OK
796/// [formualizer-docgen:schema:end]
797impl Function for StdevPop {
798    func_caps!(PURE, NUMERIC_ONLY, REDUCTION, STREAM_OK);
799    fn name(&self) -> &'static str {
800        "STDEV.P"
801    }
802    fn aliases(&self) -> &'static [&'static str] {
803        &["STDEVP"]
804    }
805    fn min_args(&self) -> usize {
806        1
807    }
808    fn variadic(&self) -> bool {
809        true
810    }
811    fn arg_schema(&self) -> &'static [ArgSchema] {
812        &ARG_RANGE_NUM_LENIENT_ONE[..]
813    }
814    fn eval<'a, 'b, 'c>(
815        &self,
816        args: &'c [ArgumentHandle<'a, 'b>],
817        _ctx: &dyn FunctionContext<'b>,
818    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
819        let nums = collect_numeric_stats(args)?;
820        let n = nums.len();
821        if n == 0 {
822            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
823                ExcelError::from_error_string("#DIV/0!"),
824            )));
825        }
826        let mean = nums.iter().sum::<f64>() / (n as f64);
827        let mut ss = 0.0;
828        for &v in &nums {
829            let d = v - mean;
830            ss += d * d;
831        }
832        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
833            (ss / (n as f64)).sqrt(),
834        )))
835    }
836}
837
838/// Estimates sample variance using `n-1` in the denominator.
839///
840/// `VAR.S` is the squared counterpart of `STDEV.S` for sample-based variability.
841///
842/// # Remarks
843/// - Requires at least two numeric values.
844/// - Returns `#DIV/0!` when fewer than two numeric values are provided.
845/// - Non-numeric values in referenced ranges are ignored.
846///
847/// # Examples
848///
849/// ```yaml,sandbox
850/// title: "Sample variance from scalar arguments"
851/// formula: "=VAR.S(2,4,6)"
852/// expected: 4
853/// ```
854///
855/// ```yaml,sandbox
856/// title: "Sample variance from a range"
857/// grid:
858///   A1: 1
859///   A2: 2
860///   A3: 3
861/// formula: "=VAR.S(A1:A3)"
862/// expected: 1
863/// ```
864///
865/// ```yaml,docs
866/// related:
867///   - VAR.P
868///   - STDEV.S
869///   - STDEV.P
870/// faq:
871///   - q: "Why does VAR.S return #DIV/0!?"
872///     a: "Sample variance requires at least two numeric observations."
873/// ```
874#[derive(Debug)]
875pub struct VarSample; // sample variance
876/// [formualizer-docgen:schema:start]
877/// Name: VAR.S
878/// Type: VarSample
879/// Min args: 1
880/// Max args: variadic
881/// Variadic: true
882/// Signature: VAR.S(arg1...: number@range)
883/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
884/// Caps: PURE, REDUCTION, NUMERIC_ONLY, STREAM_OK
885/// [formualizer-docgen:schema:end]
886impl Function for VarSample {
887    func_caps!(PURE, NUMERIC_ONLY, REDUCTION, STREAM_OK);
888    fn name(&self) -> &'static str {
889        "VAR.S"
890    }
891    fn aliases(&self) -> &'static [&'static str] {
892        &["VAR"]
893    }
894    fn min_args(&self) -> usize {
895        1
896    }
897    fn variadic(&self) -> bool {
898        true
899    }
900    fn arg_schema(&self) -> &'static [ArgSchema] {
901        &ARG_RANGE_NUM_LENIENT_ONE[..]
902    }
903    fn eval<'a, 'b, 'c>(
904        &self,
905        args: &'c [ArgumentHandle<'a, 'b>],
906        _ctx: &dyn FunctionContext<'b>,
907    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
908        let nums = collect_numeric_stats(args)?;
909        let n = nums.len();
910        if n < 2 {
911            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
912                ExcelError::from_error_string("#DIV/0!"),
913            )));
914        }
915        let mean = nums.iter().sum::<f64>() / (n as f64);
916        let mut ss = 0.0;
917        for &v in &nums {
918            let d = v - mean;
919            ss += d * d;
920        }
921        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
922            ss / ((n - 1) as f64),
923        )))
924    }
925}
926
927/// Returns population variance using `n` in the denominator.
928///
929/// `VAR.P` describes dispersion for a complete population of numeric values.
930///
931/// # Remarks
932/// - Requires at least one numeric value.
933/// - Returns `#DIV/0!` when no numeric values are provided.
934/// - Non-numeric values in referenced ranges are ignored.
935///
936/// # Examples
937///
938/// ```yaml,sandbox
939/// title: "Population variance from scalar arguments"
940/// formula: "=VAR.P(2,4,6)"
941/// expected: 2.6666666666666665
942/// ```
943///
944/// ```yaml,sandbox
945/// title: "Population variance from a range"
946/// grid:
947///   A1: 1
948///   A2: 2
949///   A3: 3
950/// formula: "=VAR.P(A1:A3)"
951/// expected: 0.6666666666666666
952/// ```
953///
954/// ```yaml,docs
955/// related:
956///   - VAR.S
957///   - STDEV.P
958///   - STDEV.S
959/// faq:
960///   - q: "What is the denominator difference vs VAR.S?"
961///     a: "VAR.P divides by n, while VAR.S divides by n-1."
962/// ```
963#[derive(Debug)]
964pub struct VarPop; // population variance
965/// [formualizer-docgen:schema:start]
966/// Name: VAR.P
967/// Type: VarPop
968/// Min args: 1
969/// Max args: variadic
970/// Variadic: true
971/// Signature: VAR.P(arg1...: number@range)
972/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
973/// Caps: PURE, REDUCTION, NUMERIC_ONLY, STREAM_OK
974/// [formualizer-docgen:schema:end]
975impl Function for VarPop {
976    func_caps!(PURE, NUMERIC_ONLY, REDUCTION, STREAM_OK);
977    fn name(&self) -> &'static str {
978        "VAR.P"
979    }
980    fn aliases(&self) -> &'static [&'static str] {
981        &["VARP"]
982    }
983    fn min_args(&self) -> usize {
984        1
985    }
986    fn variadic(&self) -> bool {
987        true
988    }
989    fn arg_schema(&self) -> &'static [ArgSchema] {
990        &ARG_RANGE_NUM_LENIENT_ONE[..]
991    }
992    fn eval<'a, 'b, 'c>(
993        &self,
994        args: &'c [ArgumentHandle<'a, 'b>],
995        _ctx: &dyn FunctionContext<'b>,
996    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
997        let nums = collect_numeric_stats(args)?;
998        let n = nums.len();
999        if n == 0 {
1000            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1001                ExcelError::from_error_string("#DIV/0!"),
1002            )));
1003        }
1004        let mean = nums.iter().sum::<f64>() / (n as f64);
1005        let mut ss = 0.0;
1006        for &v in &nums {
1007            let d = v - mean;
1008            ss += d * d;
1009        }
1010        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
1011            ss / (n as f64),
1012        )))
1013    }
1014}
1015
1016// MODE.SNGL (alias MODE) and MODE.MULT
1017/// Returns the most frequently occurring value in a data set.
1018///
1019/// `MODE.SNGL` returns a single mode value and reports `#N/A` if no value repeats.
1020///
1021/// # Remarks
1022/// - Returns the first mode encountered after sorting when frequencies tie.
1023/// - Returns `#N/A` when every numeric value appears only once.
1024/// - Alias `MODE` is supported.
1025///
1026/// # Examples
1027///
1028/// ```yaml,sandbox
1029/// title: "Single mode from scalar arguments"
1030/// formula: "=MODE.SNGL(1,2,2,3)"
1031/// expected: 2
1032/// ```
1033///
1034/// ```yaml,sandbox
1035/// title: "Single mode from a range"
1036/// grid:
1037///   A1: 4
1038///   A2: 4
1039///   A3: 6
1040///   A4: 6
1041///   A5: 6
1042/// formula: "=MODE.SNGL(A1:A5)"
1043/// expected: 6
1044/// ```
1045///
1046/// ```yaml,docs
1047/// related:
1048///   - MODE.MULT
1049///   - MEDIAN
1050///   - AVERAGE
1051/// faq:
1052///   - q: "When does MODE.SNGL return #N/A?"
1053///     a: "It returns #N/A when no value repeats in the numeric dataset."
1054/// ```
1055#[derive(Debug)]
1056pub struct ModeSingleFn;
1057/// [formualizer-docgen:schema:start]
1058/// Name: MODE.SNGL
1059/// Type: ModeSingleFn
1060/// Min args: 1
1061/// Max args: variadic
1062/// Variadic: true
1063/// Signature: MODE.SNGL(arg1...: number@range)
1064/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1065/// Caps: PURE, REDUCTION, NUMERIC_ONLY
1066/// [formualizer-docgen:schema:end]
1067impl Function for ModeSingleFn {
1068    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
1069    fn name(&self) -> &'static str {
1070        "MODE.SNGL"
1071    }
1072    fn aliases(&self) -> &'static [&'static str] {
1073        &["MODE"]
1074    }
1075    fn min_args(&self) -> usize {
1076        1
1077    }
1078    fn variadic(&self) -> bool {
1079        true
1080    }
1081    fn arg_schema(&self) -> &'static [ArgSchema] {
1082        &ARG_RANGE_NUM_LENIENT_ONE[..]
1083    }
1084    fn eval<'a, 'b, 'c>(
1085        &self,
1086        args: &'c [ArgumentHandle<'a, 'b>],
1087        _ctx: &dyn FunctionContext<'b>,
1088    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1089        let mut nums = collect_numeric_stats(args)?;
1090        if nums.is_empty() {
1091            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1092                ExcelError::new_na(),
1093            )));
1094        }
1095        nums.sort_by(|a, b| a.partial_cmp(b).unwrap());
1096        let mut best_val = nums[0];
1097        let mut best_cnt = 1usize;
1098        let mut cur_val = nums[0];
1099        let mut cur_cnt = 1usize;
1100        for &v in &nums[1..] {
1101            if (v - cur_val).abs() < 1e-12 {
1102                cur_cnt += 1;
1103            } else {
1104                if cur_cnt > best_cnt {
1105                    best_cnt = cur_cnt;
1106                    best_val = cur_val;
1107                }
1108                cur_val = v;
1109                cur_cnt = 1;
1110            }
1111        }
1112        if cur_cnt > best_cnt {
1113            best_cnt = cur_cnt;
1114            best_val = cur_val;
1115        }
1116        if best_cnt <= 1 {
1117            Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1118                ExcelError::new_na(),
1119            )))
1120        } else {
1121            Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
1122                best_val,
1123            )))
1124        }
1125    }
1126}
1127
1128/// Returns all modal values as a vertical array.
1129///
1130/// Use `MODE.MULT` when a data set can have multiple values with the same highest frequency.
1131///
1132/// # Remarks
1133/// - Returns each tied mode as a separate row in the result array.
1134/// - Returns `#N/A` when every numeric value appears only once.
1135/// - Non-numeric values in referenced ranges are ignored.
1136///
1137/// # Examples
1138///
1139/// ```yaml,sandbox
1140/// title: "Multiple modes from direct values"
1141/// formula: "=MODE.MULT({1,2,2,3,3,4})"
1142/// expected:
1143///   - [2]
1144///   - [3]
1145/// ```
1146///
1147/// ```yaml,sandbox
1148/// title: "Single repeated mode still returns an array"
1149/// grid:
1150///   A1: 5
1151///   A2: 5
1152///   A3: 2
1153///   A4: 1
1154/// formula: "=MODE.MULT(A1:A4)"
1155/// expected:
1156///   - [5]
1157/// ```
1158///
1159/// ```yaml,docs
1160/// related:
1161///   - MODE.SNGL
1162///   - FREQUENCY
1163///   - MEDIAN
1164/// faq:
1165///   - q: "Why can MODE.MULT return an array result?"
1166///     a: "It emits every value tied for highest frequency as separate rows."
1167/// ```
1168#[derive(Debug)]
1169pub struct ModeMultiFn;
1170/// [formualizer-docgen:schema:start]
1171/// Name: MODE.MULT
1172/// Type: ModeMultiFn
1173/// Min args: 1
1174/// Max args: variadic
1175/// Variadic: true
1176/// Signature: MODE.MULT(arg1...: number@range)
1177/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1178/// Caps: PURE, REDUCTION, NUMERIC_ONLY
1179/// [formualizer-docgen:schema:end]
1180impl Function for ModeMultiFn {
1181    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
1182    fn name(&self) -> &'static str {
1183        "MODE.MULT"
1184    }
1185    fn min_args(&self) -> usize {
1186        1
1187    }
1188    fn variadic(&self) -> bool {
1189        true
1190    }
1191    fn arg_schema(&self) -> &'static [ArgSchema] {
1192        &ARG_RANGE_NUM_LENIENT_ONE[..]
1193    }
1194    fn eval<'a, 'b, 'c>(
1195        &self,
1196        args: &'c [ArgumentHandle<'a, 'b>],
1197        _ctx: &dyn FunctionContext<'b>,
1198    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1199        let mut nums = collect_numeric_stats(args)?;
1200        if nums.is_empty() {
1201            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1202                ExcelError::new_na(),
1203            )));
1204        }
1205        nums.sort_by(|a, b| a.partial_cmp(b).unwrap());
1206        let mut runs: Vec<(f64, usize)> = Vec::new();
1207        let mut cur_val = nums[0];
1208        let mut cur_cnt = 1usize;
1209        for &v in &nums[1..] {
1210            if (v - cur_val).abs() < 1e-12 {
1211                cur_cnt += 1;
1212            } else {
1213                runs.push((cur_val, cur_cnt));
1214                cur_val = v;
1215                cur_cnt = 1;
1216            }
1217        }
1218        runs.push((cur_val, cur_cnt));
1219        let max_freq = runs.iter().map(|r| r.1).max().unwrap_or(0);
1220        if max_freq <= 1 {
1221            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1222                ExcelError::new_na(),
1223            )));
1224        }
1225        let rows: Vec<Vec<LiteralValue>> = runs
1226            .into_iter()
1227            .filter(|&(_, c)| c == max_freq)
1228            .map(|(v, _)| vec![LiteralValue::Number(v)])
1229            .collect();
1230        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Array(rows)))
1231    }
1232}
1233
1234/// Returns the k-th percentile of a data set using inclusive interpolation.
1235///
1236/// `PERCENTILE.INC` accepts percentile values from `0` through `1` and interpolates between
1237/// sorted values as needed.
1238///
1239/// # Remarks
1240/// - `k` must be in the inclusive range `[0, 1]`.
1241/// - Returns `#NUM!` for empty numeric input or invalid percentile arguments.
1242/// - Alias `PERCENTILE` is supported.
1243///
1244/// # Examples
1245///
1246/// ```yaml,sandbox
1247/// title: "Inclusive 25th percentile from direct values"
1248/// formula: "=PERCENTILE.INC({1,2,3,4,5},0.25)"
1249/// expected: 2
1250/// ```
1251///
1252/// ```yaml,sandbox
1253/// title: "Inclusive median-style interpolation from a range"
1254/// grid:
1255///   A1: 10
1256///   A2: 20
1257///   A3: 30
1258///   A4: 40
1259/// formula: "=PERCENTILE.INC(A1:A4,0.5)"
1260/// expected: 25
1261/// ```
1262///
1263/// ```yaml,docs
1264/// related:
1265///   - PERCENTILE.EXC
1266///   - QUARTILE.INC
1267///   - PERCENTRANK.INC
1268/// faq:
1269///   - q: "What k range is valid for PERCENTILE.INC?"
1270///     a: "k must be between 0 and 1 inclusive; outside that range returns #NUM!."
1271/// ```
1272#[derive(Debug)]
1273pub struct PercentileInc; // inclusive
1274/// [formualizer-docgen:schema:start]
1275/// Name: PERCENTILE.INC
1276/// Type: PercentileInc
1277/// Min args: 2
1278/// Max args: variadic
1279/// Variadic: true
1280/// Signature: PERCENTILE.INC(arg1...: number@range)
1281/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1282/// Caps: PURE, NUMERIC_ONLY
1283/// [formualizer-docgen:schema:end]
1284impl Function for PercentileInc {
1285    func_caps!(PURE, NUMERIC_ONLY);
1286    fn name(&self) -> &'static str {
1287        "PERCENTILE.INC"
1288    }
1289    fn aliases(&self) -> &'static [&'static str] {
1290        &["PERCENTILE"]
1291    }
1292    fn min_args(&self) -> usize {
1293        2
1294    }
1295    fn variadic(&self) -> bool {
1296        true
1297    }
1298    fn arg_schema(&self) -> &'static [ArgSchema] {
1299        &ARG_RANGE_NUM_LENIENT_ONE[..]
1300    }
1301    fn eval<'a, 'b, 'c>(
1302        &self,
1303        args: &'c [ArgumentHandle<'a, 'b>],
1304        _ctx: &dyn FunctionContext<'b>,
1305    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1306        if args.len() < 2 {
1307            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1308                ExcelError::new_num(),
1309            )));
1310        }
1311        let pv = scalar_like_value(args.last().unwrap())?;
1312        let p = match coerce_num(&pv) {
1313            Ok(n) => n,
1314            Err(_) => {
1315                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1316                    ExcelError::new_num(),
1317                )));
1318            }
1319        };
1320        let mut nums = collect_numeric_stats(&args[..args.len() - 1])?;
1321        if nums.is_empty() {
1322            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1323                ExcelError::new_num(),
1324            )));
1325        }
1326        nums.sort_by(|a, b| a.partial_cmp(b).unwrap());
1327        match percentile_inc(&nums, p) {
1328            Ok(v) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(v))),
1329            Err(e) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
1330        }
1331    }
1332}
1333
1334/// Returns the k-th percentile of a data set using exclusive interpolation.
1335///
1336/// `PERCENTILE.EXC` uses the `n+1` rank basis and excludes the exact endpoints `0` and `1`.
1337///
1338/// # Remarks
1339/// - `k` must satisfy `0 < k < 1`.
1340/// - Returns `#NUM!` when the percentile falls outside the valid rank range for the data size.
1341/// - Returns `#NUM!` for empty numeric input.
1342///
1343/// # Examples
1344///
1345/// ```yaml,sandbox
1346/// title: "Exclusive 25th percentile from direct values"
1347/// formula: "=PERCENTILE.EXC({1,2,3,4,5},0.25)"
1348/// expected: 1.5
1349/// ```
1350///
1351/// ```yaml,sandbox
1352/// title: "Exclusive percentile from a range"
1353/// grid:
1354///   A1: 10
1355///   A2: 20
1356///   A3: 30
1357///   A4: 40
1358///   A5: 50
1359/// formula: "=PERCENTILE.EXC(A1:A5,0.6)"
1360/// expected: 36
1361/// ```
1362///
1363/// ```yaml,docs
1364/// related:
1365///   - PERCENTILE.INC
1366///   - QUARTILE.EXC
1367///   - PERCENTRANK.EXC
1368/// faq:
1369///   - q: "Why does PERCENTILE.EXC reject k=0 or k=1?"
1370///     a: "Exclusive percentile uses the n+1 basis and requires strictly 0 < k < 1."
1371/// ```
1372#[derive(Debug)]
1373pub struct PercentileExc; // exclusive
1374/// [formualizer-docgen:schema:start]
1375/// Name: PERCENTILE.EXC
1376/// Type: PercentileExc
1377/// Min args: 2
1378/// Max args: variadic
1379/// Variadic: true
1380/// Signature: PERCENTILE.EXC(arg1...: number@range)
1381/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1382/// Caps: PURE, NUMERIC_ONLY
1383/// [formualizer-docgen:schema:end]
1384impl Function for PercentileExc {
1385    func_caps!(PURE, NUMERIC_ONLY);
1386    fn name(&self) -> &'static str {
1387        "PERCENTILE.EXC"
1388    }
1389    fn min_args(&self) -> usize {
1390        2
1391    }
1392    fn variadic(&self) -> bool {
1393        true
1394    }
1395    fn arg_schema(&self) -> &'static [ArgSchema] {
1396        &ARG_RANGE_NUM_LENIENT_ONE[..]
1397    }
1398    fn eval<'a, 'b, 'c>(
1399        &self,
1400        args: &'c [ArgumentHandle<'a, 'b>],
1401        _ctx: &dyn FunctionContext<'b>,
1402    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1403        if args.len() < 2 {
1404            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1405                ExcelError::new_num(),
1406            )));
1407        }
1408        let pv = scalar_like_value(args.last().unwrap())?;
1409        let p = match coerce_num(&pv) {
1410            Ok(n) => n,
1411            Err(_) => {
1412                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1413                    ExcelError::new_num(),
1414                )));
1415            }
1416        };
1417        let mut nums = collect_numeric_stats(&args[..args.len() - 1])?;
1418        if nums.is_empty() {
1419            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1420                ExcelError::new_num(),
1421            )));
1422        }
1423        nums.sort_by(|a, b| a.partial_cmp(b).unwrap());
1424        match percentile_exc(&nums, p) {
1425            Ok(v) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(v))),
1426            Err(e) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
1427        }
1428    }
1429}
1430
1431/// Returns an inclusive quartile value for a data set.
1432///
1433/// `QUARTILE.INC` maps quartile index `0..4` onto minimum, quartiles, median, and maximum.
1434///
1435/// # Remarks
1436/// - Valid quartile index values are `0`, `1`, `2`, `3`, and `4`.
1437/// - Uses inclusive percentile logic for quartiles `1` through `3`.
1438/// - Returns `#NUM!` for invalid quartile index values or empty numeric input.
1439/// - Alias `QUARTILE` is supported.
1440///
1441/// # Examples
1442///
1443/// ```yaml,sandbox
1444/// title: "First quartile from direct values"
1445/// formula: "=QUARTILE.INC({1,2,3,4,5},1)"
1446/// expected: 2
1447/// ```
1448///
1449/// ```yaml,sandbox
1450/// title: "Third quartile from a range"
1451/// grid:
1452///   A1: 10
1453///   A2: 20
1454///   A3: 30
1455///   A4: 40
1456/// formula: "=QUARTILE.INC(A1:A4,3)"
1457/// expected: 32.5
1458/// ```
1459///
1460/// ```yaml,docs
1461/// related:
1462///   - QUARTILE.EXC
1463///   - PERCENTILE.INC
1464///   - MEDIAN
1465/// faq:
1466///   - q: "Which quartile numbers are valid for QUARTILE.INC?"
1467///     a: "Only 0 through 4 are valid; other quartile indices return #NUM!."
1468/// ```
1469#[derive(Debug)]
1470pub struct QuartileInc; // quartile inclusive
1471/// [formualizer-docgen:schema:start]
1472/// Name: QUARTILE.INC
1473/// Type: QuartileInc
1474/// Min args: 2
1475/// Max args: variadic
1476/// Variadic: true
1477/// Signature: QUARTILE.INC(arg1...: number@range)
1478/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1479/// Caps: PURE, NUMERIC_ONLY
1480/// [formualizer-docgen:schema:end]
1481impl Function for QuartileInc {
1482    func_caps!(PURE, NUMERIC_ONLY);
1483    fn name(&self) -> &'static str {
1484        "QUARTILE.INC"
1485    }
1486    fn aliases(&self) -> &'static [&'static str] {
1487        &["QUARTILE"]
1488    }
1489    fn min_args(&self) -> usize {
1490        2
1491    }
1492    fn variadic(&self) -> bool {
1493        true
1494    }
1495    fn arg_schema(&self) -> &'static [ArgSchema] {
1496        &ARG_RANGE_NUM_LENIENT_ONE[..]
1497    }
1498    fn eval<'a, 'b, 'c>(
1499        &self,
1500        args: &'c [ArgumentHandle<'a, 'b>],
1501        _ctx: &dyn FunctionContext<'b>,
1502    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1503        if args.len() < 2 {
1504            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1505                ExcelError::new_num(),
1506            )));
1507        }
1508        let qv = scalar_like_value(args.last().unwrap())?;
1509        let q = match coerce_num(&qv) {
1510            Ok(n) => n,
1511            Err(_) => {
1512                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1513                    ExcelError::new_num(),
1514                )));
1515            }
1516        };
1517        let q_i = q as i64;
1518        if !(0..=4).contains(&q_i) {
1519            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1520                ExcelError::new_num(),
1521            )));
1522        }
1523        let mut nums = collect_numeric_stats(&args[..args.len() - 1])?;
1524        if nums.is_empty() {
1525            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1526                ExcelError::new_num(),
1527            )));
1528        }
1529        nums.sort_by(|a, b| a.partial_cmp(b).unwrap());
1530        let p = match q_i {
1531            0 => {
1532                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
1533                    nums[0],
1534                )));
1535            }
1536            4 => {
1537                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
1538                    nums[nums.len() - 1],
1539                )));
1540            }
1541            1 => 0.25,
1542            2 => 0.5,
1543            3 => 0.75,
1544            _ => {
1545                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1546                    ExcelError::new_num(),
1547                )));
1548            }
1549        };
1550        match percentile_inc(&nums, p) {
1551            Ok(v) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(v))),
1552            Err(e) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
1553        }
1554    }
1555}
1556
1557/// Returns an exclusive quartile value for a data set.
1558///
1559/// `QUARTILE.EXC` applies exclusive percentile interpolation and supports quartiles `1` through
1560/// `3`.
1561///
1562/// # Remarks
1563/// - Valid quartile index values are `1`, `2`, and `3`.
1564/// - Returns `#NUM!` for invalid quartile index values.
1565/// - Returns `#NUM!` when the input is too small for exclusive quartile interpolation.
1566///
1567/// # Examples
1568///
1569/// ```yaml,sandbox
1570/// title: "First exclusive quartile from direct values"
1571/// formula: "=QUARTILE.EXC({1,2,3,4,5,6,7,8},1)"
1572/// expected: 2.25
1573/// ```
1574///
1575/// ```yaml,sandbox
1576/// title: "Third exclusive quartile from a range"
1577/// grid:
1578///   A1: 10
1579///   A2: 20
1580///   A3: 30
1581///   A4: 40
1582///   A5: 50
1583///   A6: 60
1584///   A7: 70
1585///   A8: 80
1586/// formula: "=QUARTILE.EXC(A1:A8,3)"
1587/// expected: 67.5
1588/// ```
1589///
1590/// ```yaml,docs
1591/// related:
1592///   - QUARTILE.INC
1593///   - PERCENTILE.EXC
1594///   - MEDIAN
1595/// faq:
1596///   - q: "Why can QUARTILE.EXC return #NUM! on small datasets?"
1597///     a: "Exclusive quartiles need enough data for valid interior rank interpolation."
1598/// ```
1599#[derive(Debug)]
1600pub struct QuartileExc; // quartile exclusive
1601/// [formualizer-docgen:schema:start]
1602/// Name: QUARTILE.EXC
1603/// Type: QuartileExc
1604/// Min args: 2
1605/// Max args: variadic
1606/// Variadic: true
1607/// Signature: QUARTILE.EXC(arg1...: number@range)
1608/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1609/// Caps: PURE, NUMERIC_ONLY
1610/// [formualizer-docgen:schema:end]
1611impl Function for QuartileExc {
1612    func_caps!(PURE, NUMERIC_ONLY);
1613    fn name(&self) -> &'static str {
1614        "QUARTILE.EXC"
1615    }
1616    fn min_args(&self) -> usize {
1617        2
1618    }
1619    fn variadic(&self) -> bool {
1620        true
1621    }
1622    fn arg_schema(&self) -> &'static [ArgSchema] {
1623        &ARG_RANGE_NUM_LENIENT_ONE[..]
1624    }
1625    fn eval<'a, 'b, 'c>(
1626        &self,
1627        args: &'c [ArgumentHandle<'a, 'b>],
1628        _ctx: &dyn FunctionContext<'b>,
1629    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1630        if args.len() < 2 {
1631            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1632                ExcelError::new_num(),
1633            )));
1634        }
1635        let qv = scalar_like_value(args.last().unwrap())?;
1636        let q = match coerce_num(&qv) {
1637            Ok(n) => n,
1638            Err(_) => {
1639                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1640                    ExcelError::new_num(),
1641                )));
1642            }
1643        };
1644        let q_i = q as i64;
1645        if !(1..=3).contains(&q_i) {
1646            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1647                ExcelError::new_num(),
1648            )));
1649        }
1650        let mut nums = collect_numeric_stats(&args[..args.len() - 1])?;
1651        if nums.len() < 2 {
1652            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1653                ExcelError::new_num(),
1654            )));
1655        }
1656        nums.sort_by(|a, b| a.partial_cmp(b).unwrap());
1657        let p = match q_i {
1658            1 => 0.25,
1659            2 => 0.5,
1660            3 => 0.75,
1661            _ => {
1662                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1663                    ExcelError::new_num(),
1664                )));
1665            }
1666        };
1667        match percentile_exc(&nums, p) {
1668            Ok(v) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(v))),
1669            Err(e) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
1670        }
1671    }
1672}
1673
1674/// Multiplies all numeric arguments and returns their product.
1675///
1676/// `PRODUCT` is useful for chained growth factors, scaling ratios, and compound multipliers.
1677///
1678/// # Remarks
1679/// - Non-numeric values in referenced ranges are ignored.
1680/// - Returns `0` when no numeric values are found.
1681/// - Direct scalar arguments still attempt numeric coercion.
1682///
1683/// # Examples
1684///
1685/// ```yaml,sandbox
1686/// title: "Product of scalar values"
1687/// formula: "=PRODUCT(2,3,4)"
1688/// expected: 24
1689/// ```
1690///
1691/// ```yaml,sandbox
1692/// title: "Product from a range"
1693/// grid:
1694///   A1: 1
1695///   A2: 5
1696///   A3: 10
1697/// formula: "=PRODUCT(A1:A3)"
1698/// expected: 50
1699/// ```
1700///
1701/// ```yaml,docs
1702/// related:
1703///   - SUM
1704///   - GEOMEAN
1705///   - SUMPRODUCT
1706/// faq:
1707///   - q: "Why does PRODUCT return 0 when no numeric inputs are found?"
1708///     a: "This implementation returns 0 for an empty numeric set after filtering."
1709/// ```
1710#[derive(Debug)]
1711pub struct ProductFn;
1712/// [formualizer-docgen:schema:start]
1713/// Name: PRODUCT
1714/// Type: ProductFn
1715/// Min args: 1
1716/// Max args: variadic
1717/// Variadic: true
1718/// Signature: PRODUCT(arg1...: number@range)
1719/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1720/// Caps: PURE, REDUCTION, NUMERIC_ONLY
1721/// [formualizer-docgen:schema:end]
1722impl Function for ProductFn {
1723    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
1724    fn name(&self) -> &'static str {
1725        "PRODUCT"
1726    }
1727    fn min_args(&self) -> usize {
1728        1
1729    }
1730    fn variadic(&self) -> bool {
1731        true
1732    }
1733    fn dependency_contract(&self, arity: usize) -> Option<FunctionDependencyContract> {
1734        FunctionDependencyContract::static_reduction(arity, self.min_args())
1735    }
1736    fn arg_schema(&self) -> &'static [ArgSchema] {
1737        &ARG_RANGE_NUM_LENIENT_ONE[..]
1738    }
1739    fn eval<'a, 'b, 'c>(
1740        &self,
1741        args: &'c [ArgumentHandle<'a, 'b>],
1742        _ctx: &dyn FunctionContext<'b>,
1743    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1744        let nums = collect_numeric_stats(args)?;
1745        if nums.is_empty() {
1746            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0)));
1747        }
1748        let result = nums.iter().product::<f64>();
1749        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
1750            result,
1751        )))
1752    }
1753}
1754
1755/// Returns the geometric mean of positive numeric values.
1756///
1757/// `GEOMEAN` is commonly used for rates of change and multiplicative growth comparisons.
1758///
1759/// # Remarks
1760/// - All numeric inputs must be strictly greater than `0`.
1761/// - Returns `#NUM!` if any value is `<= 0`, or if no numeric values are provided.
1762/// - Non-numeric values in referenced ranges are ignored.
1763///
1764/// # Examples
1765///
1766/// ```yaml,sandbox
1767/// title: "Geometric mean from scalar values"
1768/// formula: "=GEOMEAN(4,16)"
1769/// expected: 8
1770/// ```
1771///
1772/// ```yaml,sandbox
1773/// title: "Geometric mean from a range"
1774/// grid:
1775///   A1: 1
1776///   A2: 3
1777///   A3: 9
1778/// formula: "=GEOMEAN(A1:A3)"
1779/// expected: 3
1780/// ```
1781///
1782/// ```yaml,docs
1783/// related:
1784///   - HARMEAN
1785///   - PRODUCT
1786///   - AVERAGE
1787/// faq:
1788///   - q: "When does GEOMEAN return #NUM!?"
1789///     a: "GEOMEAN returns #NUM! if any numeric value is <= 0 or if no numeric values exist."
1790/// ```
1791#[derive(Debug)]
1792pub struct GeomeanFn;
1793/// [formualizer-docgen:schema:start]
1794/// Name: GEOMEAN
1795/// Type: GeomeanFn
1796/// Min args: 1
1797/// Max args: variadic
1798/// Variadic: true
1799/// Signature: GEOMEAN(arg1...: number@range)
1800/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1801/// Caps: PURE, REDUCTION, NUMERIC_ONLY
1802/// [formualizer-docgen:schema:end]
1803impl Function for GeomeanFn {
1804    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
1805    fn name(&self) -> &'static str {
1806        "GEOMEAN"
1807    }
1808    fn min_args(&self) -> usize {
1809        1
1810    }
1811    fn variadic(&self) -> bool {
1812        true
1813    }
1814    fn arg_schema(&self) -> &'static [ArgSchema] {
1815        &ARG_RANGE_NUM_LENIENT_ONE[..]
1816    }
1817    fn eval<'a, 'b, 'c>(
1818        &self,
1819        args: &'c [ArgumentHandle<'a, 'b>],
1820        _ctx: &dyn FunctionContext<'b>,
1821    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1822        let nums = collect_numeric_stats(args)?;
1823        if nums.is_empty() {
1824            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1825                ExcelError::new_num(),
1826            )));
1827        }
1828        // All values must be positive
1829        if nums.iter().any(|&n| n <= 0.0) {
1830            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1831                ExcelError::new_num(),
1832            )));
1833        }
1834        // Geometric mean = (x1 * x2 * ... * xn)^(1/n)
1835        // Use log to avoid overflow: exp(mean(ln(x)))
1836        let log_sum: f64 = nums.iter().map(|x| x.ln()).sum();
1837        let result = (log_sum / nums.len() as f64).exp();
1838        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
1839            result,
1840        )))
1841    }
1842}
1843
1844/// Returns the harmonic mean of positive numeric values.
1845///
1846/// `HARMEAN` emphasizes smaller values and is useful for averaging rates and ratios.
1847///
1848/// # Remarks
1849/// - All numeric inputs must be strictly greater than `0`.
1850/// - Returns `#NUM!` if any value is `<= 0`, or if no numeric values are provided.
1851/// - Non-numeric values in referenced ranges are ignored.
1852///
1853/// # Examples
1854///
1855/// ```yaml,sandbox
1856/// title: "Harmonic mean from scalar values"
1857/// formula: "=HARMEAN(1,2,4)"
1858/// expected: 1.7142857142857142
1859/// ```
1860///
1861/// ```yaml,sandbox
1862/// title: "Harmonic mean from a range"
1863/// grid:
1864///   A1: 2
1865///   A2: 3
1866///   A3: 6
1867/// formula: "=HARMEAN(A1:A3)"
1868/// expected: 3
1869/// ```
1870///
1871/// ```yaml,docs
1872/// related:
1873///   - GEOMEAN
1874///   - AVERAGE
1875///   - PRODUCT
1876/// faq:
1877///   - q: "Why does HARMEAN fail on zeros?"
1878///     a: "Harmonic mean uses reciprocals, so inputs must be strictly positive."
1879/// ```
1880#[derive(Debug)]
1881pub struct HarmeanFn;
1882/// [formualizer-docgen:schema:start]
1883/// Name: HARMEAN
1884/// Type: HarmeanFn
1885/// Min args: 1
1886/// Max args: variadic
1887/// Variadic: true
1888/// Signature: HARMEAN(arg1...: number@range)
1889/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1890/// Caps: PURE, REDUCTION, NUMERIC_ONLY
1891/// [formualizer-docgen:schema:end]
1892impl Function for HarmeanFn {
1893    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
1894    fn name(&self) -> &'static str {
1895        "HARMEAN"
1896    }
1897    fn min_args(&self) -> usize {
1898        1
1899    }
1900    fn variadic(&self) -> bool {
1901        true
1902    }
1903    fn arg_schema(&self) -> &'static [ArgSchema] {
1904        &ARG_RANGE_NUM_LENIENT_ONE[..]
1905    }
1906    fn eval<'a, 'b, 'c>(
1907        &self,
1908        args: &'c [ArgumentHandle<'a, 'b>],
1909        _ctx: &dyn FunctionContext<'b>,
1910    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1911        let nums = collect_numeric_stats(args)?;
1912        if nums.is_empty() {
1913            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1914                ExcelError::new_num(),
1915            )));
1916        }
1917        // All values must be positive
1918        if nums.iter().any(|&n| n <= 0.0) {
1919            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1920                ExcelError::new_num(),
1921            )));
1922        }
1923        // Harmonic mean = n / sum(1/x)
1924        let sum_reciprocals: f64 = nums.iter().map(|x| 1.0 / x).sum();
1925        let result = nums.len() as f64 / sum_reciprocals;
1926        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
1927            result,
1928        )))
1929    }
1930}
1931
1932/// Returns the average of absolute deviations from the mean.
1933///
1934/// `AVEDEV` provides a robust spread measure that is less sensitive to outliers than squared-error
1935/// metrics.
1936///
1937/// # Remarks
1938/// - Returns `#NUM!` when no numeric values are available.
1939/// - Non-numeric values in referenced ranges are ignored.
1940/// - Uses the arithmetic mean as the center point.
1941///
1942/// # Examples
1943///
1944/// ```yaml,sandbox
1945/// title: "Average absolute deviation from scalar values"
1946/// formula: "=AVEDEV(2,4,6)"
1947/// expected: 1.3333333333333333
1948/// ```
1949///
1950/// ```yaml,sandbox
1951/// title: "Average absolute deviation from a range"
1952/// grid:
1953///   A1: 1
1954///   A2: 1
1955///   A3: 3
1956///   A4: 5
1957/// formula: "=AVEDEV(A1:A4)"
1958/// expected: 1.5
1959/// ```
1960///
1961/// ```yaml,docs
1962/// related:
1963///   - DEVSQ
1964///   - STDEV.S
1965///   - VAR.S
1966/// faq:
1967///   - q: "What center does AVEDEV use for deviations?"
1968///     a: "It computes absolute deviations around the arithmetic mean of included values."
1969/// ```
1970#[derive(Debug)]
1971pub struct AvedevFn;
1972/// [formualizer-docgen:schema:start]
1973/// Name: AVEDEV
1974/// Type: AvedevFn
1975/// Min args: 1
1976/// Max args: variadic
1977/// Variadic: true
1978/// Signature: AVEDEV(arg1...: number@range)
1979/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1980/// Caps: PURE, REDUCTION, NUMERIC_ONLY
1981/// [formualizer-docgen:schema:end]
1982impl Function for AvedevFn {
1983    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
1984    fn name(&self) -> &'static str {
1985        "AVEDEV"
1986    }
1987    fn min_args(&self) -> usize {
1988        1
1989    }
1990    fn variadic(&self) -> bool {
1991        true
1992    }
1993    fn arg_schema(&self) -> &'static [ArgSchema] {
1994        &ARG_RANGE_NUM_LENIENT_ONE[..]
1995    }
1996    fn eval<'a, 'b, 'c>(
1997        &self,
1998        args: &'c [ArgumentHandle<'a, 'b>],
1999        _ctx: &dyn FunctionContext<'b>,
2000    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2001        let nums = collect_numeric_stats(args)?;
2002        if nums.is_empty() {
2003            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2004                ExcelError::new_num(),
2005            )));
2006        }
2007        let mean = nums.iter().sum::<f64>() / nums.len() as f64;
2008        let avedev = nums.iter().map(|x| (x - mean).abs()).sum::<f64>() / nums.len() as f64;
2009        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
2010            avedev,
2011        )))
2012    }
2013}
2014
2015/// Returns the sum of squared deviations from the mean.
2016///
2017/// `DEVSQ` is useful for variance-related calculations and diagnostics of spread.
2018///
2019/// # Remarks
2020/// - Returns `#NUM!` when no numeric values are available.
2021/// - Non-numeric values in referenced ranges are ignored.
2022/// - Uses the arithmetic mean of included values.
2023///
2024/// # Examples
2025///
2026/// ```yaml,sandbox
2027/// title: "Sum of squared deviations from scalar values"
2028/// formula: "=DEVSQ(2,4,6)"
2029/// expected: 8
2030/// ```
2031///
2032/// ```yaml,sandbox
2033/// title: "Sum of squared deviations from a range"
2034/// grid:
2035///   A1: 1
2036///   A2: 2
2037///   A3: 3
2038///   A4: 4
2039/// formula: "=DEVSQ(A1:A4)"
2040/// expected: 5
2041/// ```
2042#[derive(Debug)]
2043pub struct DevsqFn;
2044
2045/* ─────────────────────────── MAXIFS / MINIFS ──────────────────────────── */
2046
2047use super::utils::{ARG_ANY_ONE, criteria_match};
2048
2049/// Returns the maximum numeric value in a range that meets all criteria.
2050///
2051/// `MAXIFS` applies one or more `(criteria_range, criteria)` pairs and returns the largest
2052/// matching numeric value.
2053///
2054/// # Remarks
2055/// - Arguments must be `target_range` plus one or more criteria pairs.
2056/// - Criteria are combined with logical AND.
2057/// - Returns `0` when no cells satisfy all criteria.
2058/// - Non-numeric cells in `target_range` are ignored.
2059///
2060/// # Examples
2061///
2062/// ```yaml,sandbox
2063/// title: "Maximum value for one condition"
2064/// grid:
2065///   A1: 10
2066///   A2: 20
2067///   A3: 15
2068///   B1: "East"
2069///   B2: "West"
2070///   B3: "East"
2071/// formula: "=MAXIFS(A1:A3,B1:B3,\"East\")"
2072/// expected: 15
2073/// ```
2074///
2075/// ```yaml,sandbox
2076/// title: "Maximum value with two criteria"
2077/// grid:
2078///   A1: 100
2079///   A2: 80
2080///   A3: 90
2081///   A4: 70
2082///   B1: "A"
2083///   B2: "A"
2084///   B3: "B"
2085///   B4: "B"
2086///   C1: "Q1"
2087///   C2: "Q2"
2088///   C3: "Q1"
2089///   C4: "Q1"
2090/// formula: "=MAXIFS(A1:A4,B1:B4,\"B\",C1:C4,\"Q1\")"
2091/// expected: 90
2092/// ```
2093///
2094/// ```yaml,docs
2095/// related:
2096///   - MINIFS
2097///   - MAX
2098///   - SUMIFS
2099/// faq:
2100///   - q: "What does MAXIFS return when no rows match all criteria?"
2101///     a: "It returns 0 when no numeric target cells satisfy every criterion."
2102/// ```
2103#[derive(Debug)]
2104pub struct MaxIfsFn;
2105/// [formualizer-docgen:schema:start]
2106/// Name: MAXIFS
2107/// Type: MaxIfsFn
2108/// Min args: 3
2109/// Max args: variadic
2110/// Variadic: true
2111/// Signature: MAXIFS(arg1...: any@scalar)
2112/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
2113/// Caps: PURE, REDUCTION
2114/// [formualizer-docgen:schema:end]
2115impl Function for MaxIfsFn {
2116    func_caps!(PURE, REDUCTION);
2117    fn name(&self) -> &'static str {
2118        "MAXIFS"
2119    }
2120    fn min_args(&self) -> usize {
2121        3
2122    }
2123    fn variadic(&self) -> bool {
2124        true
2125    }
2126    fn arg_schema(&self) -> &'static [ArgSchema] {
2127        &ARG_ANY_ONE[..]
2128    }
2129    fn eval<'a, 'b, 'c>(
2130        &self,
2131        args: &'c [ArgumentHandle<'a, 'b>],
2132        _ctx: &dyn FunctionContext<'b>,
2133    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2134        eval_maxminifs(args, true)
2135    }
2136}
2137
2138/// Returns the minimum numeric value in a range that meets all criteria.
2139///
2140/// `MINIFS` evaluates one or more `(criteria_range, criteria)` pairs and returns the smallest
2141/// matching numeric value.
2142///
2143/// # Remarks
2144/// - Arguments must be `target_range` plus one or more criteria pairs.
2145/// - Criteria are combined with logical AND.
2146/// - Returns `0` when no cells satisfy all criteria.
2147/// - Non-numeric cells in `target_range` are ignored.
2148///
2149/// # Examples
2150///
2151/// ```yaml,sandbox
2152/// title: "Minimum value for one condition"
2153/// grid:
2154///   A1: 10
2155///   A2: 20
2156///   A3: 15
2157///   B1: "East"
2158///   B2: "West"
2159///   B3: "East"
2160/// formula: "=MINIFS(A1:A3,B1:B3,\"East\")"
2161/// expected: 10
2162/// ```
2163///
2164/// ```yaml,sandbox
2165/// title: "Minimum value with two criteria"
2166/// grid:
2167///   A1: 100
2168///   A2: 80
2169///   A3: 90
2170///   A4: 70
2171///   B1: "A"
2172///   B2: "A"
2173///   B3: "B"
2174///   B4: "B"
2175///   C1: "Q1"
2176///   C2: "Q2"
2177///   C3: "Q1"
2178///   C4: "Q1"
2179/// formula: "=MINIFS(A1:A4,B1:B4,\"B\",C1:C4,\"Q1\")"
2180/// expected: 70
2181/// ```
2182///
2183/// ```yaml,docs
2184/// related:
2185///   - MAXIFS
2186///   - MIN
2187///   - SUMIFS
2188/// faq:
2189///   - q: "How does MINIFS treat non-numeric target cells?"
2190///     a: "Non-numeric target cells are ignored; only numeric matches are eligible."
2191/// ```
2192#[derive(Debug)]
2193pub struct MinIfsFn;
2194/// [formualizer-docgen:schema:start]
2195/// Name: MINIFS
2196/// Type: MinIfsFn
2197/// Min args: 3
2198/// Max args: variadic
2199/// Variadic: true
2200/// Signature: MINIFS(arg1...: any@scalar)
2201/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
2202/// Caps: PURE, REDUCTION
2203/// [formualizer-docgen:schema:end]
2204impl Function for MinIfsFn {
2205    func_caps!(PURE, REDUCTION);
2206    fn name(&self) -> &'static str {
2207        "MINIFS"
2208    }
2209    fn min_args(&self) -> usize {
2210        3
2211    }
2212    fn variadic(&self) -> bool {
2213        true
2214    }
2215    fn arg_schema(&self) -> &'static [ArgSchema] {
2216        &ARG_ANY_ONE[..]
2217    }
2218    fn eval<'a, 'b, 'c>(
2219        &self,
2220        args: &'c [ArgumentHandle<'a, 'b>],
2221        _ctx: &dyn FunctionContext<'b>,
2222    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2223        eval_maxminifs(args, false)
2224    }
2225}
2226
2227/// Shared implementation for MAXIFS and MINIFS
2228fn eval_maxminifs<'a, 'b>(
2229    args: &[ArgumentHandle<'a, 'b>],
2230    is_max: bool,
2231) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2232    // Validate argument count: must be target_range + N pairs
2233    if args.len() < 3 || !(args.len() - 1).is_multiple_of(2) {
2234        return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2235            ExcelError::new_value().with_message(format!(
2236                "Function expects 1 target_range followed by N pairs (criteria_range, criteria); got {} args",
2237                args.len()
2238            )),
2239        )));
2240    }
2241
2242    // Get target range
2243    let target_view = match args[0].range_view() {
2244        Ok(v) => v,
2245        Err(_) => {
2246            // Single value case - if criteria match, return that value
2247            let target_val = args[0].value()?.into_literal();
2248            if let LiteralValue::Error(e) = target_val {
2249                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
2250            }
2251            // Check all criteria against empty/scalar
2252            let mut all_match = true;
2253            for i in (1..args.len()).step_by(2) {
2254                let crit_val = args[i].value()?.into_literal();
2255                let pred = crate::args::parse_criteria(&args[i + 1].value()?.into_literal())?;
2256                if !criteria_match(&pred, &crit_val) {
2257                    all_match = false;
2258                    break;
2259                }
2260            }
2261            if all_match {
2262                return match coerce_num(&target_val) {
2263                    Ok(n) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(n))),
2264                    Err(_) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0))),
2265                };
2266            }
2267            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0)));
2268        }
2269    };
2270
2271    let (rows, cols) = target_view.dims();
2272
2273    // Parse all criteria
2274    let mut criteria_ranges = Vec::new();
2275    let mut predicates = Vec::new();
2276    for i in (1..args.len()).step_by(2) {
2277        let crit_view = args[i].range_view().ok();
2278        let pred = crate::args::parse_criteria(&args[i + 1].value()?.into_literal())?;
2279        criteria_ranges.push(crit_view);
2280        predicates.push(pred);
2281    }
2282
2283    // Iterate through all cells and find max/min where all criteria match
2284    let mut result: Option<f64> = None;
2285
2286    for r in 0..rows {
2287        for c in 0..cols {
2288            // Check all criteria
2289            let mut all_match = true;
2290            for (crit_idx, pred) in predicates.iter().enumerate() {
2291                let crit_val = match &criteria_ranges[crit_idx] {
2292                    Some(view) => {
2293                        let (cr, cc) = view.dims();
2294                        if r < cr && c < cc {
2295                            view.get_cell(r, c)
2296                        } else {
2297                            LiteralValue::Empty
2298                        }
2299                    }
2300                    None => LiteralValue::Empty,
2301                };
2302                if !criteria_match(pred, &crit_val) {
2303                    all_match = false;
2304                    break;
2305                }
2306            }
2307
2308            if all_match {
2309                let target_val = target_view.get_cell(r, c);
2310                match target_val {
2311                    LiteralValue::Error(e) => {
2312                        return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
2313                    }
2314                    LiteralValue::Number(n) => {
2315                        result = Some(match result {
2316                            None => n,
2317                            Some(curr) => {
2318                                if is_max {
2319                                    curr.max(n)
2320                                } else {
2321                                    curr.min(n)
2322                                }
2323                            }
2324                        });
2325                    }
2326                    LiteralValue::Int(i) => {
2327                        let n = i as f64;
2328                        result = Some(match result {
2329                            None => n,
2330                            Some(curr) => {
2331                                if is_max {
2332                                    curr.max(n)
2333                                } else {
2334                                    curr.min(n)
2335                                }
2336                            }
2337                        });
2338                    }
2339                    _ => {} // Skip non-numeric
2340                }
2341            }
2342        }
2343    }
2344
2345    // Excel returns 0 if no matches found
2346    Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
2347        result.unwrap_or(0.0),
2348    )))
2349}
2350
2351/* ─────────────────────────── TRIMMEAN ──────────────────────────── */
2352
2353/// Returns the mean after trimming a percentage of values from both tails.
2354///
2355/// `TRIMMEAN` sorts numeric data, removes an equal count from low and high ends, then averages the
2356/// remaining interior values.
2357///
2358/// # Remarks
2359/// - `percent` must satisfy `0 <= percent < 1`.
2360/// - The trimmed count per side is `floor(n * percent / 2)`.
2361/// - Returns `#NUM!` for invalid percent values or when no numeric values are available.
2362///
2363/// # Examples
2364///
2365/// ```yaml,sandbox
2366/// title: "Trimmed mean from direct values"
2367/// formula: "=TRIMMEAN({1,2,3,4,5,6},0.3333333333333333)"
2368/// expected: 3.5
2369/// ```
2370///
2371/// ```yaml,sandbox
2372/// title: "Trimmed mean from a range"
2373/// grid:
2374///   A1: 10
2375///   A2: 12
2376///   A3: 13
2377///   A4: 20
2378///   A5: 21
2379///   A6: 30
2380/// formula: "=TRIMMEAN(A1:A6,0.4)"
2381/// expected: 16.5
2382/// ```
2383#[derive(Debug)]
2384pub struct TrimmeanFn;
2385/// [formualizer-docgen:schema:start]
2386/// Name: TRIMMEAN
2387/// Type: TrimmeanFn
2388/// Min args: 2
2389/// Max args: 1
2390/// Variadic: false
2391/// Signature: TRIMMEAN(arg1: number@range)
2392/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
2393/// Caps: PURE, REDUCTION, NUMERIC_ONLY
2394/// [formualizer-docgen:schema:end]
2395impl Function for TrimmeanFn {
2396    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
2397    fn name(&self) -> &'static str {
2398        "TRIMMEAN"
2399    }
2400    fn min_args(&self) -> usize {
2401        2
2402    }
2403    fn arg_schema(&self) -> &'static [ArgSchema] {
2404        &ARG_RANGE_NUM_LENIENT_ONE[..]
2405    }
2406    fn eval<'a, 'b, 'c>(
2407        &self,
2408        args: &'c [ArgumentHandle<'a, 'b>],
2409        _ctx: &dyn FunctionContext<'b>,
2410    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2411        let mut nums = collect_numeric_stats(&args[0..1])?;
2412        if nums.is_empty() {
2413            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2414                ExcelError::new_num(),
2415            )));
2416        }
2417
2418        let percent = match args[1].value()?.into_literal() {
2419            LiteralValue::Error(e) => {
2420                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
2421            }
2422            other => coerce_num(&other)?,
2423        };
2424
2425        // Percent must be between 0 and 1 (exclusive of 1)
2426        if !(0.0..1.0).contains(&percent) {
2427            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2428                ExcelError::new_num(),
2429            )));
2430        }
2431
2432        nums.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
2433
2434        let n = nums.len();
2435        // Number of values to exclude from each end
2436        let exclude = ((n as f64 * percent) / 2.0).floor() as usize;
2437
2438        if 2 * exclude >= n {
2439            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2440                ExcelError::new_num(),
2441            )));
2442        }
2443
2444        let trimmed = &nums[exclude..n - exclude];
2445        let sum: f64 = trimmed.iter().sum();
2446        let mean = sum / trimmed.len() as f64;
2447
2448        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(mean)))
2449    }
2450}
2451
2452/* ─────────────────────────── CORREL ──────────────────────────── */
2453
2454/// Helper to collect two paired arrays for regression/correlation functions
2455fn collect_paired_arrays(args: &[ArgumentHandle]) -> Result<(Vec<f64>, Vec<f64>), ExcelError> {
2456    let y_nums = collect_numeric_stats(&args[0..1])?;
2457    let x_nums = collect_numeric_stats(&args[1..2])?;
2458
2459    // Arrays must have same length
2460    if y_nums.len() != x_nums.len() {
2461        return Err(ExcelError::new_na());
2462    }
2463
2464    if y_nums.is_empty() {
2465        return Err(ExcelError::new_div());
2466    }
2467
2468    Ok((y_nums, x_nums))
2469}
2470
2471/// Returns the Pearson correlation coefficient between two numeric arrays.
2472///
2473/// `CORREL` measures linear relationship strength from `-1` (perfect inverse) to `1` (perfect
2474/// direct).
2475///
2476/// # Remarks
2477/// - Both arrays must produce the same number of numeric values.
2478/// - Returns `#N/A` when array lengths differ.
2479/// - Returns `#DIV/0!` when either series has zero variance.
2480///
2481/// # Examples
2482///
2483/// ```yaml,sandbox
2484/// title: "Perfect positive linear correlation"
2485/// formula: "=CORREL({2,4,6},{1,2,3})"
2486/// expected: 1
2487/// ```
2488///
2489/// ```yaml,sandbox
2490/// title: "Perfect negative linear correlation"
2491/// grid:
2492///   A1: 10
2493///   A2: 8
2494///   A3: 6
2495///   B1: 1
2496///   B2: 2
2497///   B3: 3
2498/// formula: "=CORREL(A1:A3,B1:B3)"
2499/// expected: -1
2500/// ```
2501#[derive(Debug)]
2502pub struct CorrelFn;
2503/// [formualizer-docgen:schema:start]
2504/// Name: CORREL
2505/// Type: CorrelFn
2506/// Min args: 2
2507/// Max args: 1
2508/// Variadic: false
2509/// Signature: CORREL(arg1: number@range)
2510/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
2511/// Caps: PURE, REDUCTION, NUMERIC_ONLY
2512/// [formualizer-docgen:schema:end]
2513impl Function for CorrelFn {
2514    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
2515    fn name(&self) -> &'static str {
2516        "CORREL"
2517    }
2518    fn min_args(&self) -> usize {
2519        2
2520    }
2521    fn arg_schema(&self) -> &'static [ArgSchema] {
2522        &ARG_RANGE_NUM_LENIENT_ONE[..]
2523    }
2524    fn eval<'a, 'b, 'c>(
2525        &self,
2526        args: &'c [ArgumentHandle<'a, 'b>],
2527        _ctx: &dyn FunctionContext<'b>,
2528    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2529        let (y, x) = match collect_paired_arrays(args) {
2530            Ok(v) => v,
2531            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
2532        };
2533
2534        let n = x.len() as f64;
2535        let mean_x = x.iter().sum::<f64>() / n;
2536        let mean_y = y.iter().sum::<f64>() / n;
2537
2538        let mut sum_xy = 0.0;
2539        let mut sum_x2 = 0.0;
2540        let mut sum_y2 = 0.0;
2541
2542        for i in 0..x.len() {
2543            let dx = x[i] - mean_x;
2544            let dy = y[i] - mean_y;
2545            sum_xy += dx * dy;
2546            sum_x2 += dx * dx;
2547            sum_y2 += dy * dy;
2548        }
2549
2550        let denom = (sum_x2 * sum_y2).sqrt();
2551        if denom == 0.0 {
2552            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2553                ExcelError::new_div(),
2554            )));
2555        }
2556
2557        let correl = sum_xy / denom;
2558        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
2559            correl,
2560        )))
2561    }
2562}
2563
2564/* ─────────────────────────── SLOPE ──────────────────────────── */
2565
2566/// Returns the slope of the linear regression line for paired data.
2567///
2568/// `SLOPE` fits `y = m*x + b` and returns `m`, the rate of change in `y` per unit of `x`.
2569///
2570/// # Remarks
2571/// - `known_y` and `known_x` must have the same numeric length.
2572/// - Returns `#N/A` for mismatched lengths.
2573/// - Returns `#DIV/0!` if all `x` values are identical.
2574///
2575/// # Examples
2576///
2577/// ```yaml,sandbox
2578/// title: "Positive slope from direct arrays"
2579/// formula: "=SLOPE({2,4,6},{1,2,3})"
2580/// expected: 2
2581/// ```
2582///
2583/// ```yaml,sandbox
2584/// title: "Negative slope from ranges"
2585/// grid:
2586///   A1: 10
2587///   A2: 8
2588///   A3: 6
2589///   B1: 1
2590///   B2: 2
2591///   B3: 3
2592/// formula: "=SLOPE(A1:A3,B1:B3)"
2593/// expected: -2
2594/// ```
2595#[derive(Debug)]
2596pub struct SlopeFn;
2597/// [formualizer-docgen:schema:start]
2598/// Name: SLOPE
2599/// Type: SlopeFn
2600/// Min args: 2
2601/// Max args: 1
2602/// Variadic: false
2603/// Signature: SLOPE(arg1: number@range)
2604/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
2605/// Caps: PURE, REDUCTION, NUMERIC_ONLY
2606/// [formualizer-docgen:schema:end]
2607impl Function for SlopeFn {
2608    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
2609    fn name(&self) -> &'static str {
2610        "SLOPE"
2611    }
2612    fn min_args(&self) -> usize {
2613        2
2614    }
2615    fn arg_schema(&self) -> &'static [ArgSchema] {
2616        &ARG_RANGE_NUM_LENIENT_ONE[..]
2617    }
2618    fn eval<'a, 'b, 'c>(
2619        &self,
2620        args: &'c [ArgumentHandle<'a, 'b>],
2621        _ctx: &dyn FunctionContext<'b>,
2622    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2623        let (y, x) = match collect_paired_arrays(args) {
2624            Ok(v) => v,
2625            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
2626        };
2627
2628        let n = x.len() as f64;
2629        let mean_x = x.iter().sum::<f64>() / n;
2630        let mean_y = y.iter().sum::<f64>() / n;
2631
2632        let mut sum_xy = 0.0;
2633        let mut sum_x2 = 0.0;
2634
2635        for i in 0..x.len() {
2636            let dx = x[i] - mean_x;
2637            let dy = y[i] - mean_y;
2638            sum_xy += dx * dy;
2639            sum_x2 += dx * dx;
2640        }
2641
2642        if sum_x2 == 0.0 {
2643            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2644                ExcelError::new_div(),
2645            )));
2646        }
2647
2648        let slope = sum_xy / sum_x2;
2649        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
2650            slope,
2651        )))
2652    }
2653}
2654
2655/* ─────────────────────────── INTERCEPT ──────────────────────────── */
2656
2657/// Returns the y-intercept of the linear regression line for paired data.
2658///
2659/// `INTERCEPT` fits `y = m*x + b` and returns `b`, the predicted `y` when `x = 0`.
2660///
2661/// # Remarks
2662/// - `known_y` and `known_x` must have the same numeric length.
2663/// - Returns `#N/A` for mismatched lengths.
2664/// - Returns `#DIV/0!` if all `x` values are identical.
2665///
2666/// # Examples
2667///
2668/// ```yaml,sandbox
2669/// title: "Positive intercept from direct arrays"
2670/// formula: "=INTERCEPT({3,5,7},{1,2,3})"
2671/// expected: 1
2672/// ```
2673///
2674/// ```yaml,sandbox
2675/// title: "Intercept from range-based linear trend"
2676/// grid:
2677///   A1: 10
2678///   A2: 8
2679///   A3: 6
2680///   B1: 1
2681///   B2: 2
2682///   B3: 3
2683/// formula: "=INTERCEPT(A1:A3,B1:B3)"
2684/// expected: 12
2685/// ```
2686#[derive(Debug)]
2687pub struct InterceptFn;
2688/// [formualizer-docgen:schema:start]
2689/// Name: INTERCEPT
2690/// Type: InterceptFn
2691/// Min args: 2
2692/// Max args: 1
2693/// Variadic: false
2694/// Signature: INTERCEPT(arg1: number@range)
2695/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
2696/// Caps: PURE, REDUCTION, NUMERIC_ONLY
2697/// [formualizer-docgen:schema:end]
2698impl Function for InterceptFn {
2699    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
2700    fn name(&self) -> &'static str {
2701        "INTERCEPT"
2702    }
2703    fn min_args(&self) -> usize {
2704        2
2705    }
2706    fn arg_schema(&self) -> &'static [ArgSchema] {
2707        &ARG_RANGE_NUM_LENIENT_ONE[..]
2708    }
2709    fn eval<'a, 'b, 'c>(
2710        &self,
2711        args: &'c [ArgumentHandle<'a, 'b>],
2712        _ctx: &dyn FunctionContext<'b>,
2713    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2714        let (y, x) = match collect_paired_arrays(args) {
2715            Ok(v) => v,
2716            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
2717        };
2718
2719        let n = x.len() as f64;
2720        let mean_x = x.iter().sum::<f64>() / n;
2721        let mean_y = y.iter().sum::<f64>() / n;
2722
2723        let mut sum_xy = 0.0;
2724        let mut sum_x2 = 0.0;
2725
2726        for i in 0..x.len() {
2727            let dx = x[i] - mean_x;
2728            let dy = y[i] - mean_y;
2729            sum_xy += dx * dy;
2730            sum_x2 += dx * dx;
2731        }
2732
2733        if sum_x2 == 0.0 {
2734            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2735                ExcelError::new_div(),
2736            )));
2737        }
2738
2739        let slope = sum_xy / sum_x2;
2740        let intercept = mean_y - slope * mean_x;
2741        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
2742            intercept,
2743        )))
2744    }
2745}
2746
2747/// [formualizer-docgen:schema:start]
2748/// Name: DEVSQ
2749/// Type: DevsqFn
2750/// Min args: 1
2751/// Max args: variadic
2752/// Variadic: true
2753/// Signature: DEVSQ(arg1...: number@range)
2754/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
2755/// Caps: PURE, REDUCTION, NUMERIC_ONLY
2756/// [formualizer-docgen:schema:end]
2757impl Function for DevsqFn {
2758    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
2759    fn name(&self) -> &'static str {
2760        "DEVSQ"
2761    }
2762    fn min_args(&self) -> usize {
2763        1
2764    }
2765    fn variadic(&self) -> bool {
2766        true
2767    }
2768    fn arg_schema(&self) -> &'static [ArgSchema] {
2769        &ARG_RANGE_NUM_LENIENT_ONE[..]
2770    }
2771    fn eval<'a, 'b, 'c>(
2772        &self,
2773        args: &'c [ArgumentHandle<'a, 'b>],
2774        _ctx: &dyn FunctionContext<'b>,
2775    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2776        let nums = collect_numeric_stats(args)?;
2777        if nums.is_empty() {
2778            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2779                ExcelError::new_num(),
2780            )));
2781        }
2782        let mean = nums.iter().sum::<f64>() / nums.len() as f64;
2783        let devsq = nums.iter().map(|x| (x - mean).powi(2)).sum::<f64>();
2784        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
2785            devsq,
2786        )))
2787    }
2788}
2789
2790/* ═══════════════════════════════════════════════════════════════════════════
2791STATISTICAL DISTRIBUTION FUNCTIONS
2792═══════════════════════════════════════════════════════════════════════════ */
2793
2794/// Helper: Standard normal CDF using error function approximation
2795fn std_norm_cdf(z: f64) -> f64 {
2796    // Use the complementary error function: Φ(z) = 0.5 * erfc(-z / sqrt(2))
2797    // Approximation using Abramowitz and Stegun formula 7.1.26
2798    let a1 = 0.254829592;
2799    let a2 = -0.284496736;
2800    let a3 = 1.421413741;
2801    let a4 = -1.453152027;
2802    let a5 = 1.061405429;
2803    let p = 0.3275911;
2804
2805    let sign = if z < 0.0 { -1.0 } else { 1.0 };
2806    let z_abs = z.abs() / std::f64::consts::SQRT_2;
2807
2808    let t = 1.0 / (1.0 + p * z_abs);
2809    let y = 1.0 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * (-z_abs * z_abs).exp();
2810
2811    0.5 * (1.0 + sign * y)
2812}
2813
2814/// Helper: Standard normal PDF
2815fn std_norm_pdf(z: f64) -> f64 {
2816    let inv_sqrt_2pi = 1.0 / (2.0 * std::f64::consts::PI).sqrt();
2817    inv_sqrt_2pi * (-0.5 * z * z).exp()
2818}
2819
2820/// Helper: Inverse standard normal CDF (probit function)
2821/// Uses Rational approximation from Abramowitz and Stegun
2822#[allow(clippy::excessive_precision)]
2823fn std_norm_inv(p: f64) -> Option<f64> {
2824    if p <= 0.0 || p >= 1.0 {
2825        return None;
2826    }
2827
2828    // Coefficients for rational approximation
2829    const A: [f64; 6] = [
2830        -3.969683028665376e+01,
2831        2.209460984245205e+02,
2832        -2.759285104469687e+02,
2833        1.383577518672690e+02,
2834        -3.066479806614716e+01,
2835        2.506628277459239e+00,
2836    ];
2837    const B: [f64; 5] = [
2838        -5.447609879822406e+01,
2839        1.615858368580409e+02,
2840        -1.556989798598866e+02,
2841        6.680131188771972e+01,
2842        -1.328068155288572e+01,
2843    ];
2844    const C: [f64; 6] = [
2845        -7.784894002430293e-03,
2846        -3.223964580411365e-01,
2847        -2.400758277161838e+00,
2848        -2.549732539343734e+00,
2849        4.374664141464968e+00,
2850        2.938163982698783e+00,
2851    ];
2852    const D: [f64; 4] = [
2853        7.784695709041462e-03,
2854        3.224671290700398e-01,
2855        2.445134137142996e+00,
2856        3.754408661907416e+00,
2857    ];
2858
2859    const P_LOW: f64 = 0.02425;
2860    const P_HIGH: f64 = 1.0 - P_LOW;
2861
2862    let q = p - 0.5;
2863
2864    if p < P_LOW {
2865        // Lower tail
2866        let r = (-2.0 * p.ln()).sqrt();
2867        let num = ((((C[0] * r + C[1]) * r + C[2]) * r + C[3]) * r + C[4]) * r + C[5];
2868        let den = (((D[0] * r + D[1]) * r + D[2]) * r + D[3]) * r + 1.0;
2869        Some(num / den)
2870    } else if p <= P_HIGH {
2871        // Central region
2872        let r = q * q;
2873        let num = ((((A[0] * r + A[1]) * r + A[2]) * r + A[3]) * r + A[4]) * r + A[5];
2874        let den = ((((B[0] * r + B[1]) * r + B[2]) * r + B[3]) * r + B[4]) * r + 1.0;
2875        Some(q * num / den)
2876    } else {
2877        // Upper tail
2878        let r = (-2.0 * (1.0 - p).ln()).sqrt();
2879        let num = ((((C[0] * r + C[1]) * r + C[2]) * r + C[3]) * r + C[4]) * r + C[5];
2880        let den = (((D[0] * r + D[1]) * r + D[2]) * r + D[3]) * r + 1.0;
2881        Some(-num / den)
2882    }
2883}
2884
2885/// Returns the standard normal probability for a z-score as either a CDF or PDF value.
2886///
2887/// Use `NORM.S.DIST` for z-based probability lookups when the distribution has mean `0` and
2888/// standard deviation `1`.
2889///
2890/// # Remarks
2891/// - Set `cumulative` to a non-zero value for the cumulative distribution `P(Z <= z)`.
2892/// - Set `cumulative` to `0` for the probability density at exactly `z`.
2893/// - Accepts any real-valued `z`; no domain clipping is applied.
2894/// - Invalid numeric coercions propagate as spreadsheet errors.
2895///
2896/// # Examples
2897///
2898/// ```yaml,sandbox
2899/// title: "Standard normal CDF at zero"
2900/// formula: "=NORM.S.DIST(0,TRUE)"
2901/// expected: 0.5
2902/// ```
2903///
2904/// ```yaml,sandbox
2905/// title: "Standard normal PDF at zero"
2906/// formula: "=NORM.S.DIST(0,FALSE)"
2907/// expected: 0.3989422804014327
2908/// ```
2909#[derive(Debug)]
2910pub struct NormSDistFn;
2911/// [formualizer-docgen:schema:start]
2912/// Name: NORM.S.DIST
2913/// Type: NormSDistFn
2914/// Min args: 2
2915/// Max args: 2
2916/// Variadic: false
2917/// Signature: NORM.S.DIST(arg1: number@scalar, arg2: number@scalar)
2918/// 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}
2919/// Caps: PURE
2920/// [formualizer-docgen:schema:end]
2921impl Function for NormSDistFn {
2922    func_caps!(PURE);
2923    fn name(&self) -> &'static str {
2924        "NORM.S.DIST"
2925    }
2926    fn min_args(&self) -> usize {
2927        2
2928    }
2929    fn arg_schema(&self) -> &'static [ArgSchema] {
2930        use std::sync::LazyLock;
2931        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
2932            vec![
2933                ArgSchema::number_lenient_scalar(),
2934                ArgSchema::number_lenient_scalar(),
2935            ]
2936        });
2937        &SCHEMA[..]
2938    }
2939    fn eval<'a, 'b, 'c>(
2940        &self,
2941        args: &'c [ArgumentHandle<'a, 'b>],
2942        _ctx: &dyn FunctionContext<'b>,
2943    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2944        let z = coerce_num(&scalar_like_value(&args[0])?)?;
2945        let cumulative = coerce_num(&scalar_like_value(&args[1])?)? != 0.0;
2946
2947        let result = if cumulative {
2948            std_norm_cdf(z)
2949        } else {
2950            std_norm_pdf(z)
2951        };
2952        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
2953            result,
2954        )))
2955    }
2956}
2957
2958/// Returns the z-score whose standard normal cumulative probability matches `probability`.
2959///
2960/// This is the inverse of `NORM.S.DIST(z, TRUE)` and is commonly used for critical-value
2961/// thresholds.
2962///
2963/// # Remarks
2964/// - `probability` must be strictly between `0` and `1`.
2965/// - Returns `#NUM!` when `probability <= 0` or `probability >= 1`.
2966/// - Output can be negative, zero, or positive depending on which side of `0.5` you query.
2967/// - Invalid numeric coercions propagate as spreadsheet errors.
2968///
2969/// # Examples
2970///
2971/// ```yaml,sandbox
2972/// title: "Median probability maps to zero"
2973/// formula: "=NORM.S.INV(0.5)"
2974/// expected: 0
2975/// ```
2976///
2977/// ```yaml,sandbox
2978/// title: "Upper-tail critical z-score"
2979/// formula: "=NORM.S.INV(0.975)"
2980/// expected: 1.959963986120195
2981/// ```
2982#[derive(Debug)]
2983pub struct NormSInvFn;
2984/// [formualizer-docgen:schema:start]
2985/// Name: NORM.S.INV
2986/// Type: NormSInvFn
2987/// Min args: 1
2988/// Max args: 1
2989/// Variadic: false
2990/// Signature: NORM.S.INV(arg1: number@scalar)
2991/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
2992/// Caps: PURE
2993/// [formualizer-docgen:schema:end]
2994impl Function for NormSInvFn {
2995    func_caps!(PURE);
2996    fn name(&self) -> &'static str {
2997        "NORM.S.INV"
2998    }
2999    fn min_args(&self) -> usize {
3000        1
3001    }
3002    fn arg_schema(&self) -> &'static [ArgSchema] {
3003        use std::sync::LazyLock;
3004        static SCHEMA: LazyLock<Vec<ArgSchema>> =
3005            LazyLock::new(|| vec![ArgSchema::number_lenient_scalar()]);
3006        &SCHEMA[..]
3007    }
3008    fn eval<'a, 'b, 'c>(
3009        &self,
3010        args: &'c [ArgumentHandle<'a, 'b>],
3011        _ctx: &dyn FunctionContext<'b>,
3012    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3013        let p = coerce_num(&scalar_like_value(&args[0])?)?;
3014
3015        match std_norm_inv(p) {
3016            Some(z) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(z))),
3017            None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3018                ExcelError::new_num(),
3019            ))),
3020        }
3021    }
3022}
3023
3024/// Returns the normal-distribution probability at `x` for a given mean and standard deviation.
3025///
3026/// Use `NORM.DIST` for either cumulative probabilities or point density under a non-standard
3027/// normal model.
3028///
3029/// # Remarks
3030/// - Set `cumulative` to non-zero for `P(X <= x)`; set it to `0` for density mode.
3031/// - `standard_dev` must be strictly greater than `0`.
3032/// - Returns `#NUM!` when `standard_dev <= 0`.
3033/// - Invalid numeric coercions propagate as spreadsheet errors.
3034///
3035/// # Examples
3036///
3037/// ```yaml,sandbox
3038/// title: "Normal CDF at the mean"
3039/// formula: "=NORM.DIST(50,50,10,TRUE)"
3040/// expected: 0.5
3041/// ```
3042///
3043/// ```yaml,sandbox
3044/// title: "Normal PDF at the mean"
3045/// formula: "=NORM.DIST(50,50,10,FALSE)"
3046/// expected: 0.03989422804014327
3047/// ```
3048#[derive(Debug)]
3049pub struct NormDistFn;
3050/// [formualizer-docgen:schema:start]
3051/// Name: NORM.DIST
3052/// Type: NormDistFn
3053/// Min args: 4
3054/// Max args: 4
3055/// Variadic: false
3056/// Signature: NORM.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar)
3057/// 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=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
3058/// Caps: PURE
3059/// [formualizer-docgen:schema:end]
3060impl Function for NormDistFn {
3061    func_caps!(PURE);
3062    fn name(&self) -> &'static str {
3063        "NORM.DIST"
3064    }
3065    fn min_args(&self) -> usize {
3066        4
3067    }
3068    fn arg_schema(&self) -> &'static [ArgSchema] {
3069        use std::sync::LazyLock;
3070        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
3071            vec![
3072                ArgSchema::number_lenient_scalar(),
3073                ArgSchema::number_lenient_scalar(),
3074                ArgSchema::number_lenient_scalar(),
3075                ArgSchema::number_lenient_scalar(),
3076            ]
3077        });
3078        &SCHEMA[..]
3079    }
3080    fn eval<'a, 'b, 'c>(
3081        &self,
3082        args: &'c [ArgumentHandle<'a, 'b>],
3083        _ctx: &dyn FunctionContext<'b>,
3084    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3085        let x = coerce_num(&scalar_like_value(&args[0])?)?;
3086        let mean = coerce_num(&scalar_like_value(&args[1])?)?;
3087        let std_dev = coerce_num(&scalar_like_value(&args[2])?)?;
3088        let cumulative = coerce_num(&scalar_like_value(&args[3])?)? != 0.0;
3089
3090        if std_dev <= 0.0 {
3091            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3092                ExcelError::new_num(),
3093            )));
3094        }
3095
3096        let z = (x - mean) / std_dev;
3097
3098        let result = if cumulative {
3099            std_norm_cdf(z)
3100        } else {
3101            std_norm_pdf(z) / std_dev
3102        };
3103        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
3104            result,
3105        )))
3106    }
3107}
3108
3109/// Returns the value `x` whose normal cumulative probability equals `probability`.
3110///
3111/// This function is the inverse of `NORM.DIST(x, mean, standard_dev, TRUE)`.
3112///
3113/// # Remarks
3114/// - `probability` must be strictly between `0` and `1`.
3115/// - `standard_dev` must be strictly greater than `0`.
3116/// - Returns `#NUM!` for invalid probability bounds or non-positive standard deviation.
3117/// - Invalid numeric coercions propagate as spreadsheet errors.
3118///
3119/// # Examples
3120///
3121/// ```yaml,sandbox
3122/// title: "Median probability returns the mean"
3123/// formula: "=NORM.INV(0.5,10,2)"
3124/// expected: 10
3125/// ```
3126///
3127/// ```yaml,sandbox
3128/// title: "One-standard-deviation quantile"
3129/// formula: "=NORM.INV(0.841344746068543,0,1)"
3130/// expected: 1
3131/// ```
3132#[derive(Debug)]
3133pub struct NormInvFn;
3134/// [formualizer-docgen:schema:start]
3135/// Name: NORM.INV
3136/// Type: NormInvFn
3137/// Min args: 3
3138/// Max args: 3
3139/// Variadic: false
3140/// Signature: NORM.INV(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
3141/// 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}
3142/// Caps: PURE
3143/// [formualizer-docgen:schema:end]
3144impl Function for NormInvFn {
3145    func_caps!(PURE);
3146    fn name(&self) -> &'static str {
3147        "NORM.INV"
3148    }
3149    fn min_args(&self) -> usize {
3150        3
3151    }
3152    fn arg_schema(&self) -> &'static [ArgSchema] {
3153        use std::sync::LazyLock;
3154        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
3155            vec![
3156                ArgSchema::number_lenient_scalar(),
3157                ArgSchema::number_lenient_scalar(),
3158                ArgSchema::number_lenient_scalar(),
3159            ]
3160        });
3161        &SCHEMA[..]
3162    }
3163    fn eval<'a, 'b, 'c>(
3164        &self,
3165        args: &'c [ArgumentHandle<'a, 'b>],
3166        _ctx: &dyn FunctionContext<'b>,
3167    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3168        let p = coerce_num(&scalar_like_value(&args[0])?)?;
3169        let mean = coerce_num(&scalar_like_value(&args[1])?)?;
3170        let std_dev = coerce_num(&scalar_like_value(&args[2])?)?;
3171
3172        if std_dev <= 0.0 {
3173            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3174                ExcelError::new_num(),
3175            )));
3176        }
3177
3178        match std_norm_inv(p) {
3179            Some(z) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
3180                mean + z * std_dev,
3181            ))),
3182            None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3183                ExcelError::new_num(),
3184            ))),
3185        }
3186    }
3187}
3188
3189/// Returns the log-normal probability at `x` as either a cumulative value or density.
3190///
3191/// `LOGNORM.DIST` models positive-valued variables where `ln(X)` follows a normal distribution.
3192///
3193/// # Remarks
3194/// - Set `cumulative` to non-zero for CDF mode; set it to `0` for PDF mode.
3195/// - Requires `x > 0` and `standard_dev > 0`.
3196/// - Returns `#NUM!` when `x <= 0` or `standard_dev <= 0`.
3197/// - Invalid numeric coercions propagate as spreadsheet errors.
3198///
3199/// # Examples
3200///
3201/// ```yaml,sandbox
3202/// title: "Log-normal CDF at x = 1"
3203/// formula: "=LOGNORM.DIST(1,0,1,TRUE)"
3204/// expected: 0.5
3205/// ```
3206///
3207/// ```yaml,sandbox
3208/// title: "Log-normal PDF at x = 1"
3209/// formula: "=LOGNORM.DIST(1,0,1,FALSE)"
3210/// expected: 0.3989422804014327
3211/// ```
3212#[derive(Debug)]
3213pub struct LognormDistFn;
3214/// [formualizer-docgen:schema:start]
3215/// Name: LOGNORM.DIST
3216/// Type: LognormDistFn
3217/// Min args: 4
3218/// Max args: 4
3219/// Variadic: false
3220/// Signature: LOGNORM.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar)
3221/// 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=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
3222/// Caps: PURE
3223/// [formualizer-docgen:schema:end]
3224impl Function for LognormDistFn {
3225    func_caps!(PURE);
3226    fn name(&self) -> &'static str {
3227        "LOGNORM.DIST"
3228    }
3229    fn min_args(&self) -> usize {
3230        4
3231    }
3232    fn arg_schema(&self) -> &'static [ArgSchema] {
3233        use std::sync::LazyLock;
3234        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
3235            vec![
3236                ArgSchema::number_lenient_scalar(),
3237                ArgSchema::number_lenient_scalar(),
3238                ArgSchema::number_lenient_scalar(),
3239                ArgSchema::number_lenient_scalar(),
3240            ]
3241        });
3242        &SCHEMA[..]
3243    }
3244    fn eval<'a, 'b, 'c>(
3245        &self,
3246        args: &'c [ArgumentHandle<'a, 'b>],
3247        _ctx: &dyn FunctionContext<'b>,
3248    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3249        let x = coerce_num(&scalar_like_value(&args[0])?)?;
3250        let mean = coerce_num(&scalar_like_value(&args[1])?)?;
3251        let std_dev = coerce_num(&scalar_like_value(&args[2])?)?;
3252        let cumulative = coerce_num(&scalar_like_value(&args[3])?)? != 0.0;
3253
3254        if x <= 0.0 || std_dev <= 0.0 {
3255            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3256                ExcelError::new_num(),
3257            )));
3258        }
3259
3260        let z = (x.ln() - mean) / std_dev;
3261
3262        let result = if cumulative {
3263            std_norm_cdf(z)
3264        } else {
3265            std_norm_pdf(z) / (x * std_dev)
3266        };
3267        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
3268            result,
3269        )))
3270    }
3271}
3272
3273/// Returns the positive value `x` whose log-normal cumulative probability is `probability`.
3274///
3275/// This function inverts `LOGNORM.DIST(x, mean, standard_dev, TRUE)`.
3276///
3277/// # Remarks
3278/// - `probability` must be strictly between `0` and `1`.
3279/// - `standard_dev` must be strictly greater than `0`.
3280/// - Returns `#NUM!` when inputs violate probability or scale constraints.
3281/// - Invalid numeric coercions propagate as spreadsheet errors.
3282///
3283/// # Examples
3284///
3285/// ```yaml,sandbox
3286/// title: "Median log-normal quantile"
3287/// formula: "=LOGNORM.INV(0.5,0,1)"
3288/// expected: 1
3289/// ```
3290///
3291/// ```yaml,sandbox
3292/// title: "Upper quantile for mean 0 and stdev 1"
3293/// formula: "=LOGNORM.INV(0.841344746068543,0,1)"
3294/// expected: 2.718281828459045
3295/// ```
3296#[derive(Debug)]
3297pub struct LognormInvFn;
3298/// [formualizer-docgen:schema:start]
3299/// Name: LOGNORM.INV
3300/// Type: LognormInvFn
3301/// Min args: 3
3302/// Max args: 3
3303/// Variadic: false
3304/// Signature: LOGNORM.INV(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
3305/// 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}
3306/// Caps: PURE
3307/// [formualizer-docgen:schema:end]
3308impl Function for LognormInvFn {
3309    func_caps!(PURE);
3310    fn name(&self) -> &'static str {
3311        "LOGNORM.INV"
3312    }
3313    fn min_args(&self) -> usize {
3314        3
3315    }
3316    fn arg_schema(&self) -> &'static [ArgSchema] {
3317        use std::sync::LazyLock;
3318        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
3319            vec![
3320                ArgSchema::number_lenient_scalar(),
3321                ArgSchema::number_lenient_scalar(),
3322                ArgSchema::number_lenient_scalar(),
3323            ]
3324        });
3325        &SCHEMA[..]
3326    }
3327    fn eval<'a, 'b, 'c>(
3328        &self,
3329        args: &'c [ArgumentHandle<'a, 'b>],
3330        _ctx: &dyn FunctionContext<'b>,
3331    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3332        let p = coerce_num(&scalar_like_value(&args[0])?)?;
3333        let mean = coerce_num(&scalar_like_value(&args[1])?)?;
3334        let std_dev = coerce_num(&scalar_like_value(&args[2])?)?;
3335
3336        if std_dev <= 0.0 {
3337            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3338                ExcelError::new_num(),
3339            )));
3340        }
3341
3342        match std_norm_inv(p) {
3343            Some(z) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
3344                (mean + z * std_dev).exp(),
3345            ))),
3346            None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3347                ExcelError::new_num(),
3348            ))),
3349        }
3350    }
3351}
3352
3353/// Returns the standard normal probability density at `x`.
3354///
3355/// `PHI` is equivalent to `NORM.S.DIST(x, FALSE)` and is useful in continuous-probability
3356/// calculations.
3357///
3358/// # Remarks
3359/// - Evaluates the density of a standard normal variable centered at `0`.
3360/// - The result is always non-negative and symmetric around `x = 0`.
3361/// - Works for any real input value.
3362/// - Invalid numeric coercions propagate as spreadsheet errors.
3363///
3364/// # Examples
3365///
3366/// ```yaml,sandbox
3367/// title: "Standard normal density at zero"
3368/// formula: "=PHI(0)"
3369/// expected: 0.3989422804014327
3370/// ```
3371///
3372/// ```yaml,sandbox
3373/// title: "Standard normal density at one"
3374/// formula: "=PHI(1)"
3375/// expected: 0.24197072451914337
3376/// ```
3377#[derive(Debug)]
3378pub struct PhiFn;
3379/// [formualizer-docgen:schema:start]
3380/// Name: PHI
3381/// Type: PhiFn
3382/// Min args: 1
3383/// Max args: 1
3384/// Variadic: false
3385/// Signature: PHI(arg1: number@scalar)
3386/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
3387/// Caps: PURE
3388/// [formualizer-docgen:schema:end]
3389impl Function for PhiFn {
3390    func_caps!(PURE);
3391    fn name(&self) -> &'static str {
3392        "PHI"
3393    }
3394    fn min_args(&self) -> usize {
3395        1
3396    }
3397    fn arg_schema(&self) -> &'static [ArgSchema] {
3398        use std::sync::LazyLock;
3399        static SCHEMA: LazyLock<Vec<ArgSchema>> =
3400            LazyLock::new(|| vec![ArgSchema::number_lenient_scalar()]);
3401        &SCHEMA[..]
3402    }
3403    fn eval<'a, 'b, 'c>(
3404        &self,
3405        args: &'c [ArgumentHandle<'a, 'b>],
3406        _ctx: &dyn FunctionContext<'b>,
3407    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3408        let z = coerce_num(&scalar_like_value(&args[0])?)?;
3409        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
3410            std_norm_pdf(z),
3411        )))
3412    }
3413}
3414
3415/// Returns the standard normal area between `0` and `z`.
3416///
3417/// `GAUSS` computes `NORM.S.DIST(z, TRUE) - 0.5`, preserving the sign of `z`.
3418///
3419/// # Remarks
3420/// - Positive `z` returns a positive area; negative `z` returns a negative area.
3421/// - `GAUSS(0)` returns `0`.
3422/// - Output magnitude is always less than `0.5`.
3423/// - Invalid numeric coercions propagate as spreadsheet errors.
3424///
3425/// # Examples
3426///
3427/// ```yaml,sandbox
3428/// title: "Area from mean to z = 1"
3429/// formula: "=GAUSS(1)"
3430/// expected: 0.3413447460685429
3431/// ```
3432///
3433/// ```yaml,sandbox
3434/// title: "Symmetric negative z-value"
3435/// formula: "=GAUSS(-1)"
3436/// expected: -0.3413447460685429
3437/// ```
3438#[derive(Debug)]
3439pub struct GaussFn;
3440/// [formualizer-docgen:schema:start]
3441/// Name: GAUSS
3442/// Type: GaussFn
3443/// Min args: 1
3444/// Max args: 1
3445/// Variadic: false
3446/// Signature: GAUSS(arg1: number@scalar)
3447/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
3448/// Caps: PURE
3449/// [formualizer-docgen:schema:end]
3450impl Function for GaussFn {
3451    func_caps!(PURE);
3452    fn name(&self) -> &'static str {
3453        "GAUSS"
3454    }
3455    fn min_args(&self) -> usize {
3456        1
3457    }
3458    fn arg_schema(&self) -> &'static [ArgSchema] {
3459        use std::sync::LazyLock;
3460        static SCHEMA: LazyLock<Vec<ArgSchema>> =
3461            LazyLock::new(|| vec![ArgSchema::number_lenient_scalar()]);
3462        &SCHEMA[..]
3463    }
3464    fn eval<'a, 'b, 'c>(
3465        &self,
3466        args: &'c [ArgumentHandle<'a, 'b>],
3467        _ctx: &dyn FunctionContext<'b>,
3468    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3469        let z = coerce_num(&scalar_like_value(&args[0])?)?;
3470        // GAUSS(z) = Φ(z) - 0.5
3471        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
3472            std_norm_cdf(z) - 0.5,
3473        )))
3474    }
3475}
3476
3477/// Helper: Log-gamma function
3478#[allow(clippy::excessive_precision)]
3479fn ln_gamma(x: f64) -> f64 {
3480    // Lanczos approximation
3481    const G: f64 = 7.0;
3482    const C: [f64; 9] = [
3483        0.99999999999980993,
3484        676.5203681218851,
3485        -1259.1392167224028,
3486        771.32342877765313,
3487        -176.61502916214059,
3488        12.507343278686905,
3489        -0.13857109526572012,
3490        9.9843695780195716e-6,
3491        1.5056327351493116e-7,
3492    ];
3493
3494    if x < 0.5 {
3495        // Reflection formula
3496        let pi = std::f64::consts::PI;
3497        pi.ln() - (pi * x).sin().ln() - ln_gamma(1.0 - x)
3498    } else {
3499        let x = x - 1.0;
3500        let mut ag = C[0];
3501        for (i, c) in C.iter().enumerate().skip(1) {
3502            ag += c / (x + i as f64);
3503        }
3504        let tmp = x + G + 0.5;
3505        0.5 * (2.0 * std::f64::consts::PI).ln() + (tmp).ln() * (x + 0.5) - tmp + ag.ln()
3506    }
3507}
3508
3509/// Helper: Regularized lower incomplete gamma function P(a, x)
3510fn gamma_p(a: f64, x: f64) -> f64 {
3511    if x < 0.0 || a <= 0.0 {
3512        return 0.0;
3513    }
3514    if x == 0.0 {
3515        return 0.0;
3516    }
3517
3518    // Use series expansion for x < a+1
3519    if x < a + 1.0 {
3520        gamma_series(a, x)
3521    } else {
3522        // Use continued fraction for x >= a+1
3523        1.0 - gamma_cf(a, x)
3524    }
3525}
3526
3527/// Helper: Series expansion for incomplete gamma
3528fn gamma_series(a: f64, x: f64) -> f64 {
3529    let ln_ga = ln_gamma(a);
3530    let mut sum = 1.0 / a;
3531    let mut term = sum;
3532    for n in 1..200 {
3533        term *= x / (a + n as f64);
3534        sum += term;
3535        if term.abs() < sum.abs() * 1e-15 {
3536            break;
3537        }
3538    }
3539    sum * (-x + a * x.ln() - ln_ga).exp()
3540}
3541
3542/// Helper: Continued fraction for upper incomplete gamma Q(a,x)
3543/// Using modified Lentz's algorithm (Numerical Recipes formulation)
3544fn gamma_cf(a: f64, x: f64) -> f64 {
3545    let ln_ga = ln_gamma(a);
3546    const TINY: f64 = 1e-30;
3547    const EPS: f64 = 1e-14;
3548
3549    // Set up for evaluating continued fraction by modified Lentz's method
3550    let mut b = x + 1.0 - a;
3551    let mut c = 1.0 / TINY;
3552    let mut d = 1.0 / b;
3553    let mut h = d;
3554
3555    for i in 1..=200 {
3556        let an = -(i as f64) * (i as f64 - a);
3557        b += 2.0;
3558        d = an * d + b;
3559        if d.abs() < TINY {
3560            d = TINY;
3561        }
3562        c = b + an / c;
3563        if c.abs() < TINY {
3564            c = TINY;
3565        }
3566        d = 1.0 / d;
3567        let delta = d * c;
3568        h *= delta;
3569        if (delta - 1.0).abs() <= EPS {
3570            break;
3571        }
3572    }
3573
3574    h * (-x + a * x.ln() - ln_ga).exp()
3575}
3576
3577/// Helper: Regularized incomplete beta function I_x(a,b)
3578/// Uses the continued fraction representation (NIST DLMF 8.17.22)
3579fn beta_i(x: f64, a: f64, b: f64) -> f64 {
3580    if x <= 0.0 {
3581        return 0.0;
3582    }
3583    if x >= 1.0 {
3584        return 1.0;
3585    }
3586    if a <= 0.0 || b <= 0.0 {
3587        return f64::NAN;
3588    }
3589
3590    // Use symmetry for better convergence: I_x(a,b) = 1 - I_{1-x}(b,a)
3591    if x > (a + 1.0) / (a + b + 2.0) {
3592        return 1.0 - beta_i(1.0 - x, b, a);
3593    }
3594
3595    // Compute the prefactor: x^a * (1-x)^b / (a * B(a,b))
3596    let ln_beta = ln_gamma(a) + ln_gamma(b) - ln_gamma(a + b);
3597    let ln_prefactor = a * x.ln() + b * (1.0 - x).ln() - ln_beta - a.ln();
3598    let prefactor = ln_prefactor.exp();
3599
3600    // Evaluate the continued fraction using modified Lentz algorithm
3601    // The CF is: 1 / (1 + d1/(1 + d2/(1 + ...)))
3602    // where d_{2m+1} = -(a+m)(a+b+m)x / ((a+2m)(a+2m+1))
3603    //       d_{2m}   = m(b-m)x / ((a+2m-1)(a+2m))
3604    const EPS: f64 = 1e-14;
3605    const TINY: f64 = 1e-30;
3606
3607    let qab = a + b;
3608    let qap = a + 1.0;
3609    let qam = a - 1.0;
3610    let mut c = 1.0;
3611    let mut d = 1.0 - qab * x / qap;
3612    if d.abs() < TINY {
3613        d = TINY;
3614    }
3615    d = 1.0 / d;
3616    let mut h = d;
3617
3618    for m in 1..=200 {
3619        let m_f64 = m as f64;
3620        let m2 = 2.0 * m_f64;
3621
3622        // Even step: d_{2m} = m(b-m)x / ((a+2m-1)(a+2m))
3623        let aa = m_f64 * (b - m_f64) * x / ((qam + m2) * (a + m2));
3624        d = 1.0 + aa * d;
3625        if d.abs() < TINY {
3626            d = TINY;
3627        }
3628        c = 1.0 + aa / c;
3629        if c.abs() < TINY {
3630            c = TINY;
3631        }
3632        d = 1.0 / d;
3633        h *= d * c;
3634
3635        // Odd step: d_{2m+1} = -(a+m)(a+b+m)x / ((a+2m)(a+2m+1))
3636        let aa = -((a + m_f64) * (qab + m_f64) * x) / ((a + m2) * (qap + m2));
3637        d = 1.0 + aa * d;
3638        if d.abs() < TINY {
3639            d = TINY;
3640        }
3641        c = 1.0 + aa / c;
3642        if c.abs() < TINY {
3643            c = TINY;
3644        }
3645        d = 1.0 / d;
3646        let delta = d * c;
3647        h *= delta;
3648
3649        if (delta - 1.0).abs() <= EPS {
3650            break;
3651        }
3652    }
3653
3654    prefactor * h
3655}
3656
3657/// Helper: T distribution CDF
3658fn t_cdf(t: f64, df: f64) -> f64 {
3659    let x = df / (df + t * t);
3660    0.5 * (1.0 + t.signum() * (1.0 - beta_i(x, df / 2.0, 0.5)))
3661}
3662
3663/// Helper: T distribution inverse CDF using Newton-Raphson
3664fn t_inv(p: f64, df: f64) -> Option<f64> {
3665    if p <= 0.0 || p >= 1.0 {
3666        return None;
3667    }
3668
3669    // Initial guess using normal approximation
3670    let mut t = std_norm_inv(p)?;
3671
3672    // Newton-Raphson iteration
3673    for _ in 0..50 {
3674        let cdf = t_cdf(t, df);
3675        let pdf = t_pdf(t, df);
3676        if pdf.abs() < 1e-30 {
3677            break;
3678        }
3679        let delta = (cdf - p) / pdf;
3680        t -= delta;
3681        if delta.abs() < 1e-12 {
3682            break;
3683        }
3684    }
3685
3686    Some(t)
3687}
3688
3689/// Helper: T distribution PDF
3690fn t_pdf(t: f64, df: f64) -> f64 {
3691    let coef =
3692        (ln_gamma((df + 1.0) / 2.0) - ln_gamma(df / 2.0) - 0.5 * (df * std::f64::consts::PI).ln())
3693            .exp();
3694    coef * (1.0 + t * t / df).powf(-(df + 1.0) / 2.0)
3695}
3696
3697/// Helper: Chi-square CDF
3698fn chisq_cdf(x: f64, df: f64) -> f64 {
3699    if x <= 0.0 {
3700        return 0.0;
3701    }
3702    gamma_p(df / 2.0, x / 2.0)
3703}
3704
3705/// Helper: Chi-square inverse CDF using Newton-Raphson
3706fn chisq_inv(p: f64, df: f64) -> Option<f64> {
3707    if p <= 0.0 || p >= 1.0 {
3708        return None;
3709    }
3710
3711    // Initial guess
3712    let mut x = df.max(1.0);
3713    if p < 0.5 {
3714        x = x.min(1.0);
3715    }
3716
3717    // Newton-Raphson iteration
3718    for _ in 0..100 {
3719        let cdf = chisq_cdf(x, df);
3720        let pdf = chisq_pdf(x, df);
3721        if pdf.abs() < 1e-30 {
3722            break;
3723        }
3724        let delta = (cdf - p) / pdf;
3725        let new_x = (x - delta).max(1e-15);
3726        if (new_x - x).abs() < 1e-12 * x {
3727            x = new_x;
3728            break;
3729        }
3730        x = new_x;
3731    }
3732
3733    Some(x)
3734}
3735
3736/// Helper: Chi-square PDF
3737fn chisq_pdf(x: f64, df: f64) -> f64 {
3738    if x <= 0.0 {
3739        return 0.0;
3740    }
3741    let k = df / 2.0;
3742    ((k - 1.0) * x.ln() - x / 2.0 - k * 2.0_f64.ln() - ln_gamma(k)).exp()
3743}
3744
3745/// Helper: F distribution CDF
3746fn f_cdf(f: f64, d1: f64, d2: f64) -> f64 {
3747    if f <= 0.0 {
3748        return 0.0;
3749    }
3750    let x = d1 * f / (d1 * f + d2);
3751    beta_i(x, d1 / 2.0, d2 / 2.0)
3752}
3753
3754/// Helper: F distribution inverse CDF using Newton-Raphson
3755fn f_inv(p: f64, d1: f64, d2: f64) -> Option<f64> {
3756    if p <= 0.0 || p >= 1.0 {
3757        return None;
3758    }
3759
3760    // Initial guess
3761    let mut f = 1.0;
3762
3763    // Newton-Raphson iteration
3764    for _ in 0..100 {
3765        let cdf = f_cdf(f, d1, d2);
3766        let pdf = f_pdf(f, d1, d2);
3767        if pdf.abs() < 1e-30 {
3768            break;
3769        }
3770        let delta = (cdf - p) / pdf;
3771        let new_f = (f - delta).max(1e-15);
3772        if (new_f - f).abs() < 1e-12 * f {
3773            f = new_f;
3774            break;
3775        }
3776        f = new_f;
3777    }
3778
3779    Some(f)
3780}
3781
3782/// Helper: F distribution PDF
3783fn f_pdf(f: f64, d1: f64, d2: f64) -> f64 {
3784    if f <= 0.0 {
3785        return 0.0;
3786    }
3787    let ln_beta = ln_gamma(d1 / 2.0) + ln_gamma(d2 / 2.0) - ln_gamma((d1 + d2) / 2.0);
3788    let coef = (d1 / 2.0) * (d1 / d2).ln() + (d1 / 2.0 - 1.0) * f.ln()
3789        - ((d1 + d2) / 2.0) * (1.0 + d1 * f / d2).ln()
3790        - ln_beta;
3791    coef.exp()
3792}
3793
3794/// Returns the Student's t probability for `x` and a given degrees-of-freedom value.
3795///
3796/// Use `T.DIST` in either cumulative mode (left-tail probability) or density mode.
3797///
3798/// # Remarks
3799/// - Set `cumulative` to non-zero for CDF mode, or `0` for PDF mode.
3800/// - `deg_freedom` must be at least `1`.
3801/// - Returns `#NUM!` when `deg_freedom < 1`.
3802/// - Invalid numeric coercions propagate as spreadsheet errors.
3803///
3804/// # Examples
3805///
3806/// ```yaml,sandbox
3807/// title: "t CDF at zero"
3808/// formula: "=T.DIST(0,10,TRUE)"
3809/// expected: 0.5
3810/// ```
3811///
3812/// ```yaml,sandbox
3813/// title: "t PDF at zero"
3814/// formula: "=T.DIST(0,10,FALSE)"
3815/// expected: 0.389108383966031
3816/// ```
3817#[derive(Debug)]
3818pub struct TDistFn;
3819/// [formualizer-docgen:schema:start]
3820/// Name: T.DIST
3821/// Type: TDistFn
3822/// Min args: 3
3823/// Max args: 3
3824/// Variadic: false
3825/// Signature: T.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
3826/// 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}
3827/// Caps: PURE
3828/// [formualizer-docgen:schema:end]
3829impl Function for TDistFn {
3830    func_caps!(PURE);
3831    fn name(&self) -> &'static str {
3832        "T.DIST"
3833    }
3834    fn min_args(&self) -> usize {
3835        3
3836    }
3837    fn arg_schema(&self) -> &'static [ArgSchema] {
3838        use std::sync::LazyLock;
3839        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
3840            vec![
3841                ArgSchema::number_lenient_scalar(),
3842                ArgSchema::number_lenient_scalar(),
3843                ArgSchema::number_lenient_scalar(),
3844            ]
3845        });
3846        &SCHEMA[..]
3847    }
3848    fn eval<'a, 'b, 'c>(
3849        &self,
3850        args: &'c [ArgumentHandle<'a, 'b>],
3851        _ctx: &dyn FunctionContext<'b>,
3852    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3853        let x = coerce_num(&scalar_like_value(&args[0])?)?;
3854        let df = coerce_num(&scalar_like_value(&args[1])?)?;
3855        let cumulative = coerce_num(&scalar_like_value(&args[2])?)? != 0.0;
3856
3857        if df < 1.0 {
3858            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3859                ExcelError::new_num(),
3860            )));
3861        }
3862
3863        let result = if cumulative {
3864            t_cdf(x, df)
3865        } else {
3866            t_pdf(x, df)
3867        };
3868        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
3869            result,
3870        )))
3871    }
3872}
3873
3874/// Returns the t-value whose left-tail probability equals `probability`.
3875///
3876/// `T.INV` is the inverse of `T.DIST(x, deg_freedom, TRUE)`.
3877///
3878/// # Remarks
3879/// - `probability` must be strictly between `0` and `1`.
3880/// - `deg_freedom` must be at least `1`.
3881/// - Returns `#NUM!` for out-of-range probability or invalid degrees of freedom.
3882/// - Invalid numeric coercions propagate as spreadsheet errors.
3883///
3884/// # Examples
3885///
3886/// ```yaml,sandbox
3887/// title: "Median t quantile"
3888/// formula: "=T.INV(0.5,10)"
3889/// expected: 0
3890/// ```
3891///
3892/// ```yaml,sandbox
3893/// title: "Upper-tail critical value"
3894/// formula: "=T.INV(0.975,10)"
3895/// expected: 2.228138851986273
3896/// ```
3897#[derive(Debug)]
3898pub struct TInvFn;
3899/// [formualizer-docgen:schema:start]
3900/// Name: T.INV
3901/// Type: TInvFn
3902/// Min args: 2
3903/// Max args: 2
3904/// Variadic: false
3905/// Signature: T.INV(arg1: number@scalar, arg2: number@scalar)
3906/// 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}
3907/// Caps: PURE
3908/// [formualizer-docgen:schema:end]
3909impl Function for TInvFn {
3910    func_caps!(PURE);
3911    fn name(&self) -> &'static str {
3912        "T.INV"
3913    }
3914    fn min_args(&self) -> usize {
3915        2
3916    }
3917    fn arg_schema(&self) -> &'static [ArgSchema] {
3918        use std::sync::LazyLock;
3919        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
3920            vec![
3921                ArgSchema::number_lenient_scalar(),
3922                ArgSchema::number_lenient_scalar(),
3923            ]
3924        });
3925        &SCHEMA[..]
3926    }
3927    fn eval<'a, 'b, 'c>(
3928        &self,
3929        args: &'c [ArgumentHandle<'a, 'b>],
3930        _ctx: &dyn FunctionContext<'b>,
3931    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3932        let p = coerce_num(&scalar_like_value(&args[0])?)?;
3933        let df = coerce_num(&scalar_like_value(&args[1])?)?;
3934
3935        if df < 1.0 {
3936            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3937                ExcelError::new_num(),
3938            )));
3939        }
3940
3941        match t_inv(p, df) {
3942            Some(result) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
3943                result,
3944            ))),
3945            None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3946                ExcelError::new_num(),
3947            ))),
3948        }
3949    }
3950}
3951
3952/// Returns the chi-square probability for `x` with the specified degrees of freedom.
3953///
3954/// Use `CHISQ.DIST` in cumulative mode for left-tail probability or density mode for the PDF.
3955///
3956/// # Remarks
3957/// - Set `cumulative` to non-zero for CDF mode, or `0` for PDF mode.
3958/// - Requires `x >= 0` and `deg_freedom >= 1`.
3959/// - Returns `#NUM!` for negative `x` or invalid degrees of freedom.
3960/// - Invalid numeric coercions propagate as spreadsheet errors.
3961///
3962/// # Examples
3963///
3964/// ```yaml,sandbox
3965/// title: "Chi-square CDF at zero"
3966/// formula: "=CHISQ.DIST(0,4,TRUE)"
3967/// expected: 0
3968/// ```
3969///
3970/// ```yaml,sandbox
3971/// title: "Chi-square PDF example"
3972/// formula: "=CHISQ.DIST(2,2,FALSE)"
3973/// expected: 0.18393972058572117
3974/// ```
3975#[derive(Debug)]
3976pub struct ChisqDistFn;
3977/// [formualizer-docgen:schema:start]
3978/// Name: CHISQ.DIST
3979/// Type: ChisqDistFn
3980/// Min args: 3
3981/// Max args: 3
3982/// Variadic: false
3983/// Signature: CHISQ.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
3984/// 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}
3985/// Caps: PURE
3986/// [formualizer-docgen:schema:end]
3987impl Function for ChisqDistFn {
3988    func_caps!(PURE);
3989    fn name(&self) -> &'static str {
3990        "CHISQ.DIST"
3991    }
3992    fn min_args(&self) -> usize {
3993        3
3994    }
3995    fn arg_schema(&self) -> &'static [ArgSchema] {
3996        use std::sync::LazyLock;
3997        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
3998            vec![
3999                ArgSchema::number_lenient_scalar(),
4000                ArgSchema::number_lenient_scalar(),
4001                ArgSchema::number_lenient_scalar(),
4002            ]
4003        });
4004        &SCHEMA[..]
4005    }
4006    fn eval<'a, 'b, 'c>(
4007        &self,
4008        args: &'c [ArgumentHandle<'a, 'b>],
4009        _ctx: &dyn FunctionContext<'b>,
4010    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4011        let x = coerce_num(&scalar_like_value(&args[0])?)?;
4012        let df = coerce_num(&scalar_like_value(&args[1])?)?;
4013        let cumulative = coerce_num(&scalar_like_value(&args[2])?)? != 0.0;
4014
4015        if df < 1.0 || x < 0.0 {
4016            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4017                ExcelError::new_num(),
4018            )));
4019        }
4020
4021        let result = if cumulative {
4022            chisq_cdf(x, df)
4023        } else {
4024            chisq_pdf(x, df)
4025        };
4026        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
4027            result,
4028        )))
4029    }
4030}
4031
4032/// Returns the chi-square value whose left-tail probability is `probability`.
4033///
4034/// `CHISQ.INV` inverts `CHISQ.DIST(x, deg_freedom, TRUE)`.
4035///
4036/// # Remarks
4037/// - `probability` must be strictly between `0` and `1`.
4038/// - `deg_freedom` must be at least `1`.
4039/// - Returns `#NUM!` when arguments are outside valid ranges.
4040/// - Invalid numeric coercions propagate as spreadsheet errors.
4041///
4042/// # Examples
4043///
4044/// ```yaml,sandbox
4045/// title: "Median chi-square quantile for df=2"
4046/// formula: "=CHISQ.INV(0.5,2)"
4047/// expected: 1.3862943611198906
4048/// ```
4049///
4050/// ```yaml,sandbox
4051/// title: "Upper quantile for df=10"
4052/// formula: "=CHISQ.INV(0.95,10)"
4053/// expected: 18.307038053275146
4054/// ```
4055#[derive(Debug)]
4056pub struct ChisqInvFn;
4057/// [formualizer-docgen:schema:start]
4058/// Name: CHISQ.INV
4059/// Type: ChisqInvFn
4060/// Min args: 2
4061/// Max args: 2
4062/// Variadic: false
4063/// Signature: CHISQ.INV(arg1: number@scalar, arg2: number@scalar)
4064/// 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}
4065/// Caps: PURE
4066/// [formualizer-docgen:schema:end]
4067impl Function for ChisqInvFn {
4068    func_caps!(PURE);
4069    fn name(&self) -> &'static str {
4070        "CHISQ.INV"
4071    }
4072    fn min_args(&self) -> usize {
4073        2
4074    }
4075    fn arg_schema(&self) -> &'static [ArgSchema] {
4076        use std::sync::LazyLock;
4077        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
4078            vec![
4079                ArgSchema::number_lenient_scalar(),
4080                ArgSchema::number_lenient_scalar(),
4081            ]
4082        });
4083        &SCHEMA[..]
4084    }
4085    fn eval<'a, 'b, 'c>(
4086        &self,
4087        args: &'c [ArgumentHandle<'a, 'b>],
4088        _ctx: &dyn FunctionContext<'b>,
4089    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4090        let p = coerce_num(&scalar_like_value(&args[0])?)?;
4091        let df = coerce_num(&scalar_like_value(&args[1])?)?;
4092
4093        if df < 1.0 {
4094            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4095                ExcelError::new_num(),
4096            )));
4097        }
4098
4099        match chisq_inv(p, df) {
4100            Some(result) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
4101                result,
4102            ))),
4103            None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4104                ExcelError::new_num(),
4105            ))),
4106        }
4107    }
4108}
4109
4110/// Returns the F-distribution probability for `x` with numerator and denominator degrees of freedom.
4111///
4112/// Use `F.DIST` for left-tail cumulative probabilities or density values in variance-ratio tests.
4113///
4114/// # Remarks
4115/// - Set `cumulative` to non-zero for CDF mode, or `0` for PDF mode.
4116/// - Requires `x >= 0`, `deg_freedom1 >= 1`, and `deg_freedom2 >= 1`.
4117/// - Returns `#NUM!` when any domain constraint is violated.
4118/// - Invalid numeric coercions propagate as spreadsheet errors.
4119///
4120/// # Examples
4121///
4122/// ```yaml,sandbox
4123/// title: "F CDF with symmetric 2 and 2 degrees of freedom"
4124/// formula: "=F.DIST(1,2,2,TRUE)"
4125/// expected: 0.5
4126/// ```
4127///
4128/// ```yaml,sandbox
4129/// title: "F PDF with symmetric 2 and 2 degrees of freedom"
4130/// formula: "=F.DIST(1,2,2,FALSE)"
4131/// expected: 0.25
4132/// ```
4133#[derive(Debug)]
4134pub struct FDistFn;
4135/// [formualizer-docgen:schema:start]
4136/// Name: F.DIST
4137/// Type: FDistFn
4138/// Min args: 4
4139/// Max args: 4
4140/// Variadic: false
4141/// Signature: F.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar)
4142/// 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=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
4143/// Caps: PURE
4144/// [formualizer-docgen:schema:end]
4145impl Function for FDistFn {
4146    func_caps!(PURE);
4147    fn name(&self) -> &'static str {
4148        "F.DIST"
4149    }
4150    fn min_args(&self) -> usize {
4151        4
4152    }
4153    fn arg_schema(&self) -> &'static [ArgSchema] {
4154        use std::sync::LazyLock;
4155        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
4156            vec![
4157                ArgSchema::number_lenient_scalar(),
4158                ArgSchema::number_lenient_scalar(),
4159                ArgSchema::number_lenient_scalar(),
4160                ArgSchema::number_lenient_scalar(),
4161            ]
4162        });
4163        &SCHEMA[..]
4164    }
4165    fn eval<'a, 'b, 'c>(
4166        &self,
4167        args: &'c [ArgumentHandle<'a, 'b>],
4168        _ctx: &dyn FunctionContext<'b>,
4169    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4170        let x = coerce_num(&scalar_like_value(&args[0])?)?;
4171        let d1 = coerce_num(&scalar_like_value(&args[1])?)?;
4172        let d2 = coerce_num(&scalar_like_value(&args[2])?)?;
4173        let cumulative = coerce_num(&scalar_like_value(&args[3])?)? != 0.0;
4174
4175        if d1 < 1.0 || d2 < 1.0 || x < 0.0 {
4176            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4177                ExcelError::new_num(),
4178            )));
4179        }
4180
4181        let result = if cumulative {
4182            f_cdf(x, d1, d2)
4183        } else {
4184            f_pdf(x, d1, d2)
4185        };
4186        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
4187            result,
4188        )))
4189    }
4190}
4191
4192/// Returns the F value whose left-tail probability equals `probability`.
4193///
4194/// `F.INV` inverts `F.DIST(x, deg_freedom1, deg_freedom2, TRUE)`.
4195///
4196/// # Remarks
4197/// - `probability` must be strictly between `0` and `1`.
4198/// - `deg_freedom1` and `deg_freedom2` must each be at least `1`.
4199/// - Returns `#NUM!` for invalid probability or degree-of-freedom arguments.
4200/// - Invalid numeric coercions propagate as spreadsheet errors.
4201///
4202/// # Examples
4203///
4204/// ```yaml,sandbox
4205/// title: "Median F quantile with symmetric 2 and 2 degrees of freedom"
4206/// formula: "=F.INV(0.5,2,2)"
4207/// expected: 1
4208/// ```
4209///
4210/// ```yaml,sandbox
4211/// title: "Upper-tail F critical value"
4212/// formula: "=F.INV(0.95,5,10)"
4213/// expected: 3.3258345304130112
4214/// ```
4215#[derive(Debug)]
4216pub struct FInvFn;
4217/// [formualizer-docgen:schema:start]
4218/// Name: F.INV
4219/// Type: FInvFn
4220/// Min args: 3
4221/// Max args: 3
4222/// Variadic: false
4223/// Signature: F.INV(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
4224/// 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}
4225/// Caps: PURE
4226/// [formualizer-docgen:schema:end]
4227impl Function for FInvFn {
4228    func_caps!(PURE);
4229    fn name(&self) -> &'static str {
4230        "F.INV"
4231    }
4232    fn min_args(&self) -> usize {
4233        3
4234    }
4235    fn arg_schema(&self) -> &'static [ArgSchema] {
4236        use std::sync::LazyLock;
4237        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
4238            vec![
4239                ArgSchema::number_lenient_scalar(),
4240                ArgSchema::number_lenient_scalar(),
4241                ArgSchema::number_lenient_scalar(),
4242            ]
4243        });
4244        &SCHEMA[..]
4245    }
4246    fn eval<'a, 'b, 'c>(
4247        &self,
4248        args: &'c [ArgumentHandle<'a, 'b>],
4249        _ctx: &dyn FunctionContext<'b>,
4250    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4251        let p = coerce_num(&scalar_like_value(&args[0])?)?;
4252        let d1 = coerce_num(&scalar_like_value(&args[1])?)?;
4253        let d2 = coerce_num(&scalar_like_value(&args[2])?)?;
4254
4255        if d1 < 1.0 || d2 < 1.0 {
4256            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4257                ExcelError::new_num(),
4258            )));
4259        }
4260
4261        match f_inv(p, d1, d2) {
4262            Some(result) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
4263                result,
4264            ))),
4265            None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4266                ExcelError::new_num(),
4267            ))),
4268        }
4269    }
4270}
4271
4272/// Returns the z-score of `x` relative to a mean and standard deviation.
4273///
4274/// `STANDARDIZE` computes `(x - mean) / standard_dev`.
4275///
4276/// # Remarks
4277/// - `standard_dev` must be strictly greater than `0`.
4278/// - Returns `#NUM!` when `standard_dev <= 0`.
4279/// - Positive output means `x` is above the mean; negative output means below.
4280/// - Invalid numeric coercions propagate as spreadsheet errors.
4281///
4282/// # Examples
4283///
4284/// ```yaml,sandbox
4285/// title: "One standard deviation above the mean"
4286/// formula: "=STANDARDIZE(42,40,2)"
4287/// expected: 1
4288/// ```
4289///
4290/// ```yaml,sandbox
4291/// title: "Exactly at the mean"
4292/// formula: "=STANDARDIZE(100,100,10)"
4293/// expected: 0
4294/// ```
4295#[derive(Debug)]
4296pub struct StandardizeFn;
4297/// [formualizer-docgen:schema:start]
4298/// Name: STANDARDIZE
4299/// Type: StandardizeFn
4300/// Min args: 3
4301/// Max args: 3
4302/// Variadic: false
4303/// Signature: STANDARDIZE(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
4304/// 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}
4305/// Caps: PURE
4306/// [formualizer-docgen:schema:end]
4307impl Function for StandardizeFn {
4308    func_caps!(PURE);
4309    fn name(&self) -> &'static str {
4310        "STANDARDIZE"
4311    }
4312    fn min_args(&self) -> usize {
4313        3
4314    }
4315    fn arg_schema(&self) -> &'static [ArgSchema] {
4316        use std::sync::LazyLock;
4317        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
4318            vec![
4319                ArgSchema::number_lenient_scalar(),
4320                ArgSchema::number_lenient_scalar(),
4321                ArgSchema::number_lenient_scalar(),
4322            ]
4323        });
4324        &SCHEMA[..]
4325    }
4326    fn eval<'a, 'b, 'c>(
4327        &self,
4328        args: &'c [ArgumentHandle<'a, 'b>],
4329        _ctx: &dyn FunctionContext<'b>,
4330    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4331        let x = coerce_num(&scalar_like_value(&args[0])?)?;
4332        let mean = coerce_num(&scalar_like_value(&args[1])?)?;
4333        let std_dev = coerce_num(&scalar_like_value(&args[2])?)?;
4334
4335        if std_dev <= 0.0 {
4336            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4337                ExcelError::new_num(),
4338            )));
4339        }
4340
4341        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
4342            (x - mean) / std_dev,
4343        )))
4344    }
4345}
4346
4347/// Helper: Factorial function
4348fn factorial(n: i64) -> f64 {
4349    if n < 0 {
4350        return f64::NAN;
4351    }
4352    if n <= 1 {
4353        return 1.0;
4354    }
4355    // For large n, use gamma function: n! = Gamma(n+1)
4356    if n > 20 {
4357        return ln_gamma((n + 1) as f64).exp();
4358    }
4359    let mut result = 1.0;
4360    for i in 2..=n {
4361        result *= i as f64;
4362    }
4363    result
4364}
4365
4366/// Helper: Log of binomial coefficient (n choose k)
4367fn ln_binom(n: i64, k: i64) -> f64 {
4368    if k < 0 || k > n {
4369        return f64::NEG_INFINITY;
4370    }
4371    if k == 0 || k == n {
4372        return 0.0;
4373    }
4374    ln_gamma((n + 1) as f64) - ln_gamma((k + 1) as f64) - ln_gamma((n - k + 1) as f64)
4375}
4376
4377/// Returns the binomial probability for a count of successes across independent trials.
4378///
4379/// Use `BINOM.DIST` to evaluate either exact-success probability (PMF) or cumulative probability
4380/// up to a success count (CDF).
4381///
4382/// # Remarks
4383/// - `number_s` and `trials` are truncated to integers.
4384/// - Requires `0 <= number_s <= trials`, `trials >= 0`, and `0 <= probability_s <= 1`.
4385/// - Set `cumulative` to non-zero for CDF mode, or `0` for PMF mode.
4386/// - Returns `#NUM!` for invalid count or probability ranges.
4387///
4388/// # Examples
4389///
4390/// ```yaml,sandbox
4391/// title: "Binomial PMF for exactly 3 successes"
4392/// formula: "=BINOM.DIST(3,10,0.5,FALSE)"
4393/// expected: 0.1171875
4394/// ```
4395///
4396/// ```yaml,sandbox
4397/// title: "Binomial CDF for at most 3 successes"
4398/// formula: "=BINOM.DIST(3,10,0.5,TRUE)"
4399/// expected: 0.171875
4400/// ```
4401#[derive(Debug)]
4402pub struct BinomDistFn;
4403/// [formualizer-docgen:schema:start]
4404/// Name: BINOM.DIST
4405/// Type: BinomDistFn
4406/// Min args: 4
4407/// Max args: 4
4408/// Variadic: false
4409/// Signature: BINOM.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar)
4410/// 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=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
4411/// Caps: PURE
4412/// [formualizer-docgen:schema:end]
4413impl Function for BinomDistFn {
4414    func_caps!(PURE);
4415    fn name(&self) -> &'static str {
4416        "BINOM.DIST"
4417    }
4418    fn min_args(&self) -> usize {
4419        4
4420    }
4421    fn arg_schema(&self) -> &'static [ArgSchema] {
4422        use std::sync::LazyLock;
4423        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
4424            vec![
4425                ArgSchema::number_lenient_scalar(),
4426                ArgSchema::number_lenient_scalar(),
4427                ArgSchema::number_lenient_scalar(),
4428                ArgSchema::number_lenient_scalar(),
4429            ]
4430        });
4431        &SCHEMA[..]
4432    }
4433    fn eval<'a, 'b, 'c>(
4434        &self,
4435        args: &'c [ArgumentHandle<'a, 'b>],
4436        _ctx: &dyn FunctionContext<'b>,
4437    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4438        let k = coerce_num(&scalar_like_value(&args[0])?)?.trunc() as i64;
4439        let n = coerce_num(&scalar_like_value(&args[1])?)?.trunc() as i64;
4440        let p = coerce_num(&scalar_like_value(&args[2])?)?;
4441        let cumulative = coerce_num(&scalar_like_value(&args[3])?)? != 0.0;
4442
4443        if n < 0 || k < 0 || k > n || !(0.0..=1.0).contains(&p) {
4444            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4445                ExcelError::new_num(),
4446            )));
4447        }
4448
4449        let result = if cumulative {
4450            // CDF: sum from i=0 to k of P(X=i)
4451            let mut sum = 0.0;
4452            for i in 0..=k {
4453                let ln_prob =
4454                    ln_binom(n, i) + (i as f64) * p.ln() + ((n - i) as f64) * (1.0 - p).ln();
4455                sum += ln_prob.exp();
4456            }
4457            sum
4458        } else {
4459            // PMF: P(X=k)
4460            let ln_prob = ln_binom(n, k) + (k as f64) * p.ln() + ((n - k) as f64) * (1.0 - p).ln();
4461            ln_prob.exp()
4462        };
4463
4464        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
4465            result,
4466        )))
4467    }
4468}
4469
4470/// Returns the Poisson probability for event count `x` at average rate `mean`.
4471///
4472/// `POISSON.DIST` supports exact-count mode (PMF) and cumulative mode (CDF).
4473///
4474/// # Remarks
4475/// - `x` is truncated to an integer and must be at least `0`.
4476/// - `mean` must be non-negative.
4477/// - Set `cumulative` to non-zero for CDF mode, or `0` for PMF mode.
4478/// - Returns `#NUM!` for negative counts or negative mean values.
4479///
4480/// # Examples
4481///
4482/// ```yaml,sandbox
4483/// title: "Poisson PMF for zero events"
4484/// formula: "=POISSON.DIST(0,2,FALSE)"
4485/// expected: 0.1353352832366127
4486/// ```
4487///
4488/// ```yaml,sandbox
4489/// title: "Poisson CDF up to two events"
4490/// formula: "=POISSON.DIST(2,2,TRUE)"
4491/// expected: 0.6766764161830634
4492/// ```
4493#[derive(Debug)]
4494pub struct PoissonDistFn;
4495/// [formualizer-docgen:schema:start]
4496/// Name: POISSON.DIST
4497/// Type: PoissonDistFn
4498/// Min args: 3
4499/// Max args: 3
4500/// Variadic: false
4501/// Signature: POISSON.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
4502/// 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}
4503/// Caps: PURE
4504/// [formualizer-docgen:schema:end]
4505impl Function for PoissonDistFn {
4506    func_caps!(PURE);
4507    fn name(&self) -> &'static str {
4508        "POISSON.DIST"
4509    }
4510    fn min_args(&self) -> usize {
4511        3
4512    }
4513    fn arg_schema(&self) -> &'static [ArgSchema] {
4514        use std::sync::LazyLock;
4515        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
4516            vec![
4517                ArgSchema::number_lenient_scalar(),
4518                ArgSchema::number_lenient_scalar(),
4519                ArgSchema::number_lenient_scalar(),
4520            ]
4521        });
4522        &SCHEMA[..]
4523    }
4524    fn eval<'a, 'b, 'c>(
4525        &self,
4526        args: &'c [ArgumentHandle<'a, 'b>],
4527        _ctx: &dyn FunctionContext<'b>,
4528    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4529        let k = coerce_num(&scalar_like_value(&args[0])?)?.trunc() as i64;
4530        let lambda = coerce_num(&scalar_like_value(&args[1])?)?;
4531        let cumulative = coerce_num(&scalar_like_value(&args[2])?)? != 0.0;
4532
4533        if k < 0 || lambda < 0.0 {
4534            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4535                ExcelError::new_num(),
4536            )));
4537        }
4538
4539        let result = if cumulative {
4540            // CDF: sum from i=0 to k of P(X=i) = 1 - Q(k+1, lambda)
4541            // Using the regularized incomplete gamma function
4542            1.0 - gamma_p((k + 1) as f64, lambda)
4543        } else {
4544            // PMF: P(X=k) = lambda^k * e^(-lambda) / k!
4545            // Use log to avoid overflow
4546            let ln_prob = (k as f64) * lambda.ln() - lambda - ln_gamma((k + 1) as f64);
4547            ln_prob.exp()
4548        };
4549
4550        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
4551            result,
4552        )))
4553    }
4554}
4555
4556/// Returns the exponential-distribution probability at `x` for rate `lambda`.
4557///
4558/// Use `EXPON.DIST` for waiting-time models where events occur with a constant hazard rate.
4559///
4560/// # Remarks
4561/// - Requires `x >= 0` and `lambda > 0`.
4562/// - Set `cumulative` to non-zero for CDF mode, or `0` for PDF mode.
4563/// - Returns `#NUM!` when inputs violate domain requirements.
4564/// - Invalid numeric coercions propagate as spreadsheet errors.
4565///
4566/// # Examples
4567///
4568/// ```yaml,sandbox
4569/// title: "Exponential CDF"
4570/// formula: "=EXPON.DIST(1,1,TRUE)"
4571/// expected: 0.6321205588285577
4572/// ```
4573///
4574/// ```yaml,sandbox
4575/// title: "Exponential PDF"
4576/// formula: "=EXPON.DIST(1,1,FALSE)"
4577/// expected: 0.36787944117144233
4578/// ```
4579#[derive(Debug)]
4580pub struct ExponDistFn;
4581/// [formualizer-docgen:schema:start]
4582/// Name: EXPON.DIST
4583/// Type: ExponDistFn
4584/// Min args: 3
4585/// Max args: 3
4586/// Variadic: false
4587/// Signature: EXPON.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
4588/// 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}
4589/// Caps: PURE
4590/// [formualizer-docgen:schema:end]
4591impl Function for ExponDistFn {
4592    func_caps!(PURE);
4593    fn name(&self) -> &'static str {
4594        "EXPON.DIST"
4595    }
4596    fn min_args(&self) -> usize {
4597        3
4598    }
4599    fn arg_schema(&self) -> &'static [ArgSchema] {
4600        use std::sync::LazyLock;
4601        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
4602            vec![
4603                ArgSchema::number_lenient_scalar(),
4604                ArgSchema::number_lenient_scalar(),
4605                ArgSchema::number_lenient_scalar(),
4606            ]
4607        });
4608        &SCHEMA[..]
4609    }
4610    fn eval<'a, 'b, 'c>(
4611        &self,
4612        args: &'c [ArgumentHandle<'a, 'b>],
4613        _ctx: &dyn FunctionContext<'b>,
4614    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4615        let x = coerce_num(&scalar_like_value(&args[0])?)?;
4616        let lambda = coerce_num(&scalar_like_value(&args[1])?)?;
4617        let cumulative = coerce_num(&scalar_like_value(&args[2])?)? != 0.0;
4618
4619        if x < 0.0 || lambda <= 0.0 {
4620            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4621                ExcelError::new_num(),
4622            )));
4623        }
4624
4625        let result = if cumulative {
4626            // CDF: 1 - e^(-lambda*x)
4627            1.0 - (-lambda * x).exp()
4628        } else {
4629            // PDF: lambda * e^(-lambda*x)
4630            lambda * (-lambda * x).exp()
4631        };
4632
4633        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
4634            result,
4635        )))
4636    }
4637}
4638
4639/// Returns the gamma-distribution probability at `x` for shape `alpha` and scale `beta`.
4640///
4641/// `GAMMA.DIST` supports cumulative and density modes for right-skewed waiting-time models.
4642///
4643/// # Remarks
4644/// - Requires `x >= 0`, `alpha > 0`, and `beta > 0`.
4645/// - Set `cumulative` to non-zero for CDF mode, or `0` for PDF mode.
4646/// - Returns `#NUM!` when any parameter is outside its valid range.
4647/// - Invalid numeric coercions propagate as spreadsheet errors.
4648///
4649/// # Examples
4650///
4651/// ```yaml,sandbox
4652/// title: "Gamma CDF with alpha=1 and beta=2"
4653/// formula: "=GAMMA.DIST(2,1,2,TRUE)"
4654/// expected: 0.6321205588285577
4655/// ```
4656///
4657/// ```yaml,sandbox
4658/// title: "Gamma PDF with alpha=1 and beta=2"
4659/// formula: "=GAMMA.DIST(2,1,2,FALSE)"
4660/// expected: 0.18393972058572117
4661/// ```
4662#[derive(Debug)]
4663pub struct GammaDistFn;
4664/// [formualizer-docgen:schema:start]
4665/// Name: GAMMA.DIST
4666/// Type: GammaDistFn
4667/// Min args: 4
4668/// Max args: 4
4669/// Variadic: false
4670/// Signature: GAMMA.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar)
4671/// 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=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
4672/// Caps: PURE
4673/// [formualizer-docgen:schema:end]
4674impl Function for GammaDistFn {
4675    func_caps!(PURE);
4676    fn name(&self) -> &'static str {
4677        "GAMMA.DIST"
4678    }
4679    fn min_args(&self) -> usize {
4680        4
4681    }
4682    fn arg_schema(&self) -> &'static [ArgSchema] {
4683        use std::sync::LazyLock;
4684        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
4685            vec![
4686                ArgSchema::number_lenient_scalar(),
4687                ArgSchema::number_lenient_scalar(),
4688                ArgSchema::number_lenient_scalar(),
4689                ArgSchema::number_lenient_scalar(),
4690            ]
4691        });
4692        &SCHEMA[..]
4693    }
4694    fn eval<'a, 'b, 'c>(
4695        &self,
4696        args: &'c [ArgumentHandle<'a, 'b>],
4697        _ctx: &dyn FunctionContext<'b>,
4698    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4699        let x = coerce_num(&scalar_like_value(&args[0])?)?;
4700        let alpha = coerce_num(&scalar_like_value(&args[1])?)?; // shape
4701        let beta = coerce_num(&scalar_like_value(&args[2])?)?; // scale
4702        let cumulative = coerce_num(&scalar_like_value(&args[3])?)? != 0.0;
4703
4704        if x < 0.0 || alpha <= 0.0 || beta <= 0.0 {
4705            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4706                ExcelError::new_num(),
4707            )));
4708        }
4709
4710        let result = if cumulative {
4711            // CDF: P(alpha, x/beta) where P is the regularized lower incomplete gamma
4712            gamma_p(alpha, x / beta)
4713        } else {
4714            // PDF: x^(alpha-1) * e^(-x/beta) / (beta^alpha * Gamma(alpha))
4715            let ln_pdf = (alpha - 1.0) * x.ln() - x / beta - alpha * beta.ln() - ln_gamma(alpha);
4716            ln_pdf.exp()
4717        };
4718
4719        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
4720            result,
4721        )))
4722    }
4723}
4724
4725/// Returns the Weibull-distribution probability at `x` for shape `alpha` and scale `beta`.
4726///
4727/// `WEIBULL.DIST` is commonly used for reliability and time-to-failure analysis.
4728///
4729/// # Remarks
4730/// - Requires `x >= 0`, `alpha > 0`, and `beta > 0`.
4731/// - Set `cumulative` to non-zero for CDF mode, or `0` for PDF mode.
4732/// - Returns `#NUM!` when parameters fall outside valid ranges.
4733/// - In PDF mode at `x = 0`, behavior follows the Weibull shape-specific limit.
4734///
4735/// # Examples
4736///
4737/// ```yaml,sandbox
4738/// title: "Weibull CDF with alpha=1 and beta=2"
4739/// formula: "=WEIBULL.DIST(2,1,2,TRUE)"
4740/// expected: 0.6321205588285577
4741/// ```
4742///
4743/// ```yaml,sandbox
4744/// title: "Weibull PDF with alpha=1 and beta=2"
4745/// formula: "=WEIBULL.DIST(2,1,2,FALSE)"
4746/// expected: 0.18393972058572117
4747/// ```
4748#[derive(Debug)]
4749pub struct WeibullDistFn;
4750/// [formualizer-docgen:schema:start]
4751/// Name: WEIBULL.DIST
4752/// Type: WeibullDistFn
4753/// Min args: 4
4754/// Max args: 4
4755/// Variadic: false
4756/// Signature: WEIBULL.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar)
4757/// 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=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
4758/// Caps: PURE
4759/// [formualizer-docgen:schema:end]
4760impl Function for WeibullDistFn {
4761    func_caps!(PURE);
4762    fn name(&self) -> &'static str {
4763        "WEIBULL.DIST"
4764    }
4765    fn min_args(&self) -> usize {
4766        4
4767    }
4768    fn arg_schema(&self) -> &'static [ArgSchema] {
4769        use std::sync::LazyLock;
4770        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
4771            vec![
4772                ArgSchema::number_lenient_scalar(),
4773                ArgSchema::number_lenient_scalar(),
4774                ArgSchema::number_lenient_scalar(),
4775                ArgSchema::number_lenient_scalar(),
4776            ]
4777        });
4778        &SCHEMA[..]
4779    }
4780    fn eval<'a, 'b, 'c>(
4781        &self,
4782        args: &'c [ArgumentHandle<'a, 'b>],
4783        _ctx: &dyn FunctionContext<'b>,
4784    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4785        let x = coerce_num(&scalar_like_value(&args[0])?)?;
4786        let alpha = coerce_num(&scalar_like_value(&args[1])?)?; // shape
4787        let beta = coerce_num(&scalar_like_value(&args[2])?)?; // scale
4788        let cumulative = coerce_num(&scalar_like_value(&args[3])?)? != 0.0;
4789
4790        if x < 0.0 || alpha <= 0.0 || beta <= 0.0 {
4791            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4792                ExcelError::new_num(),
4793            )));
4794        }
4795
4796        let result = if cumulative {
4797            // CDF: 1 - e^(-(x/beta)^alpha)
4798            1.0 - (-(x / beta).powf(alpha)).exp()
4799        } else {
4800            // PDF: (alpha/beta) * (x/beta)^(alpha-1) * e^(-(x/beta)^alpha)
4801            if x == 0.0 {
4802                if alpha < 1.0 {
4803                    f64::INFINITY
4804                } else if alpha == 1.0 {
4805                    alpha / beta
4806                } else {
4807                    0.0
4808                }
4809            } else {
4810                (alpha / beta) * (x / beta).powf(alpha - 1.0) * (-(x / beta).powf(alpha)).exp()
4811            }
4812        };
4813
4814        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
4815            result,
4816        )))
4817    }
4818}
4819
4820/// Returns the beta-distribution probability for `x`, with optional lower/upper bounds.
4821///
4822/// `BETA.DIST` can evaluate either the cumulative probability or density on `[A, B]` (default
4823/// `[0, 1]`).
4824///
4825/// # Remarks
4826/// - Requires `alpha > 0`, `beta > 0`, and `A < B`.
4827/// - `x` must lie within the inclusive interval `[A, B]`.
4828/// - Set `cumulative` to non-zero for CDF mode, or `0` for PDF mode.
4829/// - Returns `#NUM!` for invalid bounds, parameters, or out-of-range `x`.
4830///
4831/// # Examples
4832///
4833/// ```yaml,sandbox
4834/// title: "Uniform beta CDF on [0,1]"
4835/// formula: "=BETA.DIST(0.3,1,1,TRUE)"
4836/// expected: 0.3
4837/// ```
4838///
4839/// ```yaml,sandbox
4840/// title: "Uniform beta PDF on [0,1]"
4841/// formula: "=BETA.DIST(0.3,1,1,FALSE)"
4842/// expected: 1
4843/// ```
4844#[derive(Debug)]
4845pub struct BetaDistFn;
4846/// [formualizer-docgen:schema:start]
4847/// Name: BETA.DIST
4848/// Type: BetaDistFn
4849/// Min args: 4
4850/// Max args: variadic
4851/// Variadic: true
4852/// Signature: BETA.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar, arg5: number@scalar, arg6...: number@scalar)
4853/// 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=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg5{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg6{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
4854/// Caps: PURE
4855/// [formualizer-docgen:schema:end]
4856impl Function for BetaDistFn {
4857    func_caps!(PURE);
4858    fn name(&self) -> &'static str {
4859        "BETA.DIST"
4860    }
4861    fn min_args(&self) -> usize {
4862        4
4863    }
4864    fn variadic(&self) -> bool {
4865        true
4866    }
4867    fn arg_schema(&self) -> &'static [ArgSchema] {
4868        use std::sync::LazyLock;
4869        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
4870            vec![
4871                ArgSchema::number_lenient_scalar(),
4872                ArgSchema::number_lenient_scalar(),
4873                ArgSchema::number_lenient_scalar(),
4874                ArgSchema::number_lenient_scalar(),
4875                ArgSchema::number_lenient_scalar(),
4876                ArgSchema::number_lenient_scalar(),
4877            ]
4878        });
4879        &SCHEMA[..]
4880    }
4881    fn eval<'a, 'b, 'c>(
4882        &self,
4883        args: &'c [ArgumentHandle<'a, 'b>],
4884        _ctx: &dyn FunctionContext<'b>,
4885    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4886        let x = coerce_num(&scalar_like_value(&args[0])?)?;
4887        let alpha = coerce_num(&scalar_like_value(&args[1])?)?;
4888        let beta_param = coerce_num(&scalar_like_value(&args[2])?)?;
4889        let cumulative = coerce_num(&scalar_like_value(&args[3])?)? != 0.0;
4890
4891        // Optional bounds A and B (default 0 and 1)
4892        let a = if args.len() > 4 {
4893            coerce_num(&scalar_like_value(&args[4])?)?
4894        } else {
4895            0.0
4896        };
4897        let b = if args.len() > 5 {
4898            coerce_num(&scalar_like_value(&args[5])?)?
4899        } else {
4900            1.0
4901        };
4902
4903        if alpha <= 0.0 || beta_param <= 0.0 || a >= b {
4904            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4905                ExcelError::new_num(),
4906            )));
4907        }
4908
4909        // x must be in [a, b]
4910        if x < a || x > b {
4911            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4912                ExcelError::new_num(),
4913            )));
4914        }
4915
4916        // Transform x to standard [0,1] interval
4917        let x_std = (x - a) / (b - a);
4918
4919        let result = if cumulative {
4920            // CDF: I_x(alpha, beta) - regularized incomplete beta function
4921            beta_i(x_std, alpha, beta_param)
4922        } else {
4923            // PDF: (x-A)^(alpha-1) * (B-x)^(beta-1) / ((B-A)^(alpha+beta-1) * B(alpha, beta))
4924            let ln_beta = ln_gamma(alpha) + ln_gamma(beta_param) - ln_gamma(alpha + beta_param);
4925            let scale = b - a;
4926            if (x_std == 0.0 && alpha < 1.0) || (x_std == 1.0 && beta_param < 1.0) {
4927                f64::INFINITY
4928            } else if x_std == 0.0 {
4929                if alpha == 1.0 {
4930                    (1.0 - x_std).powf(beta_param - 1.0) / (scale * ln_beta.exp())
4931                } else {
4932                    0.0
4933                }
4934            } else if x_std == 1.0 {
4935                if beta_param == 1.0 {
4936                    x_std.powf(alpha - 1.0) / (scale * ln_beta.exp())
4937                } else {
4938                    0.0
4939                }
4940            } else {
4941                let ln_pdf =
4942                    (alpha - 1.0) * x_std.ln() + (beta_param - 1.0) * (1.0 - x_std).ln() - ln_beta;
4943                ln_pdf.exp() / scale
4944            }
4945        };
4946
4947        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
4948            result,
4949        )))
4950    }
4951}
4952
4953/// Returns negative-binomial probabilities for failures observed before a target success count.
4954///
4955/// `NEGBINOM.DIST` supports exact-failure mode (PMF) and cumulative mode (CDF).
4956///
4957/// # Remarks
4958/// - `number_f` is truncated and must be `>= 0`.
4959/// - `number_s` is truncated and must be `>= 1`.
4960/// - `probability_s` must satisfy `0 < p < 1`.
4961/// - Returns `#NUM!` when counts or probability are outside valid ranges.
4962///
4963/// # Examples
4964///
4965/// ```yaml,sandbox
4966/// title: "Negative binomial PMF"
4967/// formula: "=NEGBINOM.DIST(2,1,0.5,FALSE)"
4968/// expected: 0.125
4969/// ```
4970///
4971/// ```yaml,sandbox
4972/// title: "Negative binomial CDF"
4973/// formula: "=NEGBINOM.DIST(2,1,0.5,TRUE)"
4974/// expected: 0.875
4975/// ```
4976#[derive(Debug)]
4977pub struct NegbinomDistFn;
4978/// [formualizer-docgen:schema:start]
4979/// Name: NEGBINOM.DIST
4980/// Type: NegbinomDistFn
4981/// Min args: 4
4982/// Max args: 4
4983/// Variadic: false
4984/// Signature: NEGBINOM.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar)
4985/// 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=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
4986/// Caps: PURE
4987/// [formualizer-docgen:schema:end]
4988impl Function for NegbinomDistFn {
4989    func_caps!(PURE);
4990    fn name(&self) -> &'static str {
4991        "NEGBINOM.DIST"
4992    }
4993    fn min_args(&self) -> usize {
4994        4
4995    }
4996    fn arg_schema(&self) -> &'static [ArgSchema] {
4997        use std::sync::LazyLock;
4998        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
4999            vec![
5000                ArgSchema::number_lenient_scalar(),
5001                ArgSchema::number_lenient_scalar(),
5002                ArgSchema::number_lenient_scalar(),
5003                ArgSchema::number_lenient_scalar(),
5004            ]
5005        });
5006        &SCHEMA[..]
5007    }
5008    fn eval<'a, 'b, 'c>(
5009        &self,
5010        args: &'c [ArgumentHandle<'a, 'b>],
5011        _ctx: &dyn FunctionContext<'b>,
5012    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5013        let number_f = coerce_num(&scalar_like_value(&args[0])?)?.trunc() as i64; // number of failures
5014        let number_s = coerce_num(&scalar_like_value(&args[1])?)?.trunc() as i64; // number of successes
5015        let prob_s = coerce_num(&scalar_like_value(&args[2])?)?; // probability of success
5016        let cumulative = coerce_num(&scalar_like_value(&args[3])?)? != 0.0;
5017
5018        if number_f < 0 || number_s < 1 || prob_s <= 0.0 || prob_s >= 1.0 {
5019            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5020                ExcelError::new_num(),
5021            )));
5022        }
5023
5024        let result = if cumulative {
5025            // CDF: sum from i=0 to number_f of P(X=i)
5026            // This is equivalent to I_{prob_s}(number_s, number_f + 1) using regularized beta
5027            beta_i(prob_s, number_s as f64, (number_f + 1) as f64)
5028        } else {
5029            // PMF: C(number_f + number_s - 1, number_s - 1) * prob_s^number_s * (1-prob_s)^number_f
5030            // = C(k + r - 1, r - 1) * p^r * (1-p)^k where k = number_f, r = number_s
5031            let ln_prob = ln_binom(number_f + number_s - 1, number_s - 1)
5032                + (number_s as f64) * prob_s.ln()
5033                + (number_f as f64) * (1.0 - prob_s).ln();
5034            ln_prob.exp()
5035        };
5036
5037        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
5038            result,
5039        )))
5040    }
5041}
5042
5043/// Returns hypergeometric probabilities for successes drawn without replacement.
5044///
5045/// Use `HYPGEOM.DIST` for finite-population sampling where each draw changes remaining odds.
5046///
5047/// # Remarks
5048/// - Count inputs are truncated to integers.
5049/// - Requires valid population/sample bounds and feasible success counts.
5050/// - Set `cumulative` to non-zero for CDF mode, or `0` for PMF mode.
5051/// - Returns `#NUM!` for invalid population setup; out-of-support PMF values return `0`.
5052///
5053/// # Examples
5054///
5055/// ```yaml,sandbox
5056/// title: "Hypergeometric PMF"
5057/// formula: "=HYPGEOM.DIST(1,3,4,10,FALSE)"
5058/// expected: 0.5
5059/// ```
5060///
5061/// ```yaml,sandbox
5062/// title: "Hypergeometric CDF"
5063/// formula: "=HYPGEOM.DIST(1,3,4,10,TRUE)"
5064/// expected: 0.6666666666666666
5065/// ```
5066#[derive(Debug)]
5067pub struct HypgeomDistFn;
5068/// [formualizer-docgen:schema:start]
5069/// Name: HYPGEOM.DIST
5070/// Type: HypgeomDistFn
5071/// Min args: 5
5072/// Max args: 5
5073/// Variadic: false
5074/// Signature: HYPGEOM.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar, arg5: number@scalar)
5075/// 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=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg5{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
5076/// Caps: PURE
5077/// [formualizer-docgen:schema:end]
5078impl Function for HypgeomDistFn {
5079    func_caps!(PURE);
5080    fn name(&self) -> &'static str {
5081        "HYPGEOM.DIST"
5082    }
5083    fn min_args(&self) -> usize {
5084        5
5085    }
5086    fn arg_schema(&self) -> &'static [ArgSchema] {
5087        use std::sync::LazyLock;
5088        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
5089            vec![
5090                ArgSchema::number_lenient_scalar(),
5091                ArgSchema::number_lenient_scalar(),
5092                ArgSchema::number_lenient_scalar(),
5093                ArgSchema::number_lenient_scalar(),
5094                ArgSchema::number_lenient_scalar(),
5095            ]
5096        });
5097        &SCHEMA[..]
5098    }
5099    fn eval<'a, 'b, 'c>(
5100        &self,
5101        args: &'c [ArgumentHandle<'a, 'b>],
5102        _ctx: &dyn FunctionContext<'b>,
5103    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5104        let sample_s = coerce_num(&scalar_like_value(&args[0])?)?.trunc() as i64; // successes in sample
5105        let number_sample = coerce_num(&scalar_like_value(&args[1])?)?.trunc() as i64; // sample size
5106        let population_s = coerce_num(&scalar_like_value(&args[2])?)?.trunc() as i64; // successes in population
5107        let number_pop = coerce_num(&scalar_like_value(&args[3])?)?.trunc() as i64; // population size
5108        let cumulative = coerce_num(&scalar_like_value(&args[4])?)? != 0.0;
5109
5110        // Validation
5111        if number_pop <= 0
5112            || population_s < 0
5113            || population_s > number_pop
5114            || number_sample < 0
5115            || number_sample > number_pop
5116            || sample_s < 0
5117        {
5118            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5119                ExcelError::new_num(),
5120            )));
5121        }
5122
5123        // sample_s must be at least max(0, number_sample - (number_pop - population_s))
5124        // and at most min(number_sample, population_s)
5125        let min_successes = 0.max(number_sample - (number_pop - population_s));
5126        let max_successes = number_sample.min(population_s);
5127
5128        if sample_s < min_successes || sample_s > max_successes {
5129            // Return 0 for PMF, or appropriate CDF value
5130            if cumulative {
5131                if sample_s < min_successes {
5132                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0)));
5133                } else {
5134                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(1.0)));
5135                }
5136            } else {
5137                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0)));
5138            }
5139        }
5140
5141        let result = if cumulative {
5142            // CDF: sum from i=min_successes to sample_s of P(X=i)
5143            let mut sum = 0.0;
5144            for i in min_successes..=sample_s {
5145                sum += hypgeom_pmf(i, number_sample, population_s, number_pop);
5146            }
5147            sum
5148        } else {
5149            // PMF: C(population_s, sample_s) * C(number_pop - population_s, number_sample - sample_s) / C(number_pop, number_sample)
5150            hypgeom_pmf(sample_s, number_sample, population_s, number_pop)
5151        };
5152
5153        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
5154            result,
5155        )))
5156    }
5157}
5158
5159/// Helper: Hypergeometric PMF
5160fn hypgeom_pmf(k: i64, n: i64, k_pop: i64, n_pop: i64) -> f64 {
5161    // P(X=k) = C(K, k) * C(N-K, n-k) / C(N, n)
5162    // Using logs to avoid overflow
5163    let ln_prob = ln_binom(k_pop, k) + ln_binom(n_pop - k_pop, n - k) - ln_binom(n_pop, n);
5164    ln_prob.exp()
5165}
5166
5167/* ═══════════════════════════════════════════════════════════════════════════
5168COVARIANCE AND CORRELATION FUNCTIONS
5169═══════════════════════════════════════════════════════════════════════════ */
5170
5171/// Returns population covariance for two paired numeric data sets.
5172///
5173/// `COVARIANCE.P` measures joint variability using `n` in the denominator.
5174///
5175/// # Remarks
5176/// - Arrays must resolve to the same number of numeric points.
5177/// - Uses population scaling (`/ n`) rather than sample scaling.
5178/// - Positive output indicates same-direction movement; negative output indicates opposite movement.
5179/// - Pairing and shape mismatches return spreadsheet errors from paired-array validation.
5180///
5181/// # Examples
5182///
5183/// ```yaml,sandbox
5184/// title: "Positive population covariance"
5185/// formula: "=COVARIANCE.P({1,3,5},{2,4,6})"
5186/// expected: 2.6666666666666665
5187/// ```
5188///
5189/// ```yaml,sandbox
5190/// title: "Negative population covariance"
5191/// formula: "=COVARIANCE.P({1,2,3},{3,2,1})"
5192/// expected: -0.6666666666666666
5193/// ```
5194#[derive(Debug)]
5195pub struct CovariancePFn;
5196/// [formualizer-docgen:schema:start]
5197/// Name: COVARIANCE.P
5198/// Type: CovariancePFn
5199/// Min args: 2
5200/// Max args: 1
5201/// Variadic: false
5202/// Signature: COVARIANCE.P(arg1: number@range)
5203/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
5204/// Caps: PURE, REDUCTION, NUMERIC_ONLY
5205/// [formualizer-docgen:schema:end]
5206impl Function for CovariancePFn {
5207    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
5208    fn name(&self) -> &'static str {
5209        "COVARIANCE.P"
5210    }
5211    fn aliases(&self) -> &'static [&'static str] {
5212        &["COVAR"]
5213    }
5214    fn min_args(&self) -> usize {
5215        2
5216    }
5217    fn arg_schema(&self) -> &'static [ArgSchema] {
5218        &ARG_RANGE_NUM_LENIENT_ONE[..]
5219    }
5220    fn eval<'a, 'b, 'c>(
5221        &self,
5222        args: &'c [ArgumentHandle<'a, 'b>],
5223        _ctx: &dyn FunctionContext<'b>,
5224    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5225        let (y, x) = match collect_paired_arrays(args) {
5226            Ok(v) => v,
5227            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
5228        };
5229
5230        let n = x.len() as f64;
5231        let mean_x = x.iter().sum::<f64>() / n;
5232        let mean_y = y.iter().sum::<f64>() / n;
5233
5234        let mut sum_xy = 0.0;
5235        for i in 0..x.len() {
5236            let dx = x[i] - mean_x;
5237            let dy = y[i] - mean_y;
5238            sum_xy += dx * dy;
5239        }
5240
5241        // Population covariance divides by n
5242        let covar = sum_xy / n;
5243        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
5244            covar,
5245        )))
5246    }
5247}
5248
5249/// Returns sample covariance for two paired numeric data sets.
5250///
5251/// `COVARIANCE.S` measures joint variability using `n - 1` in the denominator.
5252///
5253/// # Remarks
5254/// - Arrays must contain paired numeric values with matching lengths.
5255/// - Requires at least two paired points.
5256/// - Returns `#DIV/0!` when fewer than two numeric pairs are available.
5257/// - Pairing and shape mismatches return spreadsheet errors from paired-array validation.
5258///
5259/// # Examples
5260///
5261/// ```yaml,sandbox
5262/// title: "Positive sample covariance"
5263/// formula: "=COVARIANCE.S({1,3,5},{2,4,6})"
5264/// expected: 4
5265/// ```
5266///
5267/// ```yaml,sandbox
5268/// title: "Negative sample covariance"
5269/// formula: "=COVARIANCE.S({1,2,3},{3,2,1})"
5270/// expected: -1
5271/// ```
5272#[derive(Debug)]
5273pub struct CovarianceSFn;
5274/// [formualizer-docgen:schema:start]
5275/// Name: COVARIANCE.S
5276/// Type: CovarianceSFn
5277/// Min args: 2
5278/// Max args: 1
5279/// Variadic: false
5280/// Signature: COVARIANCE.S(arg1: number@range)
5281/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
5282/// Caps: PURE, REDUCTION, NUMERIC_ONLY
5283/// [formualizer-docgen:schema:end]
5284impl Function for CovarianceSFn {
5285    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
5286    fn name(&self) -> &'static str {
5287        "COVARIANCE.S"
5288    }
5289    fn min_args(&self) -> usize {
5290        2
5291    }
5292    fn arg_schema(&self) -> &'static [ArgSchema] {
5293        &ARG_RANGE_NUM_LENIENT_ONE[..]
5294    }
5295    fn eval<'a, 'b, 'c>(
5296        &self,
5297        args: &'c [ArgumentHandle<'a, 'b>],
5298        _ctx: &dyn FunctionContext<'b>,
5299    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5300        let (y, x) = match collect_paired_arrays(args) {
5301            Ok(v) => v,
5302            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
5303        };
5304
5305        let n = x.len();
5306        if n < 2 {
5307            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5308                ExcelError::new_div(),
5309            )));
5310        }
5311
5312        let mean_x = x.iter().sum::<f64>() / n as f64;
5313        let mean_y = y.iter().sum::<f64>() / n as f64;
5314
5315        let mut sum_xy = 0.0;
5316        for i in 0..n {
5317            let dx = x[i] - mean_x;
5318            let dy = y[i] - mean_y;
5319            sum_xy += dx * dy;
5320        }
5321
5322        // Sample covariance divides by (n - 1)
5323        let covar = sum_xy / (n - 1) as f64;
5324        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
5325            covar,
5326        )))
5327    }
5328}
5329
5330/// Returns the Pearson correlation coefficient between two paired numeric arrays.
5331///
5332/// `PEARSON` reports linear association on a normalized scale from `-1` to `1`.
5333///
5334/// # Remarks
5335/// - Arrays must contain the same number of numeric observations.
5336/// - Returns `#DIV/0!` when either array has zero variance.
5337/// - Positive values indicate positive linear association; negative values indicate inverse association.
5338/// - Pairing and shape mismatches return spreadsheet errors from paired-array validation.
5339///
5340/// # Examples
5341///
5342/// ```yaml,sandbox
5343/// title: "Perfect positive linear correlation"
5344/// formula: "=PEARSON({1,2,3},{2,4,6})"
5345/// expected: 1
5346/// ```
5347///
5348/// ```yaml,sandbox
5349/// title: "Perfect negative linear correlation"
5350/// formula: "=PEARSON({1,2,3},{3,2,1})"
5351/// expected: -1
5352/// ```
5353#[derive(Debug)]
5354pub struct PearsonFn;
5355/// [formualizer-docgen:schema:start]
5356/// Name: PEARSON
5357/// Type: PearsonFn
5358/// Min args: 2
5359/// Max args: 1
5360/// Variadic: false
5361/// Signature: PEARSON(arg1: number@range)
5362/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
5363/// Caps: PURE, REDUCTION, NUMERIC_ONLY
5364/// [formualizer-docgen:schema:end]
5365impl Function for PearsonFn {
5366    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
5367    fn name(&self) -> &'static str {
5368        "PEARSON"
5369    }
5370    fn min_args(&self) -> usize {
5371        2
5372    }
5373    fn arg_schema(&self) -> &'static [ArgSchema] {
5374        &ARG_RANGE_NUM_LENIENT_ONE[..]
5375    }
5376    fn eval<'a, 'b, 'c>(
5377        &self,
5378        args: &'c [ArgumentHandle<'a, 'b>],
5379        _ctx: &dyn FunctionContext<'b>,
5380    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5381        let (y, x) = match collect_paired_arrays(args) {
5382            Ok(v) => v,
5383            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
5384        };
5385
5386        let n = x.len() as f64;
5387        let mean_x = x.iter().sum::<f64>() / n;
5388        let mean_y = y.iter().sum::<f64>() / n;
5389
5390        let mut sum_xy = 0.0;
5391        let mut sum_x2 = 0.0;
5392        let mut sum_y2 = 0.0;
5393
5394        for i in 0..x.len() {
5395            let dx = x[i] - mean_x;
5396            let dy = y[i] - mean_y;
5397            sum_xy += dx * dy;
5398            sum_x2 += dx * dx;
5399            sum_y2 += dy * dy;
5400        }
5401
5402        let denom = (sum_x2 * sum_y2).sqrt();
5403        if denom == 0.0 {
5404            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5405                ExcelError::new_div(),
5406            )));
5407        }
5408
5409        let correl = sum_xy / denom;
5410        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
5411            correl,
5412        )))
5413    }
5414}
5415
5416/// Returns the coefficient of determination (`R^2`) for paired x/y data.
5417///
5418/// `RSQ` is the square of Pearson correlation and indicates explained linear variance.
5419///
5420/// # Remarks
5421/// - Arrays must contain the same number of numeric observations.
5422/// - Result is in `[0, 1]` for valid numeric inputs.
5423/// - Returns `#DIV/0!` when either input array has zero variance.
5424/// - Pairing and shape mismatches return spreadsheet errors from paired-array validation.
5425///
5426/// # Examples
5427///
5428/// ```yaml,sandbox
5429/// title: "Perfect linear fit"
5430/// formula: "=RSQ({1,2,3},{2,4,6})"
5431/// expected: 1
5432/// ```
5433///
5434/// ```yaml,sandbox
5435/// title: "Strong but imperfect linear relationship"
5436/// formula: "=RSQ({1,2,3},{1,2,4})"
5437/// expected: 0.9642857142857143
5438/// ```
5439#[derive(Debug)]
5440pub struct RsqFn;
5441/// [formualizer-docgen:schema:start]
5442/// Name: RSQ
5443/// Type: RsqFn
5444/// Min args: 2
5445/// Max args: 1
5446/// Variadic: false
5447/// Signature: RSQ(arg1: number@range)
5448/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
5449/// Caps: PURE, REDUCTION, NUMERIC_ONLY
5450/// [formualizer-docgen:schema:end]
5451impl Function for RsqFn {
5452    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
5453    fn name(&self) -> &'static str {
5454        "RSQ"
5455    }
5456    fn min_args(&self) -> usize {
5457        2
5458    }
5459    fn arg_schema(&self) -> &'static [ArgSchema] {
5460        &ARG_RANGE_NUM_LENIENT_ONE[..]
5461    }
5462    fn eval<'a, 'b, 'c>(
5463        &self,
5464        args: &'c [ArgumentHandle<'a, 'b>],
5465        _ctx: &dyn FunctionContext<'b>,
5466    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5467        let (y, x) = match collect_paired_arrays(args) {
5468            Ok(v) => v,
5469            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
5470        };
5471
5472        let n = x.len() as f64;
5473        let mean_x = x.iter().sum::<f64>() / n;
5474        let mean_y = y.iter().sum::<f64>() / n;
5475
5476        let mut sum_xy = 0.0;
5477        let mut sum_x2 = 0.0;
5478        let mut sum_y2 = 0.0;
5479
5480        for i in 0..x.len() {
5481            let dx = x[i] - mean_x;
5482            let dy = y[i] - mean_y;
5483            sum_xy += dx * dy;
5484            sum_x2 += dx * dx;
5485            sum_y2 += dy * dy;
5486        }
5487
5488        let denom = sum_x2 * sum_y2;
5489        if denom == 0.0 {
5490            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5491                ExcelError::new_div(),
5492            )));
5493        }
5494
5495        // R-squared = r^2 = (sum_xy)^2 / (sum_x2 * sum_y2)
5496        let rsq = (sum_xy * sum_xy) / denom;
5497        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(rsq)))
5498    }
5499}
5500
5501/// Returns the standard error of y-estimates from a simple linear regression.
5502///
5503/// `STEYX` measures the typical residual size around the fitted regression line.
5504///
5505/// # Remarks
5506/// - Requires paired x/y inputs with matching numeric lengths.
5507/// - Requires at least three paired points.
5508/// - Returns `#DIV/0!` when `n < 3` or x-values have zero variance.
5509/// - Pairing and shape mismatches return spreadsheet errors from paired-array validation.
5510///
5511/// # Examples
5512///
5513/// ```yaml,sandbox
5514/// title: "Perfect linear fit has zero standard error"
5515/// formula: "=STEYX({2,4,6},{1,2,3})"
5516/// expected: 0
5517/// ```
5518///
5519/// ```yaml,sandbox
5520/// title: "Non-zero regression standard error"
5521/// formula: "=STEYX({2,5,7},{1,2,3})"
5522/// expected: 0.408248290463863
5523/// ```
5524#[derive(Debug)]
5525pub struct SteyxFn;
5526/// [formualizer-docgen:schema:start]
5527/// Name: STEYX
5528/// Type: SteyxFn
5529/// Min args: 2
5530/// Max args: 1
5531/// Variadic: false
5532/// Signature: STEYX(arg1: number@range)
5533/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
5534/// Caps: PURE, REDUCTION, NUMERIC_ONLY
5535/// [formualizer-docgen:schema:end]
5536impl Function for SteyxFn {
5537    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
5538    fn name(&self) -> &'static str {
5539        "STEYX"
5540    }
5541    fn min_args(&self) -> usize {
5542        2
5543    }
5544    fn arg_schema(&self) -> &'static [ArgSchema] {
5545        &ARG_RANGE_NUM_LENIENT_ONE[..]
5546    }
5547    fn eval<'a, 'b, 'c>(
5548        &self,
5549        args: &'c [ArgumentHandle<'a, 'b>],
5550        _ctx: &dyn FunctionContext<'b>,
5551    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5552        let (y, x) = match collect_paired_arrays(args) {
5553            Ok(v) => v,
5554            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
5555        };
5556
5557        let n = x.len();
5558        if n < 3 {
5559            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5560                ExcelError::new_div(),
5561            )));
5562        }
5563
5564        let n_f = n as f64;
5565        let mean_x = x.iter().sum::<f64>() / n_f;
5566        let mean_y = y.iter().sum::<f64>() / n_f;
5567
5568        let mut sum_xy = 0.0;
5569        let mut sum_x2 = 0.0;
5570        let mut sum_y2 = 0.0;
5571
5572        for i in 0..n {
5573            let dx = x[i] - mean_x;
5574            let dy = y[i] - mean_y;
5575            sum_xy += dx * dy;
5576            sum_x2 += dx * dx;
5577            sum_y2 += dy * dy;
5578        }
5579
5580        if sum_x2 == 0.0 {
5581            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5582                ExcelError::new_div(),
5583            )));
5584        }
5585
5586        // STEYX = sqrt((sum_y2 - (sum_xy)^2 / sum_x2) / (n - 2))
5587        let sse = sum_y2 - (sum_xy * sum_xy) / sum_x2;
5588        if sse < 0.0 {
5589            // This can happen due to floating point errors; return 0 in such case
5590            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0)));
5591        }
5592        let steyx = (sse / (n_f - 2.0)).sqrt();
5593        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
5594            steyx,
5595        )))
5596    }
5597}
5598
5599/* ─────────────────────────── SKEW ──────────────────────────── */
5600
5601/// Returns the sample skewness of a numeric distribution.
5602///
5603/// `SKEW` quantifies asymmetry: positive values indicate a longer right tail, negative values a
5604/// longer left tail.
5605///
5606/// # Remarks
5607/// - Requires at least three numeric values.
5608/// - Returns `#DIV/0!` when there are fewer than three numbers or zero sample standard deviation.
5609/// - Non-numeric values in ranges are ignored by statistical-collection rules.
5610/// - Uses the Excel-style sample skewness correction factor.
5611///
5612/// # Examples
5613///
5614/// ```yaml,sandbox
5615/// title: "Symmetric sample"
5616/// formula: "=SKEW({1,2,3})"
5617/// expected: 0
5618/// ```
5619///
5620/// ```yaml,sandbox
5621/// title: "Right-skewed sample"
5622/// formula: "=SKEW({1,1,2,10})"
5623/// expected: 1.9683567600862015
5624/// ```
5625#[derive(Debug)]
5626pub struct SkewFn;
5627/// [formualizer-docgen:schema:start]
5628/// Name: SKEW
5629/// Type: SkewFn
5630/// Min args: 1
5631/// Max args: variadic
5632/// Variadic: true
5633/// Signature: SKEW(arg1...: number@range)
5634/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
5635/// Caps: PURE, REDUCTION, NUMERIC_ONLY
5636/// [formualizer-docgen:schema:end]
5637impl Function for SkewFn {
5638    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
5639    fn name(&self) -> &'static str {
5640        "SKEW"
5641    }
5642    fn min_args(&self) -> usize {
5643        1
5644    }
5645    fn variadic(&self) -> bool {
5646        true
5647    }
5648    fn arg_schema(&self) -> &'static [ArgSchema] {
5649        &ARG_RANGE_NUM_LENIENT_ONE[..]
5650    }
5651    fn eval<'a, 'b, 'c>(
5652        &self,
5653        args: &'c [ArgumentHandle<'a, 'b>],
5654        _ctx: &dyn FunctionContext<'b>,
5655    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5656        let nums = collect_numeric_stats(args)?;
5657        let n = nums.len();
5658
5659        // SKEW requires at least 3 data points
5660        if n < 3 {
5661            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5662                ExcelError::new_div(),
5663            )));
5664        }
5665
5666        let n_f = n as f64;
5667        let mean = nums.iter().sum::<f64>() / n_f;
5668
5669        // Calculate sample standard deviation
5670        let mut sum_sq = 0.0;
5671        for &v in &nums {
5672            let d = v - mean;
5673            sum_sq += d * d;
5674        }
5675        let stdev = (sum_sq / (n_f - 1.0)).sqrt();
5676
5677        if stdev == 0.0 {
5678            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5679                ExcelError::new_div(),
5680            )));
5681        }
5682
5683        // Calculate sum of cubed deviations normalized by stdev
5684        let mut sum_cubed = 0.0;
5685        for &v in &nums {
5686            let d = (v - mean) / stdev;
5687            sum_cubed += d * d * d;
5688        }
5689
5690        // Excel SKEW formula: n / ((n-1)*(n-2)) * sum((xi - mean)/stdev)^3
5691        let skew = (n_f / ((n_f - 1.0) * (n_f - 2.0))) * sum_cubed;
5692        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(skew)))
5693    }
5694}
5695
5696/* ─────────────────────────── KURT ──────────────────────────── */
5697
5698/// Returns the sample excess kurtosis of a numeric distribution.
5699///
5700/// `KURT` indicates tail heaviness relative to a normal distribution after Excel-style sample
5701/// correction.
5702///
5703/// # Remarks
5704/// - Requires at least four numeric values.
5705/// - Returns `#DIV/0!` when there are fewer than four numbers or zero sample standard deviation.
5706/// - Positive values suggest heavier tails; negative values suggest lighter tails.
5707/// - Non-numeric values in ranges are ignored by statistical-collection rules.
5708///
5709/// # Examples
5710///
5711/// ```yaml,sandbox
5712/// title: "Uniformly spaced values"
5713/// formula: "=KURT({1,2,3,4})"
5714/// expected: -1.2
5715/// ```
5716///
5717/// ```yaml,sandbox
5718/// title: "Heavier-tail sample"
5719/// formula: "=KURT({1,1,1,2,10,10,10,10})"
5720/// expected: -2.3069755007920767
5721/// ```
5722#[derive(Debug)]
5723pub struct KurtFn;
5724/// [formualizer-docgen:schema:start]
5725/// Name: KURT
5726/// Type: KurtFn
5727/// Min args: 1
5728/// Max args: variadic
5729/// Variadic: true
5730/// Signature: KURT(arg1...: number@range)
5731/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
5732/// Caps: PURE, REDUCTION, NUMERIC_ONLY
5733/// [formualizer-docgen:schema:end]
5734impl Function for KurtFn {
5735    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
5736    fn name(&self) -> &'static str {
5737        "KURT"
5738    }
5739    fn min_args(&self) -> usize {
5740        1
5741    }
5742    fn variadic(&self) -> bool {
5743        true
5744    }
5745    fn arg_schema(&self) -> &'static [ArgSchema] {
5746        &ARG_RANGE_NUM_LENIENT_ONE[..]
5747    }
5748    fn eval<'a, 'b, 'c>(
5749        &self,
5750        args: &'c [ArgumentHandle<'a, 'b>],
5751        _ctx: &dyn FunctionContext<'b>,
5752    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5753        let nums = collect_numeric_stats(args)?;
5754        let n = nums.len();
5755
5756        // KURT requires at least 4 data points
5757        if n < 4 {
5758            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5759                ExcelError::new_div(),
5760            )));
5761        }
5762
5763        let n_f = n as f64;
5764        let mean = nums.iter().sum::<f64>() / n_f;
5765
5766        // Calculate sample standard deviation
5767        let mut sum_sq = 0.0;
5768        for &v in &nums {
5769            let d = v - mean;
5770            sum_sq += d * d;
5771        }
5772        let stdev = (sum_sq / (n_f - 1.0)).sqrt();
5773
5774        if stdev == 0.0 {
5775            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5776                ExcelError::new_div(),
5777            )));
5778        }
5779
5780        // Calculate sum of fourth powers of deviations normalized by stdev
5781        let mut sum_fourth = 0.0;
5782        for &v in &nums {
5783            let d = (v - mean) / stdev;
5784            sum_fourth += d * d * d * d;
5785        }
5786
5787        // Excel KURT formula (excess kurtosis):
5788        // n*(n+1) / ((n-1)*(n-2)*(n-3)) * sum((xi - mean)/stdev)^4 - 3*(n-1)^2 / ((n-2)*(n-3))
5789        let term1 = (n_f * (n_f + 1.0)) / ((n_f - 1.0) * (n_f - 2.0) * (n_f - 3.0)) * sum_fourth;
5790        let term2 = (3.0 * (n_f - 1.0) * (n_f - 1.0)) / ((n_f - 2.0) * (n_f - 3.0));
5791        let kurt = term1 - term2;
5792        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(kurt)))
5793    }
5794}
5795
5796/* ─────────────────────────── FISHER ──────────────────────────── */
5797
5798/// Returns the Fisher z-transformation of a correlation-like value `x`.
5799///
5800/// `FISHER` maps `(-1, 1)` into `(-inf, +inf)` and is commonly used in correlation inference.
5801///
5802/// # Remarks
5803/// - Input must satisfy `-1 < x < 1`.
5804/// - Returns `#NUM!` when `x <= -1` or `x >= 1`.
5805/// - The transformation is `0.5 * ln((1 + x) / (1 - x))`.
5806/// - Invalid numeric coercions propagate as spreadsheet errors.
5807///
5808/// # Examples
5809///
5810/// ```yaml,sandbox
5811/// title: "Fisher transform at zero"
5812/// formula: "=FISHER(0)"
5813/// expected: 0
5814/// ```
5815///
5816/// ```yaml,sandbox
5817/// title: "Fisher transform at x=0.5"
5818/// formula: "=FISHER(0.5)"
5819/// expected: 0.5493061443340549
5820/// ```
5821#[derive(Debug)]
5822pub struct FisherFn;
5823/// [formualizer-docgen:schema:start]
5824/// Name: FISHER
5825/// Type: FisherFn
5826/// Min args: 1
5827/// Max args: 1
5828/// Variadic: false
5829/// Signature: FISHER(arg1: number@range)
5830/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
5831/// Caps: PURE, NUMERIC_ONLY
5832/// [formualizer-docgen:schema:end]
5833impl Function for FisherFn {
5834    func_caps!(PURE, NUMERIC_ONLY);
5835    fn name(&self) -> &'static str {
5836        "FISHER"
5837    }
5838    fn min_args(&self) -> usize {
5839        1
5840    }
5841    fn arg_schema(&self) -> &'static [ArgSchema] {
5842        &ARG_RANGE_NUM_LENIENT_ONE[..]
5843    }
5844    fn eval<'a, 'b, 'c>(
5845        &self,
5846        args: &'c [ArgumentHandle<'a, 'b>],
5847        _ctx: &dyn FunctionContext<'b>,
5848    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5849        let x = coerce_num(&scalar_like_value(&args[0])?)?;
5850
5851        // FISHER requires -1 < x < 1
5852        if x <= -1.0 || x >= 1.0 {
5853            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5854                ExcelError::new_num(),
5855            )));
5856        }
5857
5858        // Fisher transformation: 0.5 * ln((1 + x) / (1 - x))
5859        let fisher = 0.5 * ((1.0 + x) / (1.0 - x)).ln();
5860        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
5861            fisher,
5862        )))
5863    }
5864}
5865
5866/* ─────────────────────────── FISHERINV ──────────────────────────── */
5867
5868/// Returns the inverse Fisher transformation of `y`.
5869///
5870/// `FISHERINV` maps Fisher z-values back to the open interval `(-1, 1)`.
5871///
5872/// # Remarks
5873/// - The inverse form is `(e^(2y) - 1) / (e^(2y) + 1)`.
5874/// - Output is always strictly between `-1` and `1` for finite inputs.
5875/// - This function is useful for converting transformed correlation estimates back to r-space.
5876/// - Invalid numeric coercions propagate as spreadsheet errors.
5877///
5878/// # Examples
5879///
5880/// ```yaml,sandbox
5881/// title: "Inverse Fisher at zero"
5882/// formula: "=FISHERINV(0)"
5883/// expected: 0
5884/// ```
5885///
5886/// ```yaml,sandbox
5887/// title: "Round-trip with FISHER(0.5)"
5888/// formula: "=FISHERINV(0.5493061443340549)"
5889/// expected: 0.5
5890/// ```
5891#[derive(Debug)]
5892pub struct FisherInvFn;
5893/// [formualizer-docgen:schema:start]
5894/// Name: FISHERINV
5895/// Type: FisherInvFn
5896/// Min args: 1
5897/// Max args: 1
5898/// Variadic: false
5899/// Signature: FISHERINV(arg1: number@range)
5900/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
5901/// Caps: PURE, NUMERIC_ONLY
5902/// [formualizer-docgen:schema:end]
5903impl Function for FisherInvFn {
5904    func_caps!(PURE, NUMERIC_ONLY);
5905    fn name(&self) -> &'static str {
5906        "FISHERINV"
5907    }
5908    fn min_args(&self) -> usize {
5909        1
5910    }
5911    fn arg_schema(&self) -> &'static [ArgSchema] {
5912        &ARG_RANGE_NUM_LENIENT_ONE[..]
5913    }
5914    fn eval<'a, 'b, 'c>(
5915        &self,
5916        args: &'c [ArgumentHandle<'a, 'b>],
5917        _ctx: &dyn FunctionContext<'b>,
5918    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5919        let y = coerce_num(&scalar_like_value(&args[0])?)?;
5920
5921        // Inverse Fisher transformation: (e^(2y) - 1) / (e^(2y) + 1)
5922        let e2y = (2.0 * y).exp();
5923        let fisherinv = (e2y - 1.0) / (e2y + 1.0);
5924        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
5925            fisherinv,
5926        )))
5927    }
5928}
5929
5930/* ─────────────────────────── FORECAST.LINEAR ──────────────────────────── */
5931
5932/// Returns a predicted y-value at `x` from simple linear regression over known data.
5933///
5934/// `FORECAST.LINEAR` fits `y = intercept + slope * x` and evaluates that line at the requested x.
5935///
5936/// # Remarks
5937/// - Requires `known_y` and `known_x` arrays with the same numeric length.
5938/// - Returns `#N/A` when arrays are empty or lengths do not match.
5939/// - Returns `#DIV/0!` when `known_x` has zero variance.
5940/// - Alias `FORECAST` is supported.
5941///
5942/// # Examples
5943///
5944/// ```yaml,sandbox
5945/// title: "Predict next point on a perfect line"
5946/// formula: "=FORECAST.LINEAR(4,{2,4,6},{1,2,3})"
5947/// expected: 8
5948/// ```
5949///
5950/// ```yaml,sandbox
5951/// title: "Forecast with non-zero intercept"
5952/// formula: "=FORECAST.LINEAR(5,{3,5,7},{1,2,3})"
5953/// expected: 11
5954/// ```
5955#[derive(Debug)]
5956pub struct ForecastLinearFn;
5957/// [formualizer-docgen:schema:start]
5958/// Name: FORECAST.LINEAR
5959/// Type: ForecastLinearFn
5960/// Min args: 3
5961/// Max args: 1
5962/// Variadic: false
5963/// Signature: FORECAST.LINEAR(arg1: number@range)
5964/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
5965/// Caps: PURE, NUMERIC_ONLY
5966/// [formualizer-docgen:schema:end]
5967impl Function for ForecastLinearFn {
5968    func_caps!(PURE, NUMERIC_ONLY);
5969    fn name(&self) -> &'static str {
5970        "FORECAST.LINEAR"
5971    }
5972    fn aliases(&self) -> &'static [&'static str] {
5973        &["FORECAST"]
5974    }
5975    fn min_args(&self) -> usize {
5976        3
5977    }
5978    fn arg_schema(&self) -> &'static [ArgSchema] {
5979        &ARG_RANGE_NUM_LENIENT_ONE[..]
5980    }
5981    fn eval<'a, 'b, 'c>(
5982        &self,
5983        args: &'c [ArgumentHandle<'a, 'b>],
5984        _ctx: &dyn FunctionContext<'b>,
5985    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5986        // args[0] = x value to forecast
5987        // args[1] = known_y's
5988        // args[2] = known_x's
5989        let x = match coerce_num(&scalar_like_value(&args[0])?) {
5990            Ok(n) => n,
5991            Err(_) => {
5992                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5993                    ExcelError::new_value(),
5994                )));
5995            }
5996        };
5997
5998        let y_vals = collect_numeric_stats(&args[1..2])?;
5999        let x_vals = collect_numeric_stats(&args[2..3])?;
6000
6001        // Arrays must have same length
6002        if y_vals.len() != x_vals.len() {
6003            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6004                ExcelError::new_na(),
6005            )));
6006        }
6007
6008        if y_vals.is_empty() {
6009            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6010                ExcelError::new_na(),
6011            )));
6012        }
6013
6014        let n = x_vals.len() as f64;
6015        let mean_x = x_vals.iter().sum::<f64>() / n;
6016        let mean_y = y_vals.iter().sum::<f64>() / n;
6017
6018        let mut sum_xy = 0.0;
6019        let mut sum_x2 = 0.0;
6020
6021        for i in 0..x_vals.len() {
6022            let dx = x_vals[i] - mean_x;
6023            let dy = y_vals[i] - mean_y;
6024            sum_xy += dx * dy;
6025            sum_x2 += dx * dx;
6026        }
6027
6028        if sum_x2 == 0.0 {
6029            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6030                ExcelError::new_div(),
6031            )));
6032        }
6033
6034        let slope = sum_xy / sum_x2;
6035        let intercept = mean_y - slope * mean_x;
6036        let forecast = intercept + slope * x;
6037
6038        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
6039            forecast,
6040        )))
6041    }
6042}
6043
6044/* ─────────────────────────── LINEST ──────────────────────────── */
6045
6046/// Returns linear-regression coefficients and optional fit statistics.
6047///
6048/// `LINEST` fits a straight line to known y/x pairs and returns either `[slope, intercept]` or a
6049/// larger statistics matrix.
6050///
6051/// # Remarks
6052/// - `known_y` is required; `known_x` defaults to `1..n` when omitted.
6053/// - `const` controls whether an intercept is fitted (`TRUE` by default).
6054/// - `stats=TRUE` returns a `5x2` result block; otherwise it returns `1x2`.
6055/// - Returns spreadsheet errors for mismatched lengths, empty data, or degenerate x-values.
6056///
6057/// # Examples
6058///
6059/// ```yaml,sandbox
6060/// title: "Slope and intercept only"
6061/// formula: "=LINEST({2,4,6},{1,2,3})"
6062/// expected:
6063///   - [2, 0]
6064/// ```
6065///
6066/// ```yaml,sandbox
6067/// title: "Linear fit with non-zero intercept"
6068/// formula: "=LINEST({3,5,7},{1,2,3})"
6069/// expected:
6070///   - [2, 1]
6071/// ```
6072#[derive(Debug)]
6073pub struct LinestFn;
6074/// [formualizer-docgen:schema:start]
6075/// Name: LINEST
6076/// Type: LinestFn
6077/// Min args: 1
6078/// Max args: variadic
6079/// Variadic: true
6080/// Signature: LINEST(arg1...: number@range)
6081/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
6082/// Caps: PURE, NUMERIC_ONLY
6083/// [formualizer-docgen:schema:end]
6084impl Function for LinestFn {
6085    func_caps!(PURE, NUMERIC_ONLY);
6086    fn name(&self) -> &'static str {
6087        "LINEST"
6088    }
6089    fn min_args(&self) -> usize {
6090        1
6091    }
6092    fn variadic(&self) -> bool {
6093        true
6094    }
6095    fn arg_schema(&self) -> &'static [ArgSchema] {
6096        &ARG_RANGE_NUM_LENIENT_ONE[..]
6097    }
6098    fn eval<'a, 'b, 'c>(
6099        &self,
6100        args: &'c [ArgumentHandle<'a, 'b>],
6101        _ctx: &dyn FunctionContext<'b>,
6102    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
6103        // args[0] = known_y's (required)
6104        // args[1] = known_x's (optional, defaults to {1,2,3,...})
6105        // args[2] = const (optional, default TRUE - whether to compute intercept)
6106        // args[3] = stats (optional, default FALSE - whether to return additional statistics)
6107
6108        let y_vals = collect_numeric_stats(&args[0..1])?;
6109
6110        if y_vals.is_empty() {
6111            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6112                ExcelError::new_na(),
6113            )));
6114        }
6115
6116        // Get known_x's or generate default {1, 2, 3, ...}
6117        let x_vals = if args.len() >= 2 {
6118            collect_numeric_stats(&args[1..2])?
6119        } else {
6120            (1..=y_vals.len()).map(|i| i as f64).collect()
6121        };
6122
6123        // Arrays must have same length
6124        if y_vals.len() != x_vals.len() {
6125            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6126                ExcelError::new_ref(),
6127            )));
6128        }
6129
6130        // Parse const argument (default TRUE)
6131        let use_const = if args.len() >= 3 {
6132            match scalar_like_value(&args[2])? {
6133                LiteralValue::Boolean(b) => b,
6134                LiteralValue::Number(n) => n != 0.0,
6135                LiteralValue::Int(i) => i != 0,
6136                _ => true,
6137            }
6138        } else {
6139            true
6140        };
6141
6142        // Parse stats argument (default FALSE)
6143        let return_stats = if args.len() >= 4 {
6144            match scalar_like_value(&args[3])? {
6145                LiteralValue::Boolean(b) => b,
6146                LiteralValue::Number(n) => n != 0.0,
6147                LiteralValue::Int(i) => i != 0,
6148                _ => false,
6149            }
6150        } else {
6151            false
6152        };
6153
6154        let n = x_vals.len() as f64;
6155
6156        // Calculate regression coefficients
6157        let (slope, intercept) = if use_const {
6158            // Normal linear regression with intercept
6159            let mean_x = x_vals.iter().sum::<f64>() / n;
6160            let mean_y = y_vals.iter().sum::<f64>() / n;
6161
6162            let mut sum_xy = 0.0;
6163            let mut sum_x2 = 0.0;
6164
6165            for i in 0..x_vals.len() {
6166                let dx = x_vals[i] - mean_x;
6167                let dy = y_vals[i] - mean_y;
6168                sum_xy += dx * dy;
6169                sum_x2 += dx * dx;
6170            }
6171
6172            if sum_x2 == 0.0 {
6173                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6174                    ExcelError::new_div(),
6175                )));
6176            }
6177
6178            let slope = sum_xy / sum_x2;
6179            let intercept = mean_y - slope * mean_x;
6180            (slope, intercept)
6181        } else {
6182            // Regression through origin (intercept = 0)
6183            let mut sum_xy = 0.0;
6184            let mut sum_x2 = 0.0;
6185
6186            for i in 0..x_vals.len() {
6187                sum_xy += x_vals[i] * y_vals[i];
6188                sum_x2 += x_vals[i] * x_vals[i];
6189            }
6190
6191            if sum_x2 == 0.0 {
6192                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6193                    ExcelError::new_div(),
6194                )));
6195            }
6196
6197            let slope = sum_xy / sum_x2;
6198            (slope, 0.0)
6199        };
6200
6201        if !return_stats {
6202            // Return just slope and intercept as 1x2 array: [[slope, intercept]]
6203            let row = vec![LiteralValue::Number(slope), LiteralValue::Number(intercept)];
6204            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Array(vec![
6205                row,
6206            ])));
6207        }
6208
6209        // Calculate additional statistics for stats=TRUE
6210        // Row 1: [slope, intercept]
6211        // Row 2: [se_slope, se_intercept]
6212        // Row 3: [r_squared, se_y]
6213        // Row 4: [F_statistic, df]
6214        // Row 5: [ss_reg, ss_resid]
6215
6216        let mean_y = y_vals.iter().sum::<f64>() / n;
6217
6218        // Calculate residuals and sums of squares
6219        let mut ss_resid = 0.0; // Sum of squared residuals
6220        let mut ss_tot = 0.0; // Total sum of squares
6221
6222        for i in 0..x_vals.len() {
6223            let y_pred = slope * x_vals[i] + intercept;
6224            let residual = y_vals[i] - y_pred;
6225            ss_resid += residual * residual;
6226            let dy_tot = y_vals[i] - mean_y;
6227            ss_tot += dy_tot * dy_tot;
6228        }
6229
6230        let ss_reg = ss_tot - ss_resid; // Regression sum of squares
6231
6232        // R-squared
6233        let r_squared = if ss_tot == 0.0 {
6234            1.0 // Perfect fit or all y values are the same
6235        } else {
6236            1.0 - (ss_resid / ss_tot)
6237        };
6238
6239        // Degrees of freedom
6240        let df = if use_const {
6241            (n as i64 - 2).max(1) as f64 // n - k - 1 where k=1 (one predictor)
6242        } else {
6243            (n as i64 - 1).max(1) as f64 // n - k when no intercept
6244        };
6245
6246        // Standard error of y estimate
6247        let se_y = if df > 0.0 {
6248            (ss_resid / df).sqrt()
6249        } else {
6250            0.0
6251        };
6252
6253        // Standard errors of coefficients
6254        let mean_x = x_vals.iter().sum::<f64>() / n;
6255        let mut sum_x2_centered = 0.0;
6256        let mut sum_x2_raw = 0.0;
6257        for &xi in &x_vals {
6258            sum_x2_centered += (xi - mean_x).powi(2);
6259            sum_x2_raw += xi * xi;
6260        }
6261
6262        let se_slope = if sum_x2_centered > 0.0 && df > 0.0 {
6263            se_y / sum_x2_centered.sqrt()
6264        } else {
6265            f64::NAN
6266        };
6267
6268        let se_intercept = if use_const && sum_x2_centered > 0.0 && df > 0.0 {
6269            se_y * (sum_x2_raw / (n * sum_x2_centered)).sqrt()
6270        } else {
6271            f64::NAN
6272        };
6273
6274        // F-statistic
6275        let f_stat = if ss_resid > 0.0 && df > 0.0 {
6276            (ss_reg / 1.0) / (ss_resid / df) // MSR / MSE
6277        } else if ss_resid == 0.0 {
6278            f64::INFINITY // Perfect fit
6279        } else {
6280            f64::NAN
6281        };
6282
6283        // Build 5x2 result array
6284        let rows = vec![
6285            vec![LiteralValue::Number(slope), LiteralValue::Number(intercept)],
6286            vec![
6287                LiteralValue::Number(se_slope),
6288                LiteralValue::Number(se_intercept),
6289            ],
6290            vec![LiteralValue::Number(r_squared), LiteralValue::Number(se_y)],
6291            vec![LiteralValue::Number(f_stat), LiteralValue::Number(df)],
6292            vec![LiteralValue::Number(ss_reg), LiteralValue::Number(ss_resid)],
6293        ];
6294
6295        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Array(rows)))
6296    }
6297}
6298
6299/* ─────────────────────────── CONFIDENCE.NORM ──────────────────────────── */
6300
6301/// Returns the half-width of a confidence interval using a normal critical value.
6302///
6303/// `CONFIDENCE.NORM` computes `z_crit * standard_dev / sqrt(size)` for two-sided intervals.
6304///
6305/// # Remarks
6306/// - `alpha` must satisfy `0 < alpha < 1`.
6307/// - `standard_dev` must be greater than `0`.
6308/// - `size` must be at least `1`.
6309/// - Returns `#NUM!` when any input is outside valid bounds.
6310///
6311/// # Examples
6312///
6313/// ```yaml,sandbox
6314/// title: "95% confidence half-width"
6315/// formula: "=CONFIDENCE.NORM(0.05,2,100)"
6316/// expected: 0.3919927977622559
6317/// ```
6318///
6319/// ```yaml,sandbox
6320/// title: "90% confidence half-width"
6321/// formula: "=CONFIDENCE.NORM(0.1,5,25)"
6322/// expected: 1.644853625133699
6323/// ```
6324#[derive(Debug)]
6325pub struct ConfidenceNormFn;
6326/// [formualizer-docgen:schema:start]
6327/// Name: CONFIDENCE.NORM
6328/// Type: ConfidenceNormFn
6329/// Min args: 3
6330/// Max args: 3
6331/// Variadic: false
6332/// Signature: CONFIDENCE.NORM(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
6333/// 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}
6334/// Caps: PURE
6335/// [formualizer-docgen:schema:end]
6336impl Function for ConfidenceNormFn {
6337    func_caps!(PURE);
6338    fn name(&self) -> &'static str {
6339        "CONFIDENCE.NORM"
6340    }
6341    fn aliases(&self) -> &'static [&'static str] {
6342        &["CONFIDENCE"]
6343    }
6344    fn min_args(&self) -> usize {
6345        3
6346    }
6347    fn arg_schema(&self) -> &'static [ArgSchema] {
6348        use std::sync::LazyLock;
6349        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
6350            vec![
6351                ArgSchema::number_lenient_scalar(),
6352                ArgSchema::number_lenient_scalar(),
6353                ArgSchema::number_lenient_scalar(),
6354            ]
6355        });
6356        &SCHEMA[..]
6357    }
6358    fn eval<'a, 'b, 'c>(
6359        &self,
6360        args: &'c [ArgumentHandle<'a, 'b>],
6361        _ctx: &dyn FunctionContext<'b>,
6362    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
6363        let alpha = coerce_num(&scalar_like_value(&args[0])?)?;
6364        let std_dev = coerce_num(&scalar_like_value(&args[1])?)?;
6365        let size = coerce_num(&scalar_like_value(&args[2])?)?;
6366
6367        // Validate inputs
6368        if alpha <= 0.0 || alpha >= 1.0 {
6369            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6370                ExcelError::new_num(),
6371            )));
6372        }
6373        if std_dev <= 0.0 {
6374            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6375                ExcelError::new_num(),
6376            )));
6377        }
6378        if size < 1.0 {
6379            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6380                ExcelError::new_num(),
6381            )));
6382        }
6383
6384        // z_crit = NORM.S.INV(1 - alpha/2)
6385        let z_crit = match std_norm_inv(1.0 - alpha / 2.0) {
6386            Some(z) => z,
6387            None => {
6388                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6389                    ExcelError::new_num(),
6390                )));
6391            }
6392        };
6393
6394        let result = z_crit * std_dev / size.sqrt();
6395        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
6396            result,
6397        )))
6398    }
6399}
6400
6401/* ─────────────────────────── CONFIDENCE.T ──────────────────────────── */
6402
6403/// Returns the half-width of a confidence interval using a t critical value.
6404///
6405/// `CONFIDENCE.T` is typically used when population standard deviation is unknown and sample size
6406/// is limited.
6407///
6408/// # Remarks
6409/// - `alpha` must satisfy `0 < alpha < 1`.
6410/// - `standard_dev` must be greater than `0`.
6411/// - `size` must be at least `2` so that `df = size - 1` is valid.
6412/// - Returns `#NUM!` when inputs are outside valid bounds.
6413///
6414/// # Examples
6415///
6416/// ```yaml,sandbox
6417/// title: "95% t-interval half-width"
6418/// formula: "=CONFIDENCE.T(0.05,2,25)"
6419/// expected: 0.8256636934020788
6420/// ```
6421///
6422/// ```yaml,sandbox
6423/// title: "90% t-interval half-width"
6424/// formula: "=CONFIDENCE.T(0.1,5,10)"
6425/// expected: 2.9158049866307585
6426/// ```
6427#[derive(Debug)]
6428pub struct ConfidenceTFn;
6429/// [formualizer-docgen:schema:start]
6430/// Name: CONFIDENCE.T
6431/// Type: ConfidenceTFn
6432/// Min args: 3
6433/// Max args: 3
6434/// Variadic: false
6435/// Signature: CONFIDENCE.T(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
6436/// 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}
6437/// Caps: PURE
6438/// [formualizer-docgen:schema:end]
6439impl Function for ConfidenceTFn {
6440    func_caps!(PURE);
6441    fn name(&self) -> &'static str {
6442        "CONFIDENCE.T"
6443    }
6444    fn min_args(&self) -> usize {
6445        3
6446    }
6447    fn arg_schema(&self) -> &'static [ArgSchema] {
6448        use std::sync::LazyLock;
6449        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
6450            vec![
6451                ArgSchema::number_lenient_scalar(),
6452                ArgSchema::number_lenient_scalar(),
6453                ArgSchema::number_lenient_scalar(),
6454            ]
6455        });
6456        &SCHEMA[..]
6457    }
6458    fn eval<'a, 'b, 'c>(
6459        &self,
6460        args: &'c [ArgumentHandle<'a, 'b>],
6461        _ctx: &dyn FunctionContext<'b>,
6462    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
6463        let alpha = coerce_num(&scalar_like_value(&args[0])?)?;
6464        let std_dev = coerce_num(&scalar_like_value(&args[1])?)?;
6465        let size = coerce_num(&scalar_like_value(&args[2])?)?;
6466
6467        // Validate inputs - size must be >= 2 for t-distribution (df = size - 1 >= 1)
6468        if alpha <= 0.0 || alpha >= 1.0 {
6469            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6470                ExcelError::new_num(),
6471            )));
6472        }
6473        if std_dev <= 0.0 {
6474            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6475                ExcelError::new_num(),
6476            )));
6477        }
6478        if size < 2.0 {
6479            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6480                ExcelError::new_num(),
6481            )));
6482        }
6483
6484        let df = size - 1.0;
6485
6486        // t_crit = T.INV(1 - alpha/2, df)
6487        let t_crit = match t_inv(1.0 - alpha / 2.0, df) {
6488            Some(t) => t,
6489            None => {
6490                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6491                    ExcelError::new_num(),
6492                )));
6493            }
6494        };
6495
6496        let result = t_crit * std_dev / size.sqrt();
6497        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
6498            result,
6499        )))
6500    }
6501}
6502
6503/* ─────────────────────────── Z.TEST ──────────────────────────── */
6504
6505/// Returns the one-tailed p-value of a z-test against hypothesized mean `x`.
6506///
6507/// `Z.TEST` evaluates whether the sample mean is significantly greater than the target value.
6508///
6509/// # Remarks
6510/// - Uses provided `sigma` when supplied; otherwise computes population standard deviation.
6511/// - Returns `#NUM!` when `sigma <= 0`.
6512/// - Returns `#DIV/0!` when implied standard deviation is zero.
6513/// - Returns `#N/A` when the data array has no numeric values.
6514///
6515/// # Examples
6516///
6517/// ```yaml,sandbox
6518/// title: "Z-test with provided sigma"
6519/// formula: "=Z.TEST({1,2,3,4,5},2,1)"
6520/// expected: 0.012673659338734137
6521/// ```
6522///
6523/// ```yaml,sandbox
6524/// title: "Z-test with sigma estimated from sample"
6525/// formula: "=Z.TEST({1,2,3,4,5},2)"
6526/// expected: 0.056923149003329065
6527/// ```
6528#[derive(Debug)]
6529pub struct ZTestFn;
6530/// [formualizer-docgen:schema:start]
6531/// Name: Z.TEST
6532/// Type: ZTestFn
6533/// Min args: 2
6534/// Max args: variadic
6535/// Variadic: true
6536/// Signature: Z.TEST(arg1: number@range, arg2: number@scalar, arg3...: number@scalar)
6537/// Arg schema: arg1{kinds=number,required=true,shape=range,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}
6538/// Caps: PURE
6539/// [formualizer-docgen:schema:end]
6540impl Function for ZTestFn {
6541    func_caps!(PURE);
6542    fn name(&self) -> &'static str {
6543        "Z.TEST"
6544    }
6545    fn aliases(&self) -> &'static [&'static str] {
6546        &["ZTEST"]
6547    }
6548    fn min_args(&self) -> usize {
6549        2
6550    }
6551    fn variadic(&self) -> bool {
6552        true
6553    }
6554    fn arg_schema(&self) -> &'static [ArgSchema] {
6555        use std::sync::LazyLock;
6556        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
6557            vec![
6558                {
6559                    let mut s = ArgSchema::number_lenient_scalar();
6560                    s.shape = crate::args::ShapeKind::Range;
6561                    s.coercion = formualizer_common::CoercionPolicy::NumberLenientText;
6562                    s
6563                },
6564                ArgSchema::number_lenient_scalar(),
6565                ArgSchema::number_lenient_scalar(), // optional sigma
6566            ]
6567        });
6568        &SCHEMA[..]
6569    }
6570    fn eval<'a, 'b, 'c>(
6571        &self,
6572        args: &'c [ArgumentHandle<'a, 'b>],
6573        _ctx: &dyn FunctionContext<'b>,
6574    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
6575        // Collect numeric values from the array argument
6576        let data = collect_numeric_stats(&args[0..1])?;
6577
6578        if data.is_empty() {
6579            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6580                ExcelError::new_na(),
6581            )));
6582        }
6583
6584        let x = coerce_num(&scalar_like_value(&args[1])?)?;
6585
6586        let n = data.len() as f64;
6587        let mean: f64 = data.iter().sum::<f64>() / n;
6588
6589        // Calculate sigma: use provided value or compute population std dev
6590        let sigma = if args.len() > 2 {
6591            let s = coerce_num(&scalar_like_value(&args[2])?)?;
6592            if s <= 0.0 {
6593                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6594                    ExcelError::new_num(),
6595                )));
6596            }
6597            s
6598        } else {
6599            // Population standard deviation
6600            let variance: f64 = data.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / n;
6601            let std_dev = variance.sqrt();
6602            if std_dev == 0.0 {
6603                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6604                    ExcelError::new_div(),
6605                )));
6606            }
6607            std_dev
6608        };
6609
6610        // z = (mean - x) / (sigma / sqrt(n))
6611        let z = (mean - x) / (sigma / n.sqrt());
6612
6613        // P-value = 1 - NORM.S.DIST(z, TRUE)
6614        let p_value = 1.0 - std_norm_cdf(z);
6615
6616        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
6617            p_value,
6618        )))
6619    }
6620}
6621
6622/* ─────────────────────────── TREND ──────────────────────────── */
6623
6624/// Returns fitted y-values along a linear trend derived from known data.
6625///
6626/// `TREND` performs simple linear regression and returns predictions for `new_x` (or defaults).
6627///
6628/// # Remarks
6629/// - `known_y` is required; `known_x` defaults to `1..n` when omitted.
6630/// - `new_x` defaults to `known_x` when omitted.
6631/// - `const` defaults to `TRUE`; set to `FALSE` to force a zero intercept.
6632/// - Returns spreadsheet errors for empty data, mismatched lengths, or degenerate x-variance.
6633///
6634/// # Examples
6635///
6636/// ```yaml,sandbox
6637/// title: "Predict two future points on a line"
6638/// formula: "=TREND({2,4,6},{1,2,3},{4,5})"
6639/// expected:
6640///   - [8, 10]
6641/// ```
6642///
6643/// ```yaml,sandbox
6644/// title: "Default x-values with fitted trend"
6645/// formula: "=TREND({3,5,7})"
6646/// expected:
6647///   - [3, 5, 7]
6648/// ```
6649#[derive(Debug)]
6650pub struct TrendFn;
6651/// [formualizer-docgen:schema:start]
6652/// Name: TREND
6653/// Type: TrendFn
6654/// Min args: 1
6655/// Max args: variadic
6656/// Variadic: true
6657/// Signature: TREND(arg1...: number@range)
6658/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
6659/// Caps: PURE, NUMERIC_ONLY
6660/// [formualizer-docgen:schema:end]
6661impl Function for TrendFn {
6662    func_caps!(PURE, NUMERIC_ONLY);
6663    fn name(&self) -> &'static str {
6664        "TREND"
6665    }
6666    fn min_args(&self) -> usize {
6667        1
6668    }
6669    fn variadic(&self) -> bool {
6670        true
6671    }
6672    fn arg_schema(&self) -> &'static [ArgSchema] {
6673        &ARG_RANGE_NUM_LENIENT_ONE[..]
6674    }
6675    fn eval<'a, 'b, 'c>(
6676        &self,
6677        args: &'c [ArgumentHandle<'a, 'b>],
6678        _ctx: &dyn FunctionContext<'b>,
6679    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
6680        // TREND: args[0] = known_y's (required)
6681        // args[1] = known_x's (optional, defaults to {1,2,3,...})
6682        // args[2] = new_x's (optional, defaults to known_x's)
6683        // args[3] = const (optional, default TRUE - whether to compute intercept)
6684
6685        let y_vals = collect_numeric_stats(&args[0..1])?;
6686
6687        if y_vals.is_empty() {
6688            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6689                ExcelError::new_na(),
6690            )));
6691        }
6692
6693        // Helper to check if argument is empty/omitted
6694        // Note: Empty arguments are represented as empty text strings by the parser
6695        fn is_arg_empty(arg: &ArgumentHandle) -> bool {
6696            match scalar_like_value(arg) {
6697                Ok(LiteralValue::Empty) => true,
6698                Ok(LiteralValue::Text(s)) if s.is_empty() => true,
6699                _ => false,
6700            }
6701        }
6702
6703        // Get known_x's or generate default {1, 2, 3, ...}
6704        let x_vals = if args.len() >= 2 && !is_arg_empty(&args[1]) {
6705            collect_numeric_stats(&args[1..2])?
6706        } else {
6707            (1..=y_vals.len()).map(|i| i as f64).collect()
6708        };
6709
6710        // Arrays must have same length
6711        if y_vals.len() != x_vals.len() {
6712            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6713                ExcelError::new_ref(),
6714            )));
6715        }
6716
6717        // Get new_x's or use known_x's - check if argument is empty/omitted
6718        let new_x_vals = if args.len() >= 3 && !is_arg_empty(&args[2]) {
6719            collect_numeric_stats(&args[2..3])?
6720        } else {
6721            x_vals.clone()
6722        };
6723
6724        if new_x_vals.is_empty() {
6725            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6726                ExcelError::new_na(),
6727            )));
6728        }
6729
6730        // Parse const argument (default TRUE)
6731        let use_const = if args.len() >= 4 {
6732            match scalar_like_value(&args[3])? {
6733                LiteralValue::Boolean(b) => b,
6734                LiteralValue::Number(n) => n != 0.0,
6735                LiteralValue::Int(i) => i != 0,
6736                LiteralValue::Empty => true, // empty defaults to TRUE
6737                _ => true,
6738            }
6739        } else {
6740            true
6741        };
6742
6743        let n = x_vals.len() as f64;
6744
6745        // Calculate regression coefficients
6746        let (slope, intercept) = if use_const {
6747            // Normal linear regression with intercept
6748            let mean_x = x_vals.iter().sum::<f64>() / n;
6749            let mean_y = y_vals.iter().sum::<f64>() / n;
6750
6751            let mut sum_xy = 0.0;
6752            let mut sum_x2 = 0.0;
6753
6754            for i in 0..x_vals.len() {
6755                let dx = x_vals[i] - mean_x;
6756                let dy = y_vals[i] - mean_y;
6757                sum_xy += dx * dy;
6758                sum_x2 += dx * dx;
6759            }
6760
6761            if sum_x2 == 0.0 {
6762                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6763                    ExcelError::new_div(),
6764                )));
6765            }
6766
6767            let slope = sum_xy / sum_x2;
6768            let intercept = mean_y - slope * mean_x;
6769            (slope, intercept)
6770        } else {
6771            // Regression through origin (intercept = 0)
6772            let mut sum_xy = 0.0;
6773            let mut sum_x2 = 0.0;
6774
6775            for i in 0..x_vals.len() {
6776                sum_xy += x_vals[i] * y_vals[i];
6777                sum_x2 += x_vals[i] * x_vals[i];
6778            }
6779
6780            if sum_x2 == 0.0 {
6781                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6782                    ExcelError::new_div(),
6783                )));
6784            }
6785
6786            let slope = sum_xy / sum_x2;
6787            (slope, 0.0)
6788        };
6789
6790        // Calculate predicted y values for new_x's
6791        let predicted: Vec<LiteralValue> = new_x_vals
6792            .iter()
6793            .map(|&x| LiteralValue::Number(slope * x + intercept))
6794            .collect();
6795
6796        // Return as 1xN array (row vector)
6797        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Array(vec![
6798            predicted,
6799        ])))
6800    }
6801}
6802
6803/* ─────────────────────────── GROWTH ──────────────────────────── */
6804
6805/// Returns fitted values from an exponential trend model.
6806///
6807/// `GROWTH` fits `y = b * m^x` by linearizing in log space, then returns predictions for `new_x`.
6808///
6809/// # Remarks
6810/// - All known y-values must be strictly greater than `0`.
6811/// - `known_x` defaults to `1..n`; `new_x` defaults to `known_x`.
6812/// - `const` defaults to `TRUE`; set to `FALSE` to force `b = 1`.
6813/// - Returns spreadsheet errors for invalid domains, mismatched lengths, or degenerate x-variance.
6814///
6815/// # Examples
6816///
6817/// ```yaml,sandbox
6818/// title: "Exponential growth forecast"
6819/// formula: "=GROWTH({2,4,8},{1,2,3},{4,5})"
6820/// expected:
6821///   - [16, 32]
6822/// ```
6823///
6824/// ```yaml,sandbox
6825/// title: "Default x-values with perfect doubling pattern"
6826/// formula: "=GROWTH({3,6,12})"
6827/// expected:
6828///   - [3, 6, 12]
6829/// ```
6830#[derive(Debug)]
6831pub struct GrowthFn;
6832/// [formualizer-docgen:schema:start]
6833/// Name: GROWTH
6834/// Type: GrowthFn
6835/// Min args: 1
6836/// Max args: variadic
6837/// Variadic: true
6838/// Signature: GROWTH(arg1...: number@range)
6839/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
6840/// Caps: PURE, NUMERIC_ONLY
6841/// [formualizer-docgen:schema:end]
6842impl Function for GrowthFn {
6843    func_caps!(PURE, NUMERIC_ONLY);
6844    fn name(&self) -> &'static str {
6845        "GROWTH"
6846    }
6847    fn min_args(&self) -> usize {
6848        1
6849    }
6850    fn variadic(&self) -> bool {
6851        true
6852    }
6853    fn arg_schema(&self) -> &'static [ArgSchema] {
6854        &ARG_RANGE_NUM_LENIENT_ONE[..]
6855    }
6856    fn eval<'a, 'b, 'c>(
6857        &self,
6858        args: &'c [ArgumentHandle<'a, 'b>],
6859        _ctx: &dyn FunctionContext<'b>,
6860    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
6861        // GROWTH: args[0] = known_y's (required)
6862        // args[1] = known_x's (optional, defaults to {1,2,3,...})
6863        // args[2] = new_x's (optional, defaults to known_x's)
6864        // args[3] = const (optional, default TRUE - whether to compute intercept)
6865
6866        let y_vals = collect_numeric_stats(&args[0..1])?;
6867
6868        if y_vals.is_empty() {
6869            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6870                ExcelError::new_na(),
6871            )));
6872        }
6873
6874        // Check that all y values are positive (required for log transformation)
6875        for &y in &y_vals {
6876            if y <= 0.0 {
6877                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6878                    ExcelError::new_num(),
6879                )));
6880            }
6881        }
6882
6883        // Helper to check if argument is empty/omitted
6884        // Note: Empty arguments are represented as empty text strings by the parser
6885        fn is_arg_empty(arg: &ArgumentHandle) -> bool {
6886            match scalar_like_value(arg) {
6887                Ok(LiteralValue::Empty) => true,
6888                Ok(LiteralValue::Text(s)) if s.is_empty() => true,
6889                _ => false,
6890            }
6891        }
6892
6893        // Get known_x's or generate default {1, 2, 3, ...}
6894        let x_vals = if args.len() >= 2 && !is_arg_empty(&args[1]) {
6895            collect_numeric_stats(&args[1..2])?
6896        } else {
6897            (1..=y_vals.len()).map(|i| i as f64).collect()
6898        };
6899
6900        // Arrays must have same length
6901        if y_vals.len() != x_vals.len() {
6902            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6903                ExcelError::new_ref(),
6904            )));
6905        }
6906
6907        // Get new_x's or use known_x's - check if argument is empty/omitted
6908        let new_x_vals = if args.len() >= 3 && !is_arg_empty(&args[2]) {
6909            collect_numeric_stats(&args[2..3])?
6910        } else {
6911            x_vals.clone()
6912        };
6913
6914        if new_x_vals.is_empty() {
6915            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6916                ExcelError::new_na(),
6917            )));
6918        }
6919
6920        // Parse const argument (default TRUE)
6921        let use_const = if args.len() >= 4 {
6922            match scalar_like_value(&args[3])? {
6923                LiteralValue::Boolean(b) => b,
6924                LiteralValue::Number(n) => n != 0.0,
6925                LiteralValue::Int(i) => i != 0,
6926                LiteralValue::Empty => true, // empty defaults to TRUE
6927                _ => true,
6928            }
6929        } else {
6930            true
6931        };
6932
6933        // Transform to log space: ln(y) = ln(b) + x*ln(m)
6934        // This is linear regression on log-transformed y values
6935        let ln_y_vals: Vec<f64> = y_vals.iter().map(|&y| y.ln()).collect();
6936
6937        let n = x_vals.len() as f64;
6938
6939        // Calculate regression coefficients in log space
6940        let (ln_m, ln_b) = if use_const {
6941            // Normal linear regression with intercept
6942            let mean_x = x_vals.iter().sum::<f64>() / n;
6943            let mean_ln_y = ln_y_vals.iter().sum::<f64>() / n;
6944
6945            let mut sum_xy = 0.0;
6946            let mut sum_x2 = 0.0;
6947
6948            for i in 0..x_vals.len() {
6949                let dx = x_vals[i] - mean_x;
6950                let dy = ln_y_vals[i] - mean_ln_y;
6951                sum_xy += dx * dy;
6952                sum_x2 += dx * dx;
6953            }
6954
6955            if sum_x2 == 0.0 {
6956                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6957                    ExcelError::new_div(),
6958                )));
6959            }
6960
6961            let ln_m = sum_xy / sum_x2;
6962            let ln_b = mean_ln_y - ln_m * mean_x;
6963            (ln_m, ln_b)
6964        } else {
6965            // Regression through origin in log space (ln_b = 0, so b = 1)
6966            let mut sum_xy = 0.0;
6967            let mut sum_x2 = 0.0;
6968
6969            for i in 0..x_vals.len() {
6970                sum_xy += x_vals[i] * ln_y_vals[i];
6971                sum_x2 += x_vals[i] * x_vals[i];
6972            }
6973
6974            if sum_x2 == 0.0 {
6975                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6976                    ExcelError::new_div(),
6977                )));
6978            }
6979
6980            let ln_m = sum_xy / sum_x2;
6981            (ln_m, 0.0)
6982        };
6983
6984        // Convert back from log space: m = e^ln_m, b = e^ln_b
6985        let m = ln_m.exp();
6986        let b = ln_b.exp();
6987
6988        // Calculate predicted y values: y = b * m^x
6989        let predicted: Vec<LiteralValue> = new_x_vals
6990            .iter()
6991            .map(|&x| LiteralValue::Number(b * m.powf(x)))
6992            .collect();
6993
6994        // Return as 1xN array (row vector)
6995        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Array(vec![
6996            predicted,
6997        ])))
6998    }
6999}
7000
7001/* ─────────────────────────── LOGEST ──────────────────────────── */
7002
7003/// Returns parameters for an exponential model fitted to known data.
7004///
7005/// `LOGEST` fits `y = b * m^x` and returns either `[m, b]` or an expanded statistics matrix.
7006///
7007/// # Remarks
7008/// - All known y-values must be strictly greater than `0`.
7009/// - `known_x` defaults to `1..n` when omitted.
7010/// - `const` controls whether `b` is fitted (`TRUE` by default).
7011/// - `stats=TRUE` returns a `5x2` statistics block; otherwise returns `1x2`.
7012///
7013/// # Examples
7014///
7015/// ```yaml,sandbox
7016/// title: "Exponential base and intercept"
7017/// formula: "=LOGEST({2,4,8},{1,2,3})"
7018/// expected:
7019///   - [2, 1]
7020/// ```
7021///
7022/// ```yaml,sandbox
7023/// title: "Alternative growth series"
7024/// formula: "=LOGEST({3,6,12},{1,2,3})"
7025/// expected:
7026///   - [2, 1.5]
7027/// ```
7028#[derive(Debug)]
7029pub struct LogestFn;
7030/// [formualizer-docgen:schema:start]
7031/// Name: LOGEST
7032/// Type: LogestFn
7033/// Min args: 1
7034/// Max args: variadic
7035/// Variadic: true
7036/// Signature: LOGEST(arg1...: number@range)
7037/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
7038/// Caps: PURE, NUMERIC_ONLY
7039/// [formualizer-docgen:schema:end]
7040impl Function for LogestFn {
7041    func_caps!(PURE, NUMERIC_ONLY);
7042    fn name(&self) -> &'static str {
7043        "LOGEST"
7044    }
7045    fn min_args(&self) -> usize {
7046        1
7047    }
7048    fn variadic(&self) -> bool {
7049        true
7050    }
7051    fn arg_schema(&self) -> &'static [ArgSchema] {
7052        &ARG_RANGE_NUM_LENIENT_ONE[..]
7053    }
7054    fn eval<'a, 'b, 'c>(
7055        &self,
7056        args: &'c [ArgumentHandle<'a, 'b>],
7057        _ctx: &dyn FunctionContext<'b>,
7058    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
7059        // args[0] = known_y's (required)
7060        // args[1] = known_x's (optional, defaults to {1,2,3,...})
7061        // args[2] = const (optional, default TRUE - whether to compute b)
7062        // args[3] = stats (optional, default FALSE - whether to return additional statistics)
7063
7064        let y_vals = collect_numeric_stats(&args[0..1])?;
7065
7066        if y_vals.is_empty() {
7067            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7068                ExcelError::new_na(),
7069            )));
7070        }
7071
7072        // Check that all y values are positive (required for log transformation)
7073        for &y in &y_vals {
7074            if y <= 0.0 {
7075                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7076                    ExcelError::new_num(),
7077                )));
7078            }
7079        }
7080
7081        // Get known_x's or generate default {1, 2, 3, ...}
7082        let x_vals = if args.len() >= 2 {
7083            collect_numeric_stats(&args[1..2])?
7084        } else {
7085            (1..=y_vals.len()).map(|i| i as f64).collect()
7086        };
7087
7088        // Arrays must have same length
7089        if y_vals.len() != x_vals.len() {
7090            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7091                ExcelError::new_ref(),
7092            )));
7093        }
7094
7095        // Parse const argument (default TRUE)
7096        let use_const = if args.len() >= 3 {
7097            match scalar_like_value(&args[2])? {
7098                LiteralValue::Boolean(b) => b,
7099                LiteralValue::Number(n) => n != 0.0,
7100                LiteralValue::Int(i) => i != 0,
7101                _ => true,
7102            }
7103        } else {
7104            true
7105        };
7106
7107        // Parse stats argument (default FALSE)
7108        let return_stats = if args.len() >= 4 {
7109            match scalar_like_value(&args[3])? {
7110                LiteralValue::Boolean(b) => b,
7111                LiteralValue::Number(n) => n != 0.0,
7112                LiteralValue::Int(i) => i != 0,
7113                _ => false,
7114            }
7115        } else {
7116            false
7117        };
7118
7119        // Transform to log space: ln(y) = ln(b) + x*ln(m)
7120        let ln_y_vals: Vec<f64> = y_vals.iter().map(|&y| y.ln()).collect();
7121
7122        let n = x_vals.len() as f64;
7123
7124        // Calculate regression coefficients in log space
7125        let (ln_m, ln_b) = if use_const {
7126            // Normal linear regression with intercept
7127            let mean_x = x_vals.iter().sum::<f64>() / n;
7128            let mean_ln_y = ln_y_vals.iter().sum::<f64>() / n;
7129
7130            let mut sum_xy = 0.0;
7131            let mut sum_x2 = 0.0;
7132
7133            for i in 0..x_vals.len() {
7134                let dx = x_vals[i] - mean_x;
7135                let dy = ln_y_vals[i] - mean_ln_y;
7136                sum_xy += dx * dy;
7137                sum_x2 += dx * dx;
7138            }
7139
7140            if sum_x2 == 0.0 {
7141                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7142                    ExcelError::new_div(),
7143                )));
7144            }
7145
7146            let ln_m = sum_xy / sum_x2;
7147            let ln_b = mean_ln_y - ln_m * mean_x;
7148            (ln_m, ln_b)
7149        } else {
7150            // Regression through origin in log space (ln_b = 0, so b = 1)
7151            let mut sum_xy = 0.0;
7152            let mut sum_x2 = 0.0;
7153
7154            for i in 0..x_vals.len() {
7155                sum_xy += x_vals[i] * ln_y_vals[i];
7156                sum_x2 += x_vals[i] * x_vals[i];
7157            }
7158
7159            if sum_x2 == 0.0 {
7160                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7161                    ExcelError::new_div(),
7162                )));
7163            }
7164
7165            let ln_m = sum_xy / sum_x2;
7166            (ln_m, 0.0)
7167        };
7168
7169        // Convert from log space to get m and b
7170        let m = ln_m.exp();
7171        let b = ln_b.exp();
7172
7173        if !return_stats {
7174            // Return just m and b as 1x2 array: [[m, b]]
7175            let row = vec![LiteralValue::Number(m), LiteralValue::Number(b)];
7176            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Array(vec![
7177                row,
7178            ])));
7179        }
7180
7181        // Calculate additional statistics for stats=TRUE
7182        // Statistics are computed in log space, then converted
7183        // Row 1: [m, b]
7184        // Row 2: [se_m, se_b] - standard errors (converted from log space)
7185        // Row 3: [r_squared, se_y] - R-squared and standard error of y estimate
7186        // Row 4: [F_statistic, df] - F-statistic and degrees of freedom
7187        // Row 5: [ss_reg, ss_resid] - regression sum of squares and residual sum of squares
7188
7189        let mean_ln_y = ln_y_vals.iter().sum::<f64>() / n;
7190
7191        // Calculate residuals and sums of squares in log space
7192        let mut ss_resid = 0.0;
7193        let mut ss_tot = 0.0;
7194
7195        for i in 0..x_vals.len() {
7196            let ln_y_pred = ln_m * x_vals[i] + ln_b;
7197            let residual = ln_y_vals[i] - ln_y_pred;
7198            ss_resid += residual * residual;
7199            let dy_tot = ln_y_vals[i] - mean_ln_y;
7200            ss_tot += dy_tot * dy_tot;
7201        }
7202
7203        let ss_reg = ss_tot - ss_resid;
7204
7205        // R-squared (same in both spaces for transformed regression)
7206        let r_squared = if ss_tot == 0.0 {
7207            1.0
7208        } else {
7209            1.0 - (ss_resid / ss_tot)
7210        };
7211
7212        // Degrees of freedom
7213        let df = if use_const {
7214            (n as i64 - 2).max(1) as f64
7215        } else {
7216            (n as i64 - 1).max(1) as f64
7217        };
7218
7219        // Standard error of y estimate (in log space)
7220        let se_ln_y = if df > 0.0 {
7221            (ss_resid / df).sqrt()
7222        } else {
7223            0.0
7224        };
7225
7226        // Standard errors of coefficients in log space
7227        let mean_x = x_vals.iter().sum::<f64>() / n;
7228        let mut sum_x2_centered = 0.0;
7229        let mut sum_x2_raw = 0.0;
7230        for &xi in &x_vals {
7231            sum_x2_centered += (xi - mean_x).powi(2);
7232            sum_x2_raw += xi * xi;
7233        }
7234
7235        let se_ln_m = if sum_x2_centered > 0.0 && df > 0.0 {
7236            se_ln_y / sum_x2_centered.sqrt()
7237        } else {
7238            f64::NAN
7239        };
7240
7241        let se_ln_b = if use_const && sum_x2_centered > 0.0 && df > 0.0 {
7242            se_ln_y * (sum_x2_raw / (n * sum_x2_centered)).sqrt()
7243        } else {
7244            f64::NAN
7245        };
7246
7247        // Convert standard errors: se_m = m * se_ln_m (delta method approximation)
7248        let se_m = m * se_ln_m;
7249        let se_b = b * se_ln_b;
7250
7251        // Standard error of y estimate - convert from log space
7252        // This is an approximation; for exponential models, se_y in original space varies with x
7253        let se_y = se_ln_y;
7254
7255        // F-statistic
7256        let f_stat = if ss_resid > 0.0 && df > 0.0 {
7257            (ss_reg / 1.0) / (ss_resid / df)
7258        } else if ss_resid == 0.0 {
7259            f64::INFINITY
7260        } else {
7261            f64::NAN
7262        };
7263
7264        // Build 5x2 result array
7265        let rows = vec![
7266            vec![LiteralValue::Number(m), LiteralValue::Number(b)],
7267            vec![LiteralValue::Number(se_m), LiteralValue::Number(se_b)],
7268            vec![LiteralValue::Number(r_squared), LiteralValue::Number(se_y)],
7269            vec![LiteralValue::Number(f_stat), LiteralValue::Number(df)],
7270            vec![LiteralValue::Number(ss_reg), LiteralValue::Number(ss_resid)],
7271        ];
7272
7273        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Array(rows)))
7274    }
7275}
7276
7277/* ─────────────────────────── PERCENTRANK ──────────────────────────── */
7278
7279/// Returns the inclusive percentile rank of `x` within a numeric data array.
7280///
7281/// `PERCENTRANK.INC` maps values to `[0, 1]` and interpolates linearly between data points.
7282///
7283/// # Remarks
7284/// - `x` must be within the observed min/max range; otherwise returns `#N/A`.
7285/// - Optional `significance` controls decimal truncation and defaults to `3`.
7286/// - `significance` must be at least `1`.
7287/// - Returns `#NUM!` for invalid setup such as empty numeric input.
7288///
7289/// # Examples
7290///
7291/// ```yaml,sandbox
7292/// title: "Exact inclusive percentile rank"
7293/// formula: "=PERCENTRANK.INC({1,2,3,4,5},3)"
7294/// expected: 0.5
7295/// ```
7296///
7297/// ```yaml,sandbox
7298/// title: "Interpolated inclusive percentile rank"
7299/// formula: "=PERCENTRANK.INC({1,2,3,4,5},2.5)"
7300/// expected: 0.375
7301/// ```
7302#[derive(Debug)]
7303pub struct PercentRankIncFn;
7304/// [formualizer-docgen:schema:start]
7305/// Name: PERCENTRANK.INC
7306/// Type: PercentRankIncFn
7307/// Min args: 2
7308/// Max args: variadic
7309/// Variadic: true
7310/// Signature: PERCENTRANK.INC(arg1...: number@range)
7311/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
7312/// Caps: PURE, NUMERIC_ONLY
7313/// [formualizer-docgen:schema:end]
7314impl Function for PercentRankIncFn {
7315    func_caps!(PURE, NUMERIC_ONLY);
7316    fn name(&self) -> &'static str {
7317        "PERCENTRANK.INC"
7318    }
7319    fn aliases(&self) -> &'static [&'static str] {
7320        &["PERCENTRANK"]
7321    }
7322    fn min_args(&self) -> usize {
7323        2
7324    }
7325    fn variadic(&self) -> bool {
7326        true
7327    }
7328    fn arg_schema(&self) -> &'static [ArgSchema] {
7329        &ARG_RANGE_NUM_LENIENT_ONE[..]
7330    }
7331    fn eval<'a, 'b, 'c>(
7332        &self,
7333        args: &'c [ArgumentHandle<'a, 'b>],
7334        _ctx: &dyn FunctionContext<'b>,
7335    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
7336        if args.len() < 2 {
7337            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7338                ExcelError::new_num(),
7339            )));
7340        }
7341
7342        // Get x value (the value to find the rank of)
7343        let x = match coerce_num(&scalar_like_value(&args[1])?) {
7344            Ok(n) => n,
7345            Err(_) => {
7346                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7347                    ExcelError::new_num(),
7348                )));
7349            }
7350        };
7351
7352        // Get optional significance (default 3)
7353        let significance = if args.len() > 2 {
7354            match coerce_num(&scalar_like_value(&args[2])?) {
7355                Ok(n) => {
7356                    let s = n as i32;
7357                    if s < 1 {
7358                        return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7359                            ExcelError::new_num(),
7360                        )));
7361                    }
7362                    s as u32
7363                }
7364                Err(_) => 3,
7365            }
7366        } else {
7367            3
7368        };
7369
7370        // Collect and sort the data array
7371        let mut nums = collect_numeric_stats(&args[0..1])?;
7372        if nums.is_empty() {
7373            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7374                ExcelError::new_num(),
7375            )));
7376        }
7377        nums.sort_by(|a, b| a.partial_cmp(b).unwrap());
7378
7379        let n = nums.len();
7380
7381        // Check if x is outside the range
7382        if x < nums[0] || x > nums[n - 1] {
7383            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7384                ExcelError::new_na(),
7385            )));
7386        }
7387
7388        // Find the rank using linear interpolation
7389        // For PERCENTRANK.INC, the formula is: rank = (position) / (n-1)
7390        // where position is 0-based and uses linear interpolation
7391        let rank = if n == 1 {
7392            // Single element - rank is 0 (or 1.0 if we want, but Excel returns 0)
7393            0.0
7394        } else {
7395            let mut rank_val = 0.0;
7396            for i in 0..n - 1 {
7397                if (nums[i] - x).abs() < 1e-12 {
7398                    // Exact match at position i
7399                    rank_val = (i as f64) / ((n - 1) as f64);
7400                    break;
7401                } else if nums[i] < x && x < nums[i + 1] {
7402                    // Interpolate between positions i and i+1
7403                    let frac = (x - nums[i]) / (nums[i + 1] - nums[i]);
7404                    rank_val = ((i as f64) + frac) / ((n - 1) as f64);
7405                    break;
7406                } else if i == n - 2 && (nums[n - 1] - x).abs() < 1e-12 {
7407                    // Exact match at last position
7408                    rank_val = 1.0;
7409                }
7410            }
7411            rank_val
7412        };
7413
7414        // Truncate to significance decimal places
7415        let multiplier = 10_f64.powi(significance as i32);
7416        let truncated = (rank * multiplier).trunc() / multiplier;
7417
7418        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
7419            truncated,
7420        )))
7421    }
7422}
7423
7424/// Returns the exclusive percentile rank of `x` within a numeric data array.
7425///
7426/// `PERCENTRANK.EXC` uses an open ranking scale that excludes exact `0` and `1` endpoints.
7427///
7428/// # Remarks
7429/// - `x` must lie within the observed min/max range; otherwise returns `#N/A`.
7430/// - Output is based on position divided by `n + 1`, with interpolation between points.
7431/// - Optional `significance` defaults to `3` and must be at least `1`.
7432/// - Returns `#NUM!` for invalid setup such as empty numeric input.
7433///
7434/// # Examples
7435///
7436/// ```yaml,sandbox
7437/// title: "Exact exclusive percentile rank"
7438/// formula: "=PERCENTRANK.EXC({1,2,3,4,5},3)"
7439/// expected: 0.5
7440/// ```
7441///
7442/// ```yaml,sandbox
7443/// title: "Interpolated exclusive percentile rank"
7444/// formula: "=PERCENTRANK.EXC({1,2,3,4,5},2.5)"
7445/// expected: 0.416
7446/// ```
7447#[derive(Debug)]
7448pub struct PercentRankExcFn;
7449/// [formualizer-docgen:schema:start]
7450/// Name: PERCENTRANK.EXC
7451/// Type: PercentRankExcFn
7452/// Min args: 2
7453/// Max args: variadic
7454/// Variadic: true
7455/// Signature: PERCENTRANK.EXC(arg1...: number@range)
7456/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
7457/// Caps: PURE, NUMERIC_ONLY
7458/// [formualizer-docgen:schema:end]
7459impl Function for PercentRankExcFn {
7460    func_caps!(PURE, NUMERIC_ONLY);
7461    fn name(&self) -> &'static str {
7462        "PERCENTRANK.EXC"
7463    }
7464    fn min_args(&self) -> usize {
7465        2
7466    }
7467    fn variadic(&self) -> bool {
7468        true
7469    }
7470    fn arg_schema(&self) -> &'static [ArgSchema] {
7471        &ARG_RANGE_NUM_LENIENT_ONE[..]
7472    }
7473    fn eval<'a, 'b, 'c>(
7474        &self,
7475        args: &'c [ArgumentHandle<'a, 'b>],
7476        _ctx: &dyn FunctionContext<'b>,
7477    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
7478        if args.len() < 2 {
7479            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7480                ExcelError::new_num(),
7481            )));
7482        }
7483
7484        // Get x value (the value to find the rank of)
7485        let x = match coerce_num(&scalar_like_value(&args[1])?) {
7486            Ok(n) => n,
7487            Err(_) => {
7488                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7489                    ExcelError::new_num(),
7490                )));
7491            }
7492        };
7493
7494        // Get optional significance (default 3)
7495        let significance = if args.len() > 2 {
7496            match coerce_num(&scalar_like_value(&args[2])?) {
7497                Ok(n) => {
7498                    let s = n as i32;
7499                    if s < 1 {
7500                        return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7501                            ExcelError::new_num(),
7502                        )));
7503                    }
7504                    s as u32
7505                }
7506                Err(_) => 3,
7507            }
7508        } else {
7509            3
7510        };
7511
7512        // Collect and sort the data array
7513        let mut nums = collect_numeric_stats(&args[0..1])?;
7514        if nums.is_empty() {
7515            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7516                ExcelError::new_num(),
7517            )));
7518        }
7519        nums.sort_by(|a, b| a.partial_cmp(b).unwrap());
7520
7521        let n = nums.len();
7522
7523        // Check if x is outside the range
7524        if x < nums[0] || x > nums[n - 1] {
7525            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7526                ExcelError::new_na(),
7527            )));
7528        }
7529
7530        // For PERCENTRANK.EXC, the formula is: rank = position / (n+1)
7531        // where position is 1-based and uses linear interpolation
7532        let rank = {
7533            let mut rank_val = 0.0;
7534            for i in 0..n {
7535                if (nums[i] - x).abs() < 1e-12 {
7536                    // Exact match at position i (1-based: i+1)
7537                    rank_val = ((i + 1) as f64) / ((n + 1) as f64);
7538                    break;
7539                } else if i < n - 1 && nums[i] < x && x < nums[i + 1] {
7540                    // Interpolate between positions i and i+1 (1-based: i+1 and i+2)
7541                    let frac = (x - nums[i]) / (nums[i + 1] - nums[i]);
7542                    let position = ((i + 1) as f64) + frac;
7543                    rank_val = position / ((n + 1) as f64);
7544                    break;
7545                }
7546            }
7547            rank_val
7548        };
7549
7550        // Truncate to significance decimal places
7551        let multiplier = 10_f64.powi(significance as i32);
7552        let truncated = (rank * multiplier).trunc() / multiplier;
7553
7554        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
7555            truncated,
7556        )))
7557    }
7558}
7559
7560/* ─────────────────────────── FREQUENCY ──────────────────────────── */
7561
7562/// Returns a vertical frequency distribution for numeric data across bin cutoffs.
7563///
7564/// `FREQUENCY` counts values into `<= first bin`, intermediate right-closed bins, and an overflow
7565/// bucket above the final bin.
7566///
7567/// # Remarks
7568/// - Returns an array with `bins + 1` rows.
7569/// - Bins are sorted before counting.
7570/// - If `bins_array` has no numeric values, result is a single count of all data points.
7571/// - Non-numeric values in input ranges are ignored by statistical-collection rules.
7572///
7573/// # Examples
7574///
7575/// ```yaml,sandbox
7576/// title: "Frequency buckets with two bins"
7577/// formula: "=FREQUENCY({1,2,3,4,5},{2,4})"
7578/// expected:
7579///   - [2]
7580///   - [2]
7581///   - [1]
7582/// ```
7583///
7584/// ```yaml,sandbox
7585/// title: "Frequency with repeated values"
7586/// formula: "=FREQUENCY({1,1,2,2,3},{1,2})"
7587/// expected:
7588///   - [2]
7589///   - [2]
7590///   - [1]
7591/// ```
7592#[derive(Debug)]
7593pub struct FrequencyFn;
7594/// [formualizer-docgen:schema:start]
7595/// Name: FREQUENCY
7596/// Type: FrequencyFn
7597/// Min args: 2
7598/// Max args: 1
7599/// Variadic: false
7600/// Signature: FREQUENCY(arg1: number@range)
7601/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
7602/// Caps: PURE, NUMERIC_ONLY
7603/// [formualizer-docgen:schema:end]
7604impl Function for FrequencyFn {
7605    func_caps!(PURE, NUMERIC_ONLY);
7606    fn name(&self) -> &'static str {
7607        "FREQUENCY"
7608    }
7609    fn min_args(&self) -> usize {
7610        2
7611    }
7612    fn variadic(&self) -> bool {
7613        false
7614    }
7615    fn arg_schema(&self) -> &'static [ArgSchema] {
7616        &ARG_RANGE_NUM_LENIENT_ONE[..]
7617    }
7618    fn eval<'a, 'b, 'c>(
7619        &self,
7620        args: &'c [ArgumentHandle<'a, 'b>],
7621        _ctx: &dyn FunctionContext<'b>,
7622    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
7623        if args.len() < 2 {
7624            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7625                ExcelError::new_num(),
7626            )));
7627        }
7628
7629        // Collect data array
7630        let data = collect_numeric_stats(&args[0..1])?;
7631
7632        // Collect bins array
7633        let mut bins = collect_numeric_stats(&args[1..2])?;
7634
7635        // Handle empty bins - return single count of all data
7636        if bins.is_empty() {
7637            let rows = vec![vec![LiteralValue::Number(data.len() as f64)]];
7638            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Array(rows)));
7639        }
7640
7641        // Sort bins
7642        bins.sort_by(|a, b| a.partial_cmp(b).unwrap());
7643
7644        // Calculate frequencies
7645        // Result has bins.len() + 1 elements
7646        let mut frequencies = vec![0usize; bins.len() + 1];
7647
7648        for &value in &data {
7649            // Find which bin the value belongs to
7650            let mut found = false;
7651            for (i, &bin) in bins.iter().enumerate() {
7652                if i == 0 {
7653                    // First bin: count values <= bins[0]
7654                    if value <= bin {
7655                        frequencies[0] += 1;
7656                        found = true;
7657                        break;
7658                    }
7659                } else {
7660                    // Intermediate bins: count values > bins[i-1] AND <= bins[i]
7661                    if value <= bin {
7662                        frequencies[i] += 1;
7663                        found = true;
7664                        break;
7665                    }
7666                }
7667            }
7668            // Last bin: values > bins[last]
7669            if !found {
7670                frequencies[bins.len()] += 1;
7671            }
7672        }
7673
7674        // Return as vertical array (column vector)
7675        let rows: Vec<Vec<LiteralValue>> = frequencies
7676            .into_iter()
7677            .map(|f| vec![LiteralValue::Number(f as f64)])
7678            .collect();
7679
7680        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Array(rows)))
7681    }
7682}
7683
7684/* ─────────────────────────── T.DIST.2T ──────────────────────────── */
7685
7686/// Returns the two-tailed Student's t probability beyond `x`.
7687///
7688/// `T.DIST.2T` computes `P(|T| > x)` for the specified degrees of freedom.
7689///
7690/// # Remarks
7691/// - Requires `x >= 0` and `deg_freedom >= 1`.
7692/// - Represents a two-sided tail area.
7693/// - Returns `#NUM!` when arguments are outside valid ranges.
7694/// - Invalid numeric coercions propagate as spreadsheet errors.
7695///
7696/// # Examples
7697///
7698/// ```yaml,sandbox
7699/// title: "Two-tailed t probability at zero"
7700/// formula: "=T.DIST.2T(0,10)"
7701/// expected: 1
7702/// ```
7703///
7704/// ```yaml,sandbox
7705/// title: "Two-tailed t probability at x=2"
7706/// formula: "=T.DIST.2T(2,10)"
7707/// expected: 0.0733880342639167
7708/// ```
7709#[derive(Debug)]
7710pub struct TDist2TFn;
7711/// [formualizer-docgen:schema:start]
7712/// Name: T.DIST.2T
7713/// Type: TDist2TFn
7714/// Min args: 2
7715/// Max args: 2
7716/// Variadic: false
7717/// Signature: T.DIST.2T(arg1: number@scalar, arg2: number@scalar)
7718/// 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}
7719/// Caps: PURE
7720/// [formualizer-docgen:schema:end]
7721impl Function for TDist2TFn {
7722    func_caps!(PURE);
7723    fn name(&self) -> &'static str {
7724        "T.DIST.2T"
7725    }
7726    fn min_args(&self) -> usize {
7727        2
7728    }
7729    fn arg_schema(&self) -> &'static [ArgSchema] {
7730        use std::sync::LazyLock;
7731        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
7732            vec![
7733                ArgSchema::number_lenient_scalar(),
7734                ArgSchema::number_lenient_scalar(),
7735            ]
7736        });
7737        &SCHEMA[..]
7738    }
7739    fn eval<'a, 'b, 'c>(
7740        &self,
7741        args: &'c [ArgumentHandle<'a, 'b>],
7742        _ctx: &dyn FunctionContext<'b>,
7743    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
7744        let x = coerce_num(&scalar_like_value(&args[0])?)?;
7745        let df = coerce_num(&scalar_like_value(&args[1])?)?;
7746
7747        // x must be non-negative for T.DIST.2T, df must be >= 1
7748        if x < 0.0 || df < 1.0 {
7749            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7750                ExcelError::new_num(),
7751            )));
7752        }
7753
7754        // Two-tailed: P(|T| > x) = 2 * (1 - t_cdf(x, df))
7755        let p = 2.0 * (1.0 - t_cdf(x, df));
7756        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(p)))
7757    }
7758}
7759
7760/* ─────────────────────────── T.INV.2T ──────────────────────────── */
7761
7762/// Returns the positive t critical value for a two-tailed probability.
7763///
7764/// `T.INV.2T` solves for `t` such that `P(|T| > t) = probability`.
7765///
7766/// # Remarks
7767/// - `probability` must satisfy `0 < probability <= 1`.
7768/// - `deg_freedom` must be at least `1`.
7769/// - Returns `#NUM!` for invalid probability or degree-of-freedom arguments.
7770/// - Alias `TINV` is supported.
7771///
7772/// # Examples
7773///
7774/// ```yaml,sandbox
7775/// title: "Maximum two-tailed probability"
7776/// formula: "=T.INV.2T(1,10)"
7777/// expected: 0
7778/// ```
7779///
7780/// ```yaml,sandbox
7781/// title: "95% two-sided critical value"
7782/// formula: "=T.INV.2T(0.05,10)"
7783/// expected: 2.228138851986273
7784/// ```
7785#[derive(Debug)]
7786pub struct TInv2TFn;
7787/// [formualizer-docgen:schema:start]
7788/// Name: T.INV.2T
7789/// Type: TInv2TFn
7790/// Min args: 2
7791/// Max args: 2
7792/// Variadic: false
7793/// Signature: T.INV.2T(arg1: number@scalar, arg2: number@scalar)
7794/// 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}
7795/// Caps: PURE
7796/// [formualizer-docgen:schema:end]
7797impl Function for TInv2TFn {
7798    func_caps!(PURE);
7799    fn name(&self) -> &'static str {
7800        "T.INV.2T"
7801    }
7802    fn aliases(&self) -> &'static [&'static str] {
7803        &["TINV"]
7804    }
7805    fn min_args(&self) -> usize {
7806        2
7807    }
7808    fn arg_schema(&self) -> &'static [ArgSchema] {
7809        use std::sync::LazyLock;
7810        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
7811            vec![
7812                ArgSchema::number_lenient_scalar(),
7813                ArgSchema::number_lenient_scalar(),
7814            ]
7815        });
7816        &SCHEMA[..]
7817    }
7818    fn eval<'a, 'b, 'c>(
7819        &self,
7820        args: &'c [ArgumentHandle<'a, 'b>],
7821        _ctx: &dyn FunctionContext<'b>,
7822    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
7823        let p = coerce_num(&scalar_like_value(&args[0])?)?;
7824        let df = coerce_num(&scalar_like_value(&args[1])?)?;
7825
7826        // probability must be in (0, 1], df >= 1
7827        if p <= 0.0 || p > 1.0 || df < 1.0 {
7828            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7829                ExcelError::new_num(),
7830            )));
7831        }
7832
7833        // For two-tailed: we want t such that P(|T| > t) = p
7834        // P(|T| > t) = 2 * (1 - F(t)) where F is CDF
7835        // So 1 - F(t) = p/2, meaning F(t) = 1 - p/2
7836        // Thus t = t_inv(1 - p/2, df)
7837        match t_inv(1.0 - p / 2.0, df) {
7838            Some(result) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
7839                result,
7840            ))),
7841            None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7842                ExcelError::new_num(),
7843            ))),
7844        }
7845    }
7846}
7847
7848/* ─────────────────────────── T.TEST ──────────────────────────── */
7849
7850/// Returns the p-value from a Student t-test comparing two numeric samples.
7851///
7852/// `T.TEST` supports paired, equal-variance two-sample, and unequal-variance (Welch) modes.
7853///
7854/// # Remarks
7855/// - `tails` must be `1` (one-tailed) or `2` (two-tailed).
7856/// - `type` must be `1` (paired), `2` (two-sample equal variance), or `3` (Welch).
7857/// - Returns `#N/A` when paired mode arrays have different lengths.
7858/// - Returns `#NUM!` or `#DIV/0!` for invalid setup or degenerate variance conditions.
7859///
7860/// # Examples
7861///
7862/// ```yaml,sandbox
7863/// title: "Two-tailed equal-variance test with identical samples"
7864/// formula: "=T.TEST({1,2,3},{1,2,3},2,2)"
7865/// expected: 1
7866/// ```
7867///
7868/// ```yaml,sandbox
7869/// title: "One-tailed Welch test with identical samples"
7870/// formula: "=T.TEST({1,2,3},{1,2,3},1,3)"
7871/// expected: 0.5
7872/// ```
7873#[derive(Debug)]
7874pub struct TTestFn;
7875/// [formualizer-docgen:schema:start]
7876/// Name: T.TEST
7877/// Type: TTestFn
7878/// Min args: 4
7879/// Max args: 4
7880/// Variadic: false
7881/// Signature: T.TEST(arg1: number@range, arg2: number@range, arg3: number@scalar, arg4: number@scalar)
7882/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=range,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=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
7883/// Caps: PURE
7884/// [formualizer-docgen:schema:end]
7885impl Function for TTestFn {
7886    func_caps!(PURE);
7887    fn name(&self) -> &'static str {
7888        "T.TEST"
7889    }
7890    fn aliases(&self) -> &'static [&'static str] {
7891        &["TTEST"]
7892    }
7893    fn min_args(&self) -> usize {
7894        4
7895    }
7896    fn arg_schema(&self) -> &'static [ArgSchema] {
7897        use std::sync::LazyLock;
7898        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
7899            vec![
7900                {
7901                    let mut s = ArgSchema::number_lenient_scalar();
7902                    s.shape = crate::args::ShapeKind::Range;
7903                    s.coercion = formualizer_common::CoercionPolicy::NumberLenientText;
7904                    s
7905                },
7906                {
7907                    let mut s = ArgSchema::number_lenient_scalar();
7908                    s.shape = crate::args::ShapeKind::Range;
7909                    s.coercion = formualizer_common::CoercionPolicy::NumberLenientText;
7910                    s
7911                },
7912                ArgSchema::number_lenient_scalar(), // tails
7913                ArgSchema::number_lenient_scalar(), // type
7914            ]
7915        });
7916        &SCHEMA[..]
7917    }
7918    fn eval<'a, 'b, 'c>(
7919        &self,
7920        args: &'c [ArgumentHandle<'a, 'b>],
7921        _ctx: &dyn FunctionContext<'b>,
7922    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
7923        let array1 = collect_numeric_stats(&args[0..1])?;
7924        let array2 = collect_numeric_stats(&args[1..2])?;
7925        let tails = coerce_num(&scalar_like_value(&args[2])?)? as i32;
7926        let test_type = coerce_num(&scalar_like_value(&args[3])?)? as i32;
7927
7928        // Validate tails (1 or 2) and type (1, 2, or 3)
7929        if !(1..=2).contains(&tails) || !(1..=3).contains(&test_type) {
7930            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7931                ExcelError::new_num(),
7932            )));
7933        }
7934
7935        let n1 = array1.len();
7936        let n2 = array2.len();
7937
7938        // For paired test, arrays must have same length
7939        if test_type == 1 && n1 != n2 {
7940            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7941                ExcelError::new_na(),
7942            )));
7943        }
7944
7945        // Need at least 2 data points for meaningful t-test
7946        if n1 < 2 || n2 < 2 {
7947            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7948                ExcelError::new_num(),
7949            )));
7950        }
7951
7952        let (t_stat, df) = match test_type {
7953            1 => {
7954                // Paired t-test
7955                let n = n1 as f64;
7956                let diffs: Vec<f64> = array1
7957                    .iter()
7958                    .zip(array2.iter())
7959                    .map(|(a, b)| a - b)
7960                    .collect();
7961                let mean_diff = diffs.iter().sum::<f64>() / n;
7962                let var_diff =
7963                    diffs.iter().map(|d| (d - mean_diff).powi(2)).sum::<f64>() / (n - 1.0);
7964                if var_diff == 0.0 {
7965                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7966                        ExcelError::new_div(),
7967                    )));
7968                }
7969                let se = (var_diff / n).sqrt();
7970                (mean_diff / se, n - 1.0)
7971            }
7972            2 => {
7973                // Two-sample equal variance (pooled)
7974                let n1f = n1 as f64;
7975                let n2f = n2 as f64;
7976                let mean1 = array1.iter().sum::<f64>() / n1f;
7977                let mean2 = array2.iter().sum::<f64>() / n2f;
7978                let var1 = array1.iter().map(|x| (x - mean1).powi(2)).sum::<f64>() / (n1f - 1.0);
7979                let var2 = array2.iter().map(|x| (x - mean2).powi(2)).sum::<f64>() / (n2f - 1.0);
7980
7981                // Pooled variance
7982                let sp2 = ((n1f - 1.0) * var1 + (n2f - 1.0) * var2) / (n1f + n2f - 2.0);
7983                if sp2 == 0.0 {
7984                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7985                        ExcelError::new_div(),
7986                    )));
7987                }
7988                let se = (sp2 * (1.0 / n1f + 1.0 / n2f)).sqrt();
7989                ((mean1 - mean2) / se, n1f + n2f - 2.0)
7990            }
7991            3 => {
7992                // Welch's t-test (unequal variance)
7993                let n1f = n1 as f64;
7994                let n2f = n2 as f64;
7995                let mean1 = array1.iter().sum::<f64>() / n1f;
7996                let mean2 = array2.iter().sum::<f64>() / n2f;
7997                let var1 = array1.iter().map(|x| (x - mean1).powi(2)).sum::<f64>() / (n1f - 1.0);
7998                let var2 = array2.iter().map(|x| (x - mean2).powi(2)).sum::<f64>() / (n2f - 1.0);
7999
8000                let s1_n = var1 / n1f;
8001                let s2_n = var2 / n2f;
8002                let se = (s1_n + s2_n).sqrt();
8003                if se == 0.0 {
8004                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8005                        ExcelError::new_div(),
8006                    )));
8007                }
8008
8009                // Welch-Satterthwaite degrees of freedom
8010                let df_num = (s1_n + s2_n).powi(2);
8011                let df_denom = s1_n.powi(2) / (n1f - 1.0) + s2_n.powi(2) / (n2f - 1.0);
8012                let df = if df_denom == 0.0 {
8013                    1.0
8014                } else {
8015                    df_num / df_denom
8016                };
8017                ((mean1 - mean2) / se, df)
8018            }
8019            _ => unreachable!(),
8020        };
8021
8022        // Calculate p-value based on tails
8023        let t_abs = t_stat.abs();
8024        let p = if tails == 1 {
8025            1.0 - t_cdf(t_abs, df)
8026        } else {
8027            2.0 * (1.0 - t_cdf(t_abs, df))
8028        };
8029
8030        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(p)))
8031    }
8032}
8033
8034/* ─────────────────────────── F.TEST ──────────────────────────── */
8035
8036/// Returns the two-tailed p-value from an F-test comparing sample variances.
8037///
8038/// `F.TEST` evaluates whether two samples have significantly different variances.
8039///
8040/// # Remarks
8041/// - Each array must contain at least two numeric values.
8042/// - Uses sample variances and computes a two-tailed probability.
8043/// - Returns `#DIV/0!` when either sample variance is zero.
8044/// - Alias `FTEST` is supported.
8045///
8046/// # Examples
8047///
8048/// ```yaml,sandbox
8049/// title: "Identical samples yield p-value 1"
8050/// formula: "=F.TEST({1,2,3,4},{1,2,3,4})"
8051/// expected: 1
8052/// ```
8053///
8054/// ```yaml,sandbox
8055/// title: "Different variances example"
8056/// formula: "=F.TEST({1,2,3,4},{1,1,1,5})"
8057/// expected: 0.5466810975407987
8058/// ```
8059#[derive(Debug)]
8060pub struct FTestFn;
8061/// [formualizer-docgen:schema:start]
8062/// Name: F.TEST
8063/// Type: FTestFn
8064/// Min args: 2
8065/// Max args: 2
8066/// Variadic: false
8067/// Signature: F.TEST(arg1: number@range, arg2: number@range)
8068/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
8069/// Caps: PURE
8070/// [formualizer-docgen:schema:end]
8071impl Function for FTestFn {
8072    func_caps!(PURE);
8073    fn name(&self) -> &'static str {
8074        "F.TEST"
8075    }
8076    fn aliases(&self) -> &'static [&'static str] {
8077        &["FTEST"]
8078    }
8079    fn min_args(&self) -> usize {
8080        2
8081    }
8082    fn arg_schema(&self) -> &'static [ArgSchema] {
8083        use std::sync::LazyLock;
8084        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
8085            vec![
8086                {
8087                    let mut s = ArgSchema::number_lenient_scalar();
8088                    s.shape = crate::args::ShapeKind::Range;
8089                    s.coercion = formualizer_common::CoercionPolicy::NumberLenientText;
8090                    s
8091                },
8092                {
8093                    let mut s = ArgSchema::number_lenient_scalar();
8094                    s.shape = crate::args::ShapeKind::Range;
8095                    s.coercion = formualizer_common::CoercionPolicy::NumberLenientText;
8096                    s
8097                },
8098            ]
8099        });
8100        &SCHEMA[..]
8101    }
8102    fn eval<'a, 'b, 'c>(
8103        &self,
8104        args: &'c [ArgumentHandle<'a, 'b>],
8105        _ctx: &dyn FunctionContext<'b>,
8106    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
8107        let array1 = collect_numeric_stats(&args[0..1])?;
8108        let array2 = collect_numeric_stats(&args[1..2])?;
8109
8110        let n1 = array1.len();
8111        let n2 = array2.len();
8112
8113        // Need at least 2 points in each array
8114        if n1 < 2 || n2 < 2 {
8115            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8116                ExcelError::new_div(),
8117            )));
8118        }
8119
8120        let n1f = n1 as f64;
8121        let n2f = n2 as f64;
8122
8123        let mean1 = array1.iter().sum::<f64>() / n1f;
8124        let mean2 = array2.iter().sum::<f64>() / n2f;
8125
8126        let var1 = array1.iter().map(|x| (x - mean1).powi(2)).sum::<f64>() / (n1f - 1.0);
8127        let var2 = array2.iter().map(|x| (x - mean2).powi(2)).sum::<f64>() / (n2f - 1.0);
8128
8129        // Handle zero variance
8130        if var1 == 0.0 || var2 == 0.0 {
8131            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8132                ExcelError::new_div(),
8133            )));
8134        }
8135
8136        // F-statistic: Excel's F.TEST uses var1/var2 (order matters for degrees of freedom)
8137        // and returns two-tailed p-value
8138        let f = var1 / var2;
8139        let df1 = n1f - 1.0;
8140        let df2 = n2f - 1.0;
8141
8142        // Two-tailed p-value: min(F.DIST(f), 1-F.DIST(f)) * 2
8143        let p_lower = f_cdf(f, df1, df2);
8144        let p_upper = 1.0 - p_lower;
8145        let p = 2.0 * p_lower.min(p_upper);
8146
8147        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(p)))
8148    }
8149}
8150
8151/* ─────────────────────────── CHISQ.TEST ──────────────────────────── */
8152
8153/// Returns the right-tail p-value from a chi-square goodness-of-fit style comparison.
8154///
8155/// `CHISQ.TEST` compares observed and expected values and computes `1 - CHISQ.DIST(...)`.
8156///
8157/// # Remarks
8158/// - `actual_range` and `expected_range` must contain the same number of numeric points.
8159/// - Expected values must be strictly greater than `0`.
8160/// - Requires at least two categories (`df >= 1`).
8161/// - Returns `#N/A` for length mismatches or empty inputs, and `#NUM!` for invalid expected values.
8162///
8163/// # Examples
8164///
8165/// ```yaml,sandbox
8166/// title: "Perfect match between observed and expected"
8167/// formula: "=CHISQ.TEST({20,30,50},{20,30,50})"
8168/// expected: 1
8169/// ```
8170///
8171/// ```yaml,sandbox
8172/// title: "Two-category chi-square test"
8173/// formula: "=CHISQ.TEST({18,22},{20,20})"
8174/// expected: 0.5270892568655381
8175/// ```
8176#[derive(Debug)]
8177pub struct ChisqTestFn;
8178/// [formualizer-docgen:schema:start]
8179/// Name: CHISQ.TEST
8180/// Type: ChisqTestFn
8181/// Min args: 2
8182/// Max args: 2
8183/// Variadic: false
8184/// Signature: CHISQ.TEST(arg1: number@range, arg2: number@range)
8185/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
8186/// Caps: PURE
8187/// [formualizer-docgen:schema:end]
8188impl Function for ChisqTestFn {
8189    func_caps!(PURE);
8190    fn name(&self) -> &'static str {
8191        "CHISQ.TEST"
8192    }
8193    fn aliases(&self) -> &'static [&'static str] {
8194        &["CHITEST"]
8195    }
8196    fn min_args(&self) -> usize {
8197        2
8198    }
8199    fn arg_schema(&self) -> &'static [ArgSchema] {
8200        use std::sync::LazyLock;
8201        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
8202            vec![
8203                {
8204                    let mut s = ArgSchema::number_lenient_scalar();
8205                    s.shape = crate::args::ShapeKind::Range;
8206                    s.coercion = formualizer_common::CoercionPolicy::NumberLenientText;
8207                    s
8208                },
8209                {
8210                    let mut s = ArgSchema::number_lenient_scalar();
8211                    s.shape = crate::args::ShapeKind::Range;
8212                    s.coercion = formualizer_common::CoercionPolicy::NumberLenientText;
8213                    s
8214                },
8215            ]
8216        });
8217        &SCHEMA[..]
8218    }
8219    fn eval<'a, 'b, 'c>(
8220        &self,
8221        args: &'c [ArgumentHandle<'a, 'b>],
8222        _ctx: &dyn FunctionContext<'b>,
8223    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
8224        let actual = collect_numeric_stats(&args[0..1])?;
8225        let expected = collect_numeric_stats(&args[1..2])?;
8226
8227        // Arrays must have same length
8228        if actual.len() != expected.len() {
8229            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8230                ExcelError::new_na(),
8231            )));
8232        }
8233
8234        if actual.is_empty() {
8235            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8236                ExcelError::new_na(),
8237            )));
8238        }
8239
8240        // Calculate chi-squared statistic: sum((observed - expected)^2 / expected)
8241        let mut chi_sq = 0.0;
8242        for (obs, exp) in actual.iter().zip(expected.iter()) {
8243            if *exp <= 0.0 {
8244                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8245                    ExcelError::new_num(),
8246                )));
8247            }
8248            chi_sq += (obs - exp).powi(2) / exp;
8249        }
8250
8251        // Degrees of freedom = number of categories - 1
8252        let df = (actual.len() - 1) as f64;
8253
8254        if df < 1.0 {
8255            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8256                ExcelError::new_num(),
8257            )));
8258        }
8259
8260        // P-value = 1 - CHISQ.DIST(chi_sq, df, TRUE) = right-tail probability
8261        let p = 1.0 - chisq_cdf(chi_sq, df);
8262
8263        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(p)))
8264    }
8265}
8266
8267/* ═══════════════════════════════════════════════════════════════════════════
8268   FZ-PAR-01: Statistical compatibility batch
8269   AVERAGEA, MAXA, MINA, STDEVA, STDEVPA, VARA, VARPA, SKEW.P,
8270   T.DIST.RT, CHISQ.DIST.RT, CHISQ.INV.RT, F.DIST.RT, F.INV.RT,
8271   BETA.INV, BINOM.DIST.RANGE, BINOM.INV, GAMMA, GAMMA.INV,
8272   GAMMALN, GAMMALN.PRECISE
8273═══════════════════════════════════════════════════════════════════════════ */
8274
8275/// Collect numeric inputs applying Excel "A"-variant semantics:
8276/// - Range references: include numbers as-is, booleans as 0/1, text as 0. Errors propagate.
8277///   Blank cells are skipped.
8278/// - Direct scalar arguments: same coercion as standard collect_numeric_stats.
8279fn collect_numeric_a(args: &[ArgumentHandle]) -> Result<Vec<f64>, ExcelError> {
8280    let mut out = Vec::new();
8281    for a in args {
8282        if let Some(arr) = a.inline_array_literal()? {
8283            for row in arr.into_iter() {
8284                for cell in row.into_iter() {
8285                    match cell {
8286                        LiteralValue::Error(e) => return Err(e),
8287                        LiteralValue::Number(n) => out.push(n),
8288                        LiteralValue::Int(i) => out.push(i as f64),
8289                        LiteralValue::Boolean(b) => out.push(if b { 1.0 } else { 0.0 }),
8290                        LiteralValue::Text(_) => out.push(0.0),
8291                        _ => {}
8292                    }
8293                }
8294            }
8295            continue;
8296        }
8297
8298        if let Ok(view) = a.range_view() {
8299            view.for_each_cell(&mut |v| {
8300                match v {
8301                    LiteralValue::Error(e) => return Err(e.clone()),
8302                    LiteralValue::Number(n) => out.push(*n),
8303                    LiteralValue::Int(i) => out.push(*i as f64),
8304                    LiteralValue::Boolean(b) => out.push(if *b { 1.0 } else { 0.0 }),
8305                    LiteralValue::Text(_) => out.push(0.0),
8306                    LiteralValue::Empty => {} // skip blanks
8307                    _ => {}
8308                }
8309                Ok(())
8310            })?;
8311        } else {
8312            let v = scalar_like_value(a)?;
8313            match v {
8314                LiteralValue::Error(e) => return Err(e),
8315                other => {
8316                    if let Ok(n) = coerce_num(&other) {
8317                        out.push(n);
8318                    }
8319                }
8320            }
8321        }
8322    }
8323    Ok(out)
8324}
8325
8326/// Helper: inverse of the regularized incomplete beta function.
8327/// Given p = I_x(a,b), find x. Uses Newton-Raphson with beta_i / beta PDF.
8328fn beta_inv_helper(p: f64, a: f64, b: f64) -> Option<f64> {
8329    if p <= 0.0 {
8330        return Some(0.0);
8331    }
8332    if p >= 1.0 {
8333        return Some(1.0);
8334    }
8335    if a <= 0.0 || b <= 0.0 {
8336        return None;
8337    }
8338
8339    // Initial guess from normal approximation (Abramowitz & Stegun 26.5.22)
8340    let mut x = 0.5f64;
8341
8342    // Newton-Raphson
8343    let ln_beta_ab = ln_gamma(a) + ln_gamma(b) - ln_gamma(a + b);
8344    for _ in 0..100 {
8345        let cdf = beta_i(x, a, b);
8346        // Beta PDF: x^(a-1) * (1-x)^(b-1) / B(a,b)
8347        let pdf = if x > 0.0 && x < 1.0 {
8348            ((a - 1.0) * x.ln() + (b - 1.0) * (1.0 - x).ln() - ln_beta_ab).exp()
8349        } else {
8350            1e-30
8351        };
8352        if pdf.abs() < 1e-30 {
8353            break;
8354        }
8355        let delta = (cdf - p) / pdf;
8356        let new_x = (x - delta).clamp(1e-15, 1.0 - 1e-15);
8357        if (new_x - x).abs() < 1e-14 {
8358            x = new_x;
8359            break;
8360        }
8361        x = new_x;
8362    }
8363
8364    Some(x)
8365}
8366
8367/// Helper: inverse of GAMMA.DIST CDF. Given p = P(alpha, x/beta), find x.
8368fn gamma_inv_helper(p: f64, alpha: f64, beta: f64) -> Option<f64> {
8369    if p <= 0.0 {
8370        return Some(0.0);
8371    }
8372    if p >= 1.0 {
8373        return None;
8374    }
8375    if alpha <= 0.0 || beta <= 0.0 {
8376        return None;
8377    }
8378
8379    // Initial guess
8380    let mut x = alpha * beta;
8381    if p < 0.5 {
8382        x = x.min(beta);
8383    }
8384
8385    // Newton-Raphson on the standardized gamma CDF (gamma_p)
8386    for _ in 0..100 {
8387        let z = x / beta;
8388        let cdf = gamma_p(alpha, z);
8389        // Gamma PDF: z^(alpha-1) * e^(-z) / Gamma(alpha) / beta
8390        let pdf = if z > 0.0 {
8391            ((alpha - 1.0) * z.ln() - z - ln_gamma(alpha)).exp() / beta
8392        } else {
8393            1e-30
8394        };
8395        if pdf.abs() < 1e-30 {
8396            break;
8397        }
8398        let delta = (cdf - p) / pdf;
8399        let new_x = (x - delta).max(1e-15);
8400        if (new_x - x).abs() < 1e-12 * x.max(1e-15) {
8401            x = new_x;
8402            break;
8403        }
8404        x = new_x;
8405    }
8406
8407    Some(x)
8408}
8409
8410/* ─────────────────────────── AVERAGEA ──────────────────────────── */
8411
8412/// Returns the arithmetic mean while treating logical values and text as numeric inputs.
8413///
8414/// # Formula example
8415/// ```excel
8416/// # returns: 1
8417/// =AVERAGEA(TRUE,2,"x")
8418/// ```
8419///
8420/// ```yaml,sandbox
8421/// title: "Average with logical and text coercion"
8422/// formula: '=AVERAGEA(TRUE,2,"x")'
8423/// expected: 1
8424/// ```
8425///
8426/// ```yaml,docs
8427/// related:
8428///   - AVERAGE
8429///   - MAXA
8430///   - MINA
8431/// faq:
8432///   - q: "How does AVERAGEA treat text and booleans?"
8433///     a: "TRUE counts as 1, FALSE and text count as 0, and blanks are ignored."
8434/// ```
8435#[derive(Debug)]
8436pub struct AverageAFn;
8437/// [formualizer-docgen:schema:start]
8438/// Name: AVERAGEA
8439/// Type: AverageAFn
8440/// Min args: 1
8441/// Max args: variadic
8442/// Variadic: true
8443/// Signature: AVERAGEA(arg1...: number@range)
8444/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
8445/// Caps: PURE, REDUCTION
8446/// [formualizer-docgen:schema:end]
8447impl Function for AverageAFn {
8448    func_caps!(PURE, REDUCTION);
8449    fn name(&self) -> &'static str {
8450        "AVERAGEA"
8451    }
8452    fn min_args(&self) -> usize {
8453        1
8454    }
8455    fn variadic(&self) -> bool {
8456        true
8457    }
8458    fn arg_schema(&self) -> &'static [ArgSchema] {
8459        &ARG_RANGE_NUM_LENIENT_ONE[..]
8460    }
8461    fn eval<'a, 'b, 'c>(
8462        &self,
8463        args: &'c [ArgumentHandle<'a, 'b>],
8464        _ctx: &dyn FunctionContext<'b>,
8465    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
8466        let nums = collect_numeric_a(args)?;
8467        if nums.is_empty() {
8468            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8469                ExcelError::new_div(),
8470            )));
8471        }
8472        let avg = nums.iter().sum::<f64>() / nums.len() as f64;
8473        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(avg)))
8474    }
8475}
8476
8477/* ─────────────────────────── MAXA ──────────────────────────── */
8478
8479/// Returns the largest value after applying A-variant coercion rules.
8480///
8481/// # Formula example
8482/// ```excel
8483/// # returns: 1
8484/// =MAXA(TRUE,-2,"x")
8485/// ```
8486///
8487/// ```yaml,sandbox
8488/// title: "Maximum with logical and text coercion"
8489/// formula: '=MAXA(TRUE,-2,"x")'
8490/// expected: 1
8491/// ```
8492///
8493/// ```yaml,docs
8494/// related:
8495///   - MAX
8496///   - MINA
8497///   - AVERAGEA
8498/// faq:
8499///   - q: "What do text values contribute to MAXA?"
8500///     a: "Text contributes 0, so negative numeric inputs can still be smaller than text in the aggregate."
8501/// ```
8502#[derive(Debug)]
8503pub struct MaxAFn;
8504/// [formualizer-docgen:schema:start]
8505/// Name: MAXA
8506/// Type: MaxAFn
8507/// Min args: 1
8508/// Max args: variadic
8509/// Variadic: true
8510/// Signature: MAXA(arg1...: number@range)
8511/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
8512/// Caps: PURE, REDUCTION
8513/// [formualizer-docgen:schema:end]
8514impl Function for MaxAFn {
8515    func_caps!(PURE, REDUCTION);
8516    fn name(&self) -> &'static str {
8517        "MAXA"
8518    }
8519    fn min_args(&self) -> usize {
8520        1
8521    }
8522    fn variadic(&self) -> bool {
8523        true
8524    }
8525    fn arg_schema(&self) -> &'static [ArgSchema] {
8526        &ARG_RANGE_NUM_LENIENT_ONE[..]
8527    }
8528    fn eval<'a, 'b, 'c>(
8529        &self,
8530        args: &'c [ArgumentHandle<'a, 'b>],
8531        _ctx: &dyn FunctionContext<'b>,
8532    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
8533        let nums = collect_numeric_a(args)?;
8534        if nums.is_empty() {
8535            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0)));
8536        }
8537        let mx = nums.iter().copied().fold(f64::NEG_INFINITY, f64::max);
8538        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(mx)))
8539    }
8540}
8541
8542/* ─────────────────────────── MINA ──────────────────────────── */
8543
8544/// Returns the smallest value after applying A-variant coercion rules.
8545///
8546/// # Formula example
8547/// ```excel
8548/// # returns: -2
8549/// =MINA(TRUE,-2,"x")
8550/// ```
8551///
8552/// ```yaml,sandbox
8553/// title: "Minimum with logical and text coercion"
8554/// formula: '=MINA(TRUE,-2,"x")'
8555/// expected: -2
8556/// ```
8557///
8558/// ```yaml,docs
8559/// related:
8560///   - MIN
8561///   - MAXA
8562///   - AVERAGEA
8563/// faq:
8564///   - q: "Do text values affect MINA?"
8565///     a: "Yes. Text is coerced to 0, so it can become the minimum when all numeric inputs are positive."
8566/// ```
8567#[derive(Debug)]
8568pub struct MinAFn;
8569/// [formualizer-docgen:schema:start]
8570/// Name: MINA
8571/// Type: MinAFn
8572/// Min args: 1
8573/// Max args: variadic
8574/// Variadic: true
8575/// Signature: MINA(arg1...: number@range)
8576/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
8577/// Caps: PURE, REDUCTION
8578/// [formualizer-docgen:schema:end]
8579impl Function for MinAFn {
8580    func_caps!(PURE, REDUCTION);
8581    fn name(&self) -> &'static str {
8582        "MINA"
8583    }
8584    fn min_args(&self) -> usize {
8585        1
8586    }
8587    fn variadic(&self) -> bool {
8588        true
8589    }
8590    fn arg_schema(&self) -> &'static [ArgSchema] {
8591        &ARG_RANGE_NUM_LENIENT_ONE[..]
8592    }
8593    fn eval<'a, 'b, 'c>(
8594        &self,
8595        args: &'c [ArgumentHandle<'a, 'b>],
8596        _ctx: &dyn FunctionContext<'b>,
8597    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
8598        let nums = collect_numeric_a(args)?;
8599        if nums.is_empty() {
8600            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0)));
8601        }
8602        let mn = nums.iter().copied().fold(f64::INFINITY, f64::min);
8603        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(mn)))
8604    }
8605}
8606
8607/* ─────────────────────────── STDEVA ──────────────────────────── */
8608
8609/// Returns the sample standard deviation using A-variant coercion semantics.
8610///
8611/// # Formula example
8612/// ```excel
8613/// # returns: 1
8614/// =STDEVA(TRUE,2,"x")
8615/// ```
8616///
8617/// ```yaml,sandbox
8618/// title: "Sample deviation with coerced values"
8619/// formula: '=STDEVA(TRUE,2,"x")'
8620/// expected: 1
8621/// ```
8622///
8623/// ```yaml,docs
8624/// related:
8625///   - STDEV.P
8626///   - STDEVPA
8627///   - VARA
8628/// faq:
8629///   - q: "When does STDEVA return #DIV/0!?"
8630///     a: "It returns #DIV/0! when fewer than two coerced values remain after evaluation."
8631/// ```
8632#[derive(Debug)]
8633pub struct StdevAFn;
8634/// [formualizer-docgen:schema:start]
8635/// Name: STDEVA
8636/// Type: StdevAFn
8637/// Min args: 1
8638/// Max args: variadic
8639/// Variadic: true
8640/// Signature: STDEVA(arg1...: number@range)
8641/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
8642/// Caps: PURE, REDUCTION
8643/// [formualizer-docgen:schema:end]
8644impl Function for StdevAFn {
8645    func_caps!(PURE, REDUCTION);
8646    fn name(&self) -> &'static str {
8647        "STDEVA"
8648    }
8649    fn min_args(&self) -> usize {
8650        1
8651    }
8652    fn variadic(&self) -> bool {
8653        true
8654    }
8655    fn arg_schema(&self) -> &'static [ArgSchema] {
8656        &ARG_RANGE_NUM_LENIENT_ONE[..]
8657    }
8658    fn eval<'a, 'b, 'c>(
8659        &self,
8660        args: &'c [ArgumentHandle<'a, 'b>],
8661        _ctx: &dyn FunctionContext<'b>,
8662    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
8663        let nums = collect_numeric_a(args)?;
8664        let n = nums.len();
8665        if n < 2 {
8666            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8667                ExcelError::new_div(),
8668            )));
8669        }
8670        let mean = nums.iter().sum::<f64>() / n as f64;
8671        let ss: f64 = nums.iter().map(|v| (v - mean).powi(2)).sum();
8672        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
8673            (ss / (n - 1) as f64).sqrt(),
8674        )))
8675    }
8676}
8677
8678/* ─────────────────────────── STDEVPA ──────────────────────────── */
8679
8680/// Returns the population standard deviation using A-variant coercion semantics.
8681///
8682/// # Formula example
8683/// ```excel
8684/// # returns: 0.816496580927726
8685/// =STDEVPA(TRUE,2,"x")
8686/// ```
8687///
8688/// ```yaml,sandbox
8689/// title: "Population deviation with coerced values"
8690/// formula: '=STDEVPA(TRUE,2,"x")'
8691/// expected: 0.816496580927726
8692/// ```
8693///
8694/// ```yaml,docs
8695/// related:
8696///   - STDEVA
8697///   - VARPA
8698///   - STDEV.P
8699/// faq:
8700///   - q: "What is the difference between STDEVA and STDEVPA?"
8701///     a: "STDEVA uses the sample denominator n-1, while STDEVPA uses the population denominator n."
8702/// ```
8703#[derive(Debug)]
8704pub struct StdevPAFn;
8705/// [formualizer-docgen:schema:start]
8706/// Name: STDEVPA
8707/// Type: StdevPAFn
8708/// Min args: 1
8709/// Max args: variadic
8710/// Variadic: true
8711/// Signature: STDEVPA(arg1...: number@range)
8712/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
8713/// Caps: PURE, REDUCTION
8714/// [formualizer-docgen:schema:end]
8715impl Function for StdevPAFn {
8716    func_caps!(PURE, REDUCTION);
8717    fn name(&self) -> &'static str {
8718        "STDEVPA"
8719    }
8720    fn min_args(&self) -> usize {
8721        1
8722    }
8723    fn variadic(&self) -> bool {
8724        true
8725    }
8726    fn arg_schema(&self) -> &'static [ArgSchema] {
8727        &ARG_RANGE_NUM_LENIENT_ONE[..]
8728    }
8729    fn eval<'a, 'b, 'c>(
8730        &self,
8731        args: &'c [ArgumentHandle<'a, 'b>],
8732        _ctx: &dyn FunctionContext<'b>,
8733    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
8734        let nums = collect_numeric_a(args)?;
8735        let n = nums.len();
8736        if n == 0 {
8737            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8738                ExcelError::new_div(),
8739            )));
8740        }
8741        let mean = nums.iter().sum::<f64>() / n as f64;
8742        let ss: f64 = nums.iter().map(|v| (v - mean).powi(2)).sum();
8743        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
8744            (ss / n as f64).sqrt(),
8745        )))
8746    }
8747}
8748
8749/* ─────────────────────────── VARA ──────────────────────────── */
8750
8751/// Returns the sample variance using A-variant coercion semantics.
8752///
8753/// # Formula example
8754/// ```excel
8755/// # returns: 1
8756/// =VARA(TRUE,2,"x")
8757/// ```
8758///
8759/// ```yaml,sandbox
8760/// title: "Sample variance with coerced values"
8761/// formula: '=VARA(TRUE,2,"x")'
8762/// expected: 1
8763/// ```
8764///
8765/// ```yaml,docs
8766/// related:
8767///   - VARPA
8768///   - STDEVA
8769///   - AVERAGEA
8770/// faq:
8771///   - q: "How are blanks handled in VARA?"
8772///     a: "Blanks are ignored, while booleans and text are coerced under A-variant rules."
8773/// ```
8774#[derive(Debug)]
8775pub struct VarAFn;
8776/// [formualizer-docgen:schema:start]
8777/// Name: VARA
8778/// Type: VarAFn
8779/// Min args: 1
8780/// Max args: variadic
8781/// Variadic: true
8782/// Signature: VARA(arg1...: number@range)
8783/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
8784/// Caps: PURE, REDUCTION
8785/// [formualizer-docgen:schema:end]
8786impl Function for VarAFn {
8787    func_caps!(PURE, REDUCTION);
8788    fn name(&self) -> &'static str {
8789        "VARA"
8790    }
8791    fn min_args(&self) -> usize {
8792        1
8793    }
8794    fn variadic(&self) -> bool {
8795        true
8796    }
8797    fn arg_schema(&self) -> &'static [ArgSchema] {
8798        &ARG_RANGE_NUM_LENIENT_ONE[..]
8799    }
8800    fn eval<'a, 'b, 'c>(
8801        &self,
8802        args: &'c [ArgumentHandle<'a, 'b>],
8803        _ctx: &dyn FunctionContext<'b>,
8804    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
8805        let nums = collect_numeric_a(args)?;
8806        let n = nums.len();
8807        if n < 2 {
8808            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8809                ExcelError::new_div(),
8810            )));
8811        }
8812        let mean = nums.iter().sum::<f64>() / n as f64;
8813        let ss: f64 = nums.iter().map(|v| (v - mean).powi(2)).sum();
8814        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
8815            ss / (n - 1) as f64,
8816        )))
8817    }
8818}
8819
8820/* ─────────────────────────── VARPA ──────────────────────────── */
8821
8822/// Returns the population variance using A-variant coercion semantics.
8823///
8824/// # Formula example
8825/// ```excel
8826/// # returns: 0.6666666666666666
8827/// =VARPA(TRUE,2,"x")
8828/// ```
8829///
8830/// ```yaml,sandbox
8831/// title: "Population variance with coerced values"
8832/// formula: '=VARPA(TRUE,2,"x")'
8833/// expected: 0.6666666666666666
8834/// ```
8835///
8836/// ```yaml,docs
8837/// related:
8838///   - VARA
8839///   - STDEVPA
8840///   - STDEV.P
8841/// faq:
8842///   - q: "When does VARPA return #DIV/0!?"
8843///     a: "It returns #DIV/0! when no coerced values remain after evaluation."
8844/// ```
8845#[derive(Debug)]
8846pub struct VarPAFn;
8847/// [formualizer-docgen:schema:start]
8848/// Name: VARPA
8849/// Type: VarPAFn
8850/// Min args: 1
8851/// Max args: variadic
8852/// Variadic: true
8853/// Signature: VARPA(arg1...: number@range)
8854/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
8855/// Caps: PURE, REDUCTION
8856/// [formualizer-docgen:schema:end]
8857impl Function for VarPAFn {
8858    func_caps!(PURE, REDUCTION);
8859    fn name(&self) -> &'static str {
8860        "VARPA"
8861    }
8862    fn min_args(&self) -> usize {
8863        1
8864    }
8865    fn variadic(&self) -> bool {
8866        true
8867    }
8868    fn arg_schema(&self) -> &'static [ArgSchema] {
8869        &ARG_RANGE_NUM_LENIENT_ONE[..]
8870    }
8871    fn eval<'a, 'b, 'c>(
8872        &self,
8873        args: &'c [ArgumentHandle<'a, 'b>],
8874        _ctx: &dyn FunctionContext<'b>,
8875    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
8876        let nums = collect_numeric_a(args)?;
8877        let n = nums.len();
8878        if n == 0 {
8879            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8880                ExcelError::new_div(),
8881            )));
8882        }
8883        let mean = nums.iter().sum::<f64>() / n as f64;
8884        let ss: f64 = nums.iter().map(|v| (v - mean).powi(2)).sum();
8885        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
8886            ss / n as f64,
8887        )))
8888    }
8889}
8890
8891/* ─────────────────────────── SKEW.P ──────────────────────────── */
8892
8893/// Returns the population skewness of a numeric data set.
8894///
8895/// # Formula example
8896/// ```excel
8897/// # returns: 0
8898/// =SKEW.P(1,2,3)
8899/// ```
8900///
8901/// ```yaml,sandbox
8902/// title: "Symmetric data has zero skew"
8903/// formula: '=SKEW.P(1,2,3)'
8904/// expected: 0
8905/// ```
8906///
8907/// ```yaml,docs
8908/// related:
8909///   - SKEW
8910///   - KURT
8911///   - AVERAGE
8912/// faq:
8913///   - q: "When does SKEW.P return #DIV/0!?"
8914///     a: "It returns #DIV/0! when fewer than three numeric values are available or the population standard deviation is zero."
8915/// ```
8916#[derive(Debug)]
8917pub struct SkewPFn;
8918/// [formualizer-docgen:schema:start]
8919/// Name: SKEW.P
8920/// Type: SkewPFn
8921/// Min args: 1
8922/// Max args: variadic
8923/// Variadic: true
8924/// Signature: SKEW.P(arg1...: number@range)
8925/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
8926/// Caps: PURE, REDUCTION, NUMERIC_ONLY
8927/// [formualizer-docgen:schema:end]
8928impl Function for SkewPFn {
8929    func_caps!(PURE, REDUCTION, NUMERIC_ONLY);
8930    fn name(&self) -> &'static str {
8931        "SKEW.P"
8932    }
8933    fn min_args(&self) -> usize {
8934        1
8935    }
8936    fn variadic(&self) -> bool {
8937        true
8938    }
8939    fn arg_schema(&self) -> &'static [ArgSchema] {
8940        &ARG_RANGE_NUM_LENIENT_ONE[..]
8941    }
8942    fn eval<'a, 'b, 'c>(
8943        &self,
8944        args: &'c [ArgumentHandle<'a, 'b>],
8945        _ctx: &dyn FunctionContext<'b>,
8946    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
8947        let nums = collect_numeric_stats(args)?;
8948        let n = nums.len();
8949        if n < 3 {
8950            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8951                ExcelError::new_div(),
8952            )));
8953        }
8954        let n_f = n as f64;
8955        let mean = nums.iter().sum::<f64>() / n_f;
8956        let mut sum_sq = 0.0;
8957        for &v in &nums {
8958            sum_sq += (v - mean).powi(2);
8959        }
8960        let stdev_pop = (sum_sq / n_f).sqrt();
8961        if stdev_pop == 0.0 {
8962            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8963                ExcelError::new_div(),
8964            )));
8965        }
8966        let mut sum_cubed = 0.0;
8967        for &v in &nums {
8968            sum_cubed += ((v - mean) / stdev_pop).powi(3);
8969        }
8970        // Population skewness: (1/n) * sum((xi - mean)/sigma)^3
8971        let skew = sum_cubed / n_f;
8972        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(skew)))
8973    }
8974}
8975
8976/* ─────────────────────────── T.DIST.RT ──────────────────────────── */
8977
8978/// Returns the right-tailed Student's t-distribution probability.
8979///
8980/// # Formula example
8981/// ```excel
8982/// # returns: 0.5
8983/// =T.DIST.RT(0,10)
8984/// ```
8985///
8986/// ```yaml,sandbox
8987/// title: "Zero lies at the midpoint of the t distribution"
8988/// formula: '=T.DIST.RT(0,10)'
8989/// expected: 0.5
8990/// ```
8991///
8992/// ```yaml,docs
8993/// related:
8994///   - T.DIST.2T
8995///   - T.INV
8996///   - T.INV.2T
8997/// faq:
8998///   - q: "When does T.DIST.RT return #NUM!?"
8999///     a: "It returns #NUM! when the degrees of freedom are less than 1."
9000/// ```
9001#[derive(Debug)]
9002pub struct TDistRtFn;
9003/// [formualizer-docgen:schema:start]
9004/// Name: T.DIST.RT
9005/// Type: TDistRtFn
9006/// Min args: 2
9007/// Max args: 2
9008/// Variadic: false
9009/// Signature: T.DIST.RT(arg1: number@scalar, arg2: number@scalar)
9010/// 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}
9011/// Caps: PURE
9012/// [formualizer-docgen:schema:end]
9013impl Function for TDistRtFn {
9014    func_caps!(PURE);
9015    fn name(&self) -> &'static str {
9016        "T.DIST.RT"
9017    }
9018    fn min_args(&self) -> usize {
9019        2
9020    }
9021    fn arg_schema(&self) -> &'static [ArgSchema] {
9022        use std::sync::LazyLock;
9023        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
9024            vec![
9025                ArgSchema::number_lenient_scalar(),
9026                ArgSchema::number_lenient_scalar(),
9027            ]
9028        });
9029        &SCHEMA[..]
9030    }
9031    fn eval<'a, 'b, 'c>(
9032        &self,
9033        args: &'c [ArgumentHandle<'a, 'b>],
9034        _ctx: &dyn FunctionContext<'b>,
9035    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
9036        let x = coerce_num(&scalar_like_value(&args[0])?)?;
9037        let df = coerce_num(&scalar_like_value(&args[1])?)?;
9038        if df < 1.0 {
9039            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9040                ExcelError::new_num(),
9041            )));
9042        }
9043        let result = 1.0 - t_cdf(x, df);
9044        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
9045            result,
9046        )))
9047    }
9048}
9049
9050/* ─────────────────────────── CHISQ.DIST.RT ──────────────────────────── */
9051
9052/// Returns the right-tailed chi-squared distribution probability.
9053///
9054/// # Formula example
9055/// ```excel
9056/// # returns: 0.36787944117144233
9057/// =CHISQ.DIST.RT(2,2)
9058/// ```
9059///
9060/// ```yaml,sandbox
9061/// title: "Right-tail chi-squared probability"
9062/// formula: '=CHISQ.DIST.RT(2,2)'
9063/// expected: 0.36787944117144233
9064/// ```
9065///
9066/// ```yaml,docs
9067/// related:
9068///   - CHISQ.INV.RT
9069///   - CHISQ.TEST
9070///   - CHISQ.DIST
9071/// faq:
9072///   - q: "Which inputs return #NUM!?"
9073///     a: "Negative x values and degrees of freedom below 1 return #NUM!."
9074/// ```
9075#[derive(Debug)]
9076pub struct ChisqDistRtFn;
9077/// [formualizer-docgen:schema:start]
9078/// Name: CHISQ.DIST.RT
9079/// Type: ChisqDistRtFn
9080/// Min args: 2
9081/// Max args: 2
9082/// Variadic: false
9083/// Signature: CHISQ.DIST.RT(arg1: number@scalar, arg2: number@scalar)
9084/// 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}
9085/// Caps: PURE
9086/// [formualizer-docgen:schema:end]
9087impl Function for ChisqDistRtFn {
9088    func_caps!(PURE);
9089    fn name(&self) -> &'static str {
9090        "CHISQ.DIST.RT"
9091    }
9092    fn min_args(&self) -> usize {
9093        2
9094    }
9095    fn arg_schema(&self) -> &'static [ArgSchema] {
9096        use std::sync::LazyLock;
9097        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
9098            vec![
9099                ArgSchema::number_lenient_scalar(),
9100                ArgSchema::number_lenient_scalar(),
9101            ]
9102        });
9103        &SCHEMA[..]
9104    }
9105    fn eval<'a, 'b, 'c>(
9106        &self,
9107        args: &'c [ArgumentHandle<'a, 'b>],
9108        _ctx: &dyn FunctionContext<'b>,
9109    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
9110        let x = coerce_num(&scalar_like_value(&args[0])?)?;
9111        let df = coerce_num(&scalar_like_value(&args[1])?)?;
9112        if df < 1.0 || x < 0.0 {
9113            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9114                ExcelError::new_num(),
9115            )));
9116        }
9117        let result = 1.0 - chisq_cdf(x, df);
9118        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
9119            result,
9120        )))
9121    }
9122}
9123
9124/* ─────────────────────────── CHISQ.INV.RT ──────────────────────────── */
9125
9126/// Returns the inverse of the right-tailed chi-squared distribution.
9127///
9128/// # Formula example
9129/// ```excel
9130/// # returns: 1.3862943611198906
9131/// =CHISQ.INV.RT(0.5,2)
9132/// ```
9133///
9134/// ```yaml,sandbox
9135/// title: "Median right-tail inverse for 2 degrees of freedom"
9136/// formula: '=CHISQ.INV.RT(0.5,2)'
9137/// expected: 1.3862943611198906
9138/// ```
9139///
9140/// ```yaml,docs
9141/// related:
9142///   - CHISQ.DIST.RT
9143///   - CHISQ.INV
9144///   - CHISQ.TEST
9145/// faq:
9146///   - q: "What p-values are valid for CHISQ.INV.RT?"
9147///     a: "p must lie in the range 0 to 1, and p=0 returns #NUM! because the right-tail inverse diverges."
9148/// ```
9149#[derive(Debug)]
9150pub struct ChisqInvRtFn;
9151/// [formualizer-docgen:schema:start]
9152/// Name: CHISQ.INV.RT
9153/// Type: ChisqInvRtFn
9154/// Min args: 2
9155/// Max args: 2
9156/// Variadic: false
9157/// Signature: CHISQ.INV.RT(arg1: number@scalar, arg2: number@scalar)
9158/// 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}
9159/// Caps: PURE
9160/// [formualizer-docgen:schema:end]
9161impl Function for ChisqInvRtFn {
9162    func_caps!(PURE);
9163    fn name(&self) -> &'static str {
9164        "CHISQ.INV.RT"
9165    }
9166    fn min_args(&self) -> usize {
9167        2
9168    }
9169    fn arg_schema(&self) -> &'static [ArgSchema] {
9170        use std::sync::LazyLock;
9171        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
9172            vec![
9173                ArgSchema::number_lenient_scalar(),
9174                ArgSchema::number_lenient_scalar(),
9175            ]
9176        });
9177        &SCHEMA[..]
9178    }
9179    fn eval<'a, 'b, 'c>(
9180        &self,
9181        args: &'c [ArgumentHandle<'a, 'b>],
9182        _ctx: &dyn FunctionContext<'b>,
9183    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
9184        let p = coerce_num(&scalar_like_value(&args[0])?)?;
9185        let df = coerce_num(&scalar_like_value(&args[1])?)?;
9186        if df < 1.0 || !(0.0..=1.0).contains(&p) {
9187            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9188                ExcelError::new_num(),
9189            )));
9190        }
9191        // Right-tail: CHISQ.INV.RT(p, df) = CHISQ.INV(1-p, df)
9192        if p == 0.0 {
9193            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9194                ExcelError::new_num(),
9195            )));
9196        }
9197        match chisq_inv(1.0 - p, df) {
9198            Some(result) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
9199                result,
9200            ))),
9201            None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9202                ExcelError::new_num(),
9203            ))),
9204        }
9205    }
9206}
9207
9208/* ─────────────────────────── F.DIST.RT ──────────────────────────── */
9209
9210/// Returns the right-tailed F-distribution probability.
9211///
9212/// # Formula example
9213/// ```excel
9214/// # returns: 1
9215/// =F.DIST.RT(0,5,10)
9216/// ```
9217///
9218/// ```yaml,sandbox
9219/// title: "Zero leaves the entire right tail"
9220/// formula: '=F.DIST.RT(0,5,10)'
9221/// expected: 1
9222/// ```
9223///
9224/// ```yaml,docs
9225/// related:
9226///   - F.INV.RT
9227///   - F.TEST
9228///   - F.DIST
9229/// faq:
9230///   - q: "Which inputs return #NUM!?"
9231///     a: "Negative x values or degrees of freedom below 1 return #NUM!."
9232/// ```
9233#[derive(Debug)]
9234pub struct FDistRtFn;
9235/// [formualizer-docgen:schema:start]
9236/// Name: F.DIST.RT
9237/// Type: FDistRtFn
9238/// Min args: 3
9239/// Max args: 3
9240/// Variadic: false
9241/// Signature: F.DIST.RT(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
9242/// 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}
9243/// Caps: PURE
9244/// [formualizer-docgen:schema:end]
9245impl Function for FDistRtFn {
9246    func_caps!(PURE);
9247    fn name(&self) -> &'static str {
9248        "F.DIST.RT"
9249    }
9250    fn min_args(&self) -> usize {
9251        3
9252    }
9253    fn arg_schema(&self) -> &'static [ArgSchema] {
9254        use std::sync::LazyLock;
9255        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
9256            vec![
9257                ArgSchema::number_lenient_scalar(),
9258                ArgSchema::number_lenient_scalar(),
9259                ArgSchema::number_lenient_scalar(),
9260            ]
9261        });
9262        &SCHEMA[..]
9263    }
9264    fn eval<'a, 'b, 'c>(
9265        &self,
9266        args: &'c [ArgumentHandle<'a, 'b>],
9267        _ctx: &dyn FunctionContext<'b>,
9268    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
9269        let x = coerce_num(&scalar_like_value(&args[0])?)?;
9270        let d1 = coerce_num(&scalar_like_value(&args[1])?)?;
9271        let d2 = coerce_num(&scalar_like_value(&args[2])?)?;
9272        if d1 < 1.0 || d2 < 1.0 || x < 0.0 {
9273            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9274                ExcelError::new_num(),
9275            )));
9276        }
9277        let result = 1.0 - f_cdf(x, d1, d2);
9278        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
9279            result,
9280        )))
9281    }
9282}
9283
9284/* ─────────────────────────── F.INV.RT ──────────────────────────── */
9285
9286/// Returns the inverse of the right-tailed F-distribution.
9287///
9288/// # Formula example
9289/// ```excel
9290/// # returns: 0
9291/// =F.INV.RT(1,5,10)
9292/// ```
9293///
9294/// ```yaml,sandbox
9295/// title: "A full right tail maps to zero"
9296/// formula: '=F.INV.RT(1,5,10)'
9297/// expected: 0
9298/// ```
9299///
9300/// ```yaml,docs
9301/// related:
9302///   - F.DIST.RT
9303///   - F.INV
9304///   - F.TEST
9305/// faq:
9306///   - q: "What p-values are valid for F.INV.RT?"
9307///     a: "p must lie in the range 0 to 1, and p=0 returns #NUM! because the inverse diverges."
9308/// ```
9309#[derive(Debug)]
9310pub struct FInvRtFn;
9311/// [formualizer-docgen:schema:start]
9312/// Name: F.INV.RT
9313/// Type: FInvRtFn
9314/// Min args: 3
9315/// Max args: 3
9316/// Variadic: false
9317/// Signature: F.INV.RT(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
9318/// 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}
9319/// Caps: PURE
9320/// [formualizer-docgen:schema:end]
9321impl Function for FInvRtFn {
9322    func_caps!(PURE);
9323    fn name(&self) -> &'static str {
9324        "F.INV.RT"
9325    }
9326    fn min_args(&self) -> usize {
9327        3
9328    }
9329    fn arg_schema(&self) -> &'static [ArgSchema] {
9330        use std::sync::LazyLock;
9331        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
9332            vec![
9333                ArgSchema::number_lenient_scalar(),
9334                ArgSchema::number_lenient_scalar(),
9335                ArgSchema::number_lenient_scalar(),
9336            ]
9337        });
9338        &SCHEMA[..]
9339    }
9340    fn eval<'a, 'b, 'c>(
9341        &self,
9342        args: &'c [ArgumentHandle<'a, 'b>],
9343        _ctx: &dyn FunctionContext<'b>,
9344    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
9345        let p = coerce_num(&scalar_like_value(&args[0])?)?;
9346        let d1 = coerce_num(&scalar_like_value(&args[1])?)?;
9347        let d2 = coerce_num(&scalar_like_value(&args[2])?)?;
9348        if d1 < 1.0 || d2 < 1.0 || !(0.0..=1.0).contains(&p) {
9349            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9350                ExcelError::new_num(),
9351            )));
9352        }
9353        if p == 0.0 {
9354            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9355                ExcelError::new_num(),
9356            )));
9357        }
9358        // F.INV.RT(1, d1, d2) = 0 (entire right tail)
9359        if p == 1.0 {
9360            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0)));
9361        }
9362        // F.INV.RT(p, d1, d2) = F.INV(1-p, d1, d2)
9363        match f_inv(1.0 - p, d1, d2) {
9364            Some(result) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
9365                result,
9366            ))),
9367            None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9368                ExcelError::new_num(),
9369            ))),
9370        }
9371    }
9372}
9373
9374/* ─────────────────────────── BETA.INV ──────────────────────────── */
9375
9376/// Returns the inverse cumulative beta distribution, optionally scaled to custom bounds.
9377///
9378/// # Formula example
9379/// ```excel
9380/// # returns: 0.5
9381/// =BETA.INV(0.5,2,2)
9382/// ```
9383///
9384/// ```yaml,sandbox
9385/// title: "Symmetric beta inverse at the median"
9386/// formula: '=BETA.INV(0.5,2,2)'
9387/// expected: 0.5
9388/// ```
9389///
9390/// ```yaml,docs
9391/// related:
9392///   - BETA.DIST
9393///   - GAMMA.INV
9394///   - NORM.INV
9395/// faq:
9396///   - q: "When does BETA.INV return #NUM!?"
9397///     a: "It returns #NUM! for non-positive alpha or beta, invalid bounds, or probabilities outside 0..1."
9398/// ```
9399#[derive(Debug)]
9400pub struct BetaInvFn;
9401/// [formualizer-docgen:schema:start]
9402/// Name: BETA.INV
9403/// Type: BetaInvFn
9404/// Min args: 3
9405/// Max args: variadic
9406/// Variadic: true
9407/// Signature: BETA.INV(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar, arg5...: number@scalar)
9408/// 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=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg5{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
9409/// Caps: PURE
9410/// [formualizer-docgen:schema:end]
9411impl Function for BetaInvFn {
9412    func_caps!(PURE);
9413    fn name(&self) -> &'static str {
9414        "BETA.INV"
9415    }
9416    fn min_args(&self) -> usize {
9417        3
9418    }
9419    fn variadic(&self) -> bool {
9420        true
9421    }
9422    fn arg_schema(&self) -> &'static [ArgSchema] {
9423        use std::sync::LazyLock;
9424        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
9425            vec![
9426                ArgSchema::number_lenient_scalar(),
9427                ArgSchema::number_lenient_scalar(),
9428                ArgSchema::number_lenient_scalar(),
9429                ArgSchema::number_lenient_scalar(),
9430                ArgSchema::number_lenient_scalar(),
9431            ]
9432        });
9433        &SCHEMA[..]
9434    }
9435    fn eval<'a, 'b, 'c>(
9436        &self,
9437        args: &'c [ArgumentHandle<'a, 'b>],
9438        _ctx: &dyn FunctionContext<'b>,
9439    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
9440        let p = coerce_num(&scalar_like_value(&args[0])?)?;
9441        let alpha = coerce_num(&scalar_like_value(&args[1])?)?;
9442        let beta_param = coerce_num(&scalar_like_value(&args[2])?)?;
9443        let a_bound = if args.len() > 3 {
9444            coerce_num(&scalar_like_value(&args[3])?)?
9445        } else {
9446            0.0
9447        };
9448        let b_bound = if args.len() > 4 {
9449            coerce_num(&scalar_like_value(&args[4])?)?
9450        } else {
9451            1.0
9452        };
9453
9454        if alpha <= 0.0 || beta_param <= 0.0 || a_bound >= b_bound || !(0.0..=1.0).contains(&p) {
9455            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9456                ExcelError::new_num(),
9457            )));
9458        }
9459
9460        match beta_inv_helper(p, alpha, beta_param) {
9461            Some(x_std) => {
9462                let result = a_bound + x_std * (b_bound - a_bound);
9463                Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
9464                    result,
9465                )))
9466            }
9467            None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9468                ExcelError::new_num(),
9469            ))),
9470        }
9471    }
9472}
9473
9474/* ─────────────────────────── BINOM.DIST.RANGE ──────────────────────────── */
9475
9476/// Returns the probability that a binomial random variable falls within a range of successes.
9477///
9478/// # Formula example
9479/// ```excel
9480/// # returns: 0.1171875
9481/// =BINOM.DIST.RANGE(10,0.5,3,3)
9482/// ```
9483///
9484/// ```yaml,sandbox
9485/// title: "Probability of exactly three successes"
9486/// formula: '=BINOM.DIST.RANGE(10,0.5,3,3)'
9487/// expected: 0.1171875
9488/// ```
9489///
9490/// ```yaml,docs
9491/// related:
9492///   - BINOM.DIST
9493///   - BINOM.INV
9494///   - POISSON.DIST
9495/// faq:
9496///   - q: "What happens if the upper bound is omitted?"
9497///     a: "The function treats the lower and upper bounds as the same value, yielding the probability of exactly that many successes."
9498/// ```
9499#[derive(Debug)]
9500pub struct BinomDistRangeFn;
9501/// [formualizer-docgen:schema:start]
9502/// Name: BINOM.DIST.RANGE
9503/// Type: BinomDistRangeFn
9504/// Min args: 3
9505/// Max args: variadic
9506/// Variadic: true
9507/// Signature: BINOM.DIST.RANGE(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4...: number@scalar)
9508/// 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=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
9509/// Caps: PURE
9510/// [formualizer-docgen:schema:end]
9511impl Function for BinomDistRangeFn {
9512    func_caps!(PURE);
9513    fn name(&self) -> &'static str {
9514        "BINOM.DIST.RANGE"
9515    }
9516    fn min_args(&self) -> usize {
9517        3
9518    }
9519    fn variadic(&self) -> bool {
9520        true
9521    }
9522    fn arg_schema(&self) -> &'static [ArgSchema] {
9523        use std::sync::LazyLock;
9524        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
9525            vec![
9526                ArgSchema::number_lenient_scalar(),
9527                ArgSchema::number_lenient_scalar(),
9528                ArgSchema::number_lenient_scalar(),
9529                ArgSchema::number_lenient_scalar(),
9530            ]
9531        });
9532        &SCHEMA[..]
9533    }
9534    fn eval<'a, 'b, 'c>(
9535        &self,
9536        args: &'c [ArgumentHandle<'a, 'b>],
9537        _ctx: &dyn FunctionContext<'b>,
9538    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
9539        let n = coerce_num(&scalar_like_value(&args[0])?)?.trunc() as i64;
9540        let p = coerce_num(&scalar_like_value(&args[1])?)?;
9541        let s = coerce_num(&scalar_like_value(&args[2])?)?.trunc() as i64;
9542        let s2 = if args.len() > 3 {
9543            coerce_num(&scalar_like_value(&args[3])?)?.trunc() as i64
9544        } else {
9545            s
9546        };
9547
9548        if n < 0 || !(0.0..=1.0).contains(&p) || s < 0 || s > n || s2 < s || s2 > n {
9549            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9550                ExcelError::new_num(),
9551            )));
9552        }
9553
9554        let mut sum = 0.0;
9555        for k in s..=s2 {
9556            let ln_prob = ln_binom(n, k) + (k as f64) * p.ln() + ((n - k) as f64) * (1.0 - p).ln();
9557            sum += ln_prob.exp();
9558        }
9559        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(sum)))
9560    }
9561}
9562
9563/* ─────────────────────────── BINOM.INV ──────────────────────────── */
9564
9565/// Returns the smallest number of successes whose cumulative binomial probability meets a threshold.
9566///
9567/// # Formula example
9568/// ```excel
9569/// # returns: 5
9570/// =BINOM.INV(10,0.5,0.5)
9571/// ```
9572///
9573/// ```yaml,sandbox
9574/// title: "Median success threshold"
9575/// formula: '=BINOM.INV(10,0.5,0.5)'
9576/// expected: 5
9577/// ```
9578///
9579/// ```yaml,docs
9580/// related:
9581///   - BINOM.DIST
9582///   - BINOM.DIST.RANGE
9583///   - CRITBINOM
9584/// faq:
9585///   - q: "Is CRITBINOM supported?"
9586///     a: "Yes. CRITBINOM is registered as an alias of BINOM.INV."
9587/// ```
9588#[derive(Debug)]
9589pub struct BinomInvFn;
9590/// [formualizer-docgen:schema:start]
9591/// Name: BINOM.INV
9592/// Type: BinomInvFn
9593/// Min args: 3
9594/// Max args: 3
9595/// Variadic: false
9596/// Signature: BINOM.INV(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
9597/// 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}
9598/// Caps: PURE
9599/// [formualizer-docgen:schema:end]
9600impl Function for BinomInvFn {
9601    func_caps!(PURE);
9602    fn name(&self) -> &'static str {
9603        "BINOM.INV"
9604    }
9605    fn aliases(&self) -> &'static [&'static str] {
9606        &["CRITBINOM"]
9607    }
9608    fn min_args(&self) -> usize {
9609        3
9610    }
9611    fn arg_schema(&self) -> &'static [ArgSchema] {
9612        use std::sync::LazyLock;
9613        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
9614            vec![
9615                ArgSchema::number_lenient_scalar(),
9616                ArgSchema::number_lenient_scalar(),
9617                ArgSchema::number_lenient_scalar(),
9618            ]
9619        });
9620        &SCHEMA[..]
9621    }
9622    fn eval<'a, 'b, 'c>(
9623        &self,
9624        args: &'c [ArgumentHandle<'a, 'b>],
9625        _ctx: &dyn FunctionContext<'b>,
9626    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
9627        let n = coerce_num(&scalar_like_value(&args[0])?)?.trunc() as i64;
9628        let p = coerce_num(&scalar_like_value(&args[1])?)?;
9629        let alpha = coerce_num(&scalar_like_value(&args[2])?)?;
9630
9631        if n < 0 || !(0.0..=1.0).contains(&p) || !(0.0..=1.0).contains(&alpha) {
9632            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9633                ExcelError::new_num(),
9634            )));
9635        }
9636
9637        // Find smallest k such that BINOM.DIST(k, n, p, TRUE) >= alpha
9638        let mut cum = 0.0;
9639        for k in 0..=n {
9640            let ln_prob = ln_binom(n, k) + (k as f64) * p.ln() + ((n - k) as f64) * (1.0 - p).ln();
9641            cum += ln_prob.exp();
9642            if cum >= alpha {
9643                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
9644                    k as f64,
9645                )));
9646            }
9647        }
9648        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
9649            n as f64,
9650        )))
9651    }
9652}
9653
9654/* ─────────────────────────── GAMMA ──────────────────────────── */
9655
9656/// Returns the value of the gamma function.
9657///
9658/// # Formula example
9659/// ```excel
9660/// # returns: 24
9661/// =GAMMA(5)
9662/// ```
9663///
9664/// ```yaml,sandbox
9665/// title: "Gamma extends factorials"
9666/// formula: '=GAMMA(5)'
9667/// expected: 24
9668/// ```
9669///
9670/// ```yaml,docs
9671/// related:
9672///   - GAMMALN
9673///   - GAMMA.INV
9674///   - FACT
9675/// faq:
9676///   - q: "When does GAMMA return #NUM!?"
9677///     a: "It returns #NUM! for zero and negative integers, where the gamma function has poles."
9678/// ```
9679#[derive(Debug)]
9680pub struct GammaFn;
9681/// [formualizer-docgen:schema:start]
9682/// Name: GAMMA
9683/// Type: GammaFn
9684/// Min args: 1
9685/// Max args: 1
9686/// Variadic: false
9687/// Signature: GAMMA(arg1: number@scalar)
9688/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
9689/// Caps: PURE
9690/// [formualizer-docgen:schema:end]
9691impl Function for GammaFn {
9692    func_caps!(PURE);
9693    fn name(&self) -> &'static str {
9694        "GAMMA"
9695    }
9696    fn min_args(&self) -> usize {
9697        1
9698    }
9699    fn arg_schema(&self) -> &'static [ArgSchema] {
9700        use std::sync::LazyLock;
9701        static SCHEMA: LazyLock<Vec<ArgSchema>> =
9702            LazyLock::new(|| vec![ArgSchema::number_lenient_scalar()]);
9703        &SCHEMA[..]
9704    }
9705    fn eval<'a, 'b, 'c>(
9706        &self,
9707        args: &'c [ArgumentHandle<'a, 'b>],
9708        _ctx: &dyn FunctionContext<'b>,
9709    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
9710        let x = coerce_num(&scalar_like_value(&args[0])?)?;
9711        // GAMMA(0) and negative integers are #NUM!
9712        if x == 0.0 || (x < 0.0 && x == x.trunc()) {
9713            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9714                ExcelError::new_num(),
9715            )));
9716        }
9717        let result = ln_gamma(x).exp();
9718        if result.is_infinite() || result.is_nan() {
9719            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9720                ExcelError::new_num(),
9721            )));
9722        }
9723        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
9724            result,
9725        )))
9726    }
9727}
9728
9729/* ─────────────────────────── GAMMA.INV ──────────────────────────── */
9730
9731/// Returns the inverse cumulative gamma distribution.
9732///
9733/// # Formula example
9734/// ```excel
9735/// # returns: 0.6931471805599453
9736/// =GAMMA.INV(0.5,1,1)
9737/// ```
9738///
9739/// ```yaml,sandbox
9740/// title: "Exponential special case"
9741/// formula: '=GAMMA.INV(0.5,1,1)'
9742/// expected: 0.6931471805599453
9743/// ```
9744///
9745/// ```yaml,docs
9746/// related:
9747///   - GAMMA.DIST
9748///   - GAMMA
9749///   - BETA.INV
9750/// faq:
9751///   - q: "Is GAMMAINV supported?"
9752///     a: "Yes. GAMMAINV is registered as an alias of GAMMA.INV."
9753/// ```
9754#[derive(Debug)]
9755pub struct GammaInvFn;
9756/// [formualizer-docgen:schema:start]
9757/// Name: GAMMA.INV
9758/// Type: GammaInvFn
9759/// Min args: 3
9760/// Max args: 3
9761/// Variadic: false
9762/// Signature: GAMMA.INV(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
9763/// 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}
9764/// Caps: PURE
9765/// [formualizer-docgen:schema:end]
9766impl Function for GammaInvFn {
9767    func_caps!(PURE);
9768    fn name(&self) -> &'static str {
9769        "GAMMA.INV"
9770    }
9771    fn aliases(&self) -> &'static [&'static str] {
9772        &["GAMMAINV"]
9773    }
9774    fn min_args(&self) -> usize {
9775        3
9776    }
9777    fn arg_schema(&self) -> &'static [ArgSchema] {
9778        use std::sync::LazyLock;
9779        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
9780            vec![
9781                ArgSchema::number_lenient_scalar(),
9782                ArgSchema::number_lenient_scalar(),
9783                ArgSchema::number_lenient_scalar(),
9784            ]
9785        });
9786        &SCHEMA[..]
9787    }
9788    fn eval<'a, 'b, 'c>(
9789        &self,
9790        args: &'c [ArgumentHandle<'a, 'b>],
9791        _ctx: &dyn FunctionContext<'b>,
9792    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
9793        let p = coerce_num(&scalar_like_value(&args[0])?)?;
9794        let alpha = coerce_num(&scalar_like_value(&args[1])?)?;
9795        let beta = coerce_num(&scalar_like_value(&args[2])?)?;
9796
9797        if alpha <= 0.0 || beta <= 0.0 || !(0.0..=1.0).contains(&p) {
9798            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9799                ExcelError::new_num(),
9800            )));
9801        }
9802
9803        match gamma_inv_helper(p, alpha, beta) {
9804            Some(result) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
9805                result,
9806            ))),
9807            None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9808                ExcelError::new_num(),
9809            ))),
9810        }
9811    }
9812}
9813
9814/* ─────────────────────────── GAMMALN ──────────────────────────── */
9815
9816/// Returns the natural logarithm of the gamma function.
9817///
9818/// # Formula example
9819/// ```excel
9820/// # returns: 3.1780538303479458
9821/// =GAMMALN(5)
9822/// ```
9823///
9824/// ```yaml,sandbox
9825/// title: "Log gamma at 5"
9826/// formula: '=GAMMALN(5)'
9827/// expected: 3.1780538303479458
9828/// ```
9829///
9830/// ```yaml,docs
9831/// related:
9832///   - GAMMALN.PRECISE
9833///   - GAMMA
9834///   - LN
9835/// faq:
9836///   - q: "When does GAMMALN return #NUM!?"
9837///     a: "It returns #NUM! for zero or negative inputs."
9838/// ```
9839#[derive(Debug)]
9840pub struct GammaLnFn;
9841/// [formualizer-docgen:schema:start]
9842/// Name: GAMMALN
9843/// Type: GammaLnFn
9844/// Min args: 1
9845/// Max args: 1
9846/// Variadic: false
9847/// Signature: GAMMALN(arg1: number@scalar)
9848/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
9849/// Caps: PURE
9850/// [formualizer-docgen:schema:end]
9851impl Function for GammaLnFn {
9852    func_caps!(PURE);
9853    fn name(&self) -> &'static str {
9854        "GAMMALN"
9855    }
9856    fn min_args(&self) -> usize {
9857        1
9858    }
9859    fn arg_schema(&self) -> &'static [ArgSchema] {
9860        use std::sync::LazyLock;
9861        static SCHEMA: LazyLock<Vec<ArgSchema>> =
9862            LazyLock::new(|| vec![ArgSchema::number_lenient_scalar()]);
9863        &SCHEMA[..]
9864    }
9865    fn eval<'a, 'b, 'c>(
9866        &self,
9867        args: &'c [ArgumentHandle<'a, 'b>],
9868        _ctx: &dyn FunctionContext<'b>,
9869    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
9870        let x = coerce_num(&scalar_like_value(&args[0])?)?;
9871        if x <= 0.0 {
9872            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9873                ExcelError::new_num(),
9874            )));
9875        }
9876        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
9877            ln_gamma(x),
9878        )))
9879    }
9880}
9881
9882/* ─────────────────────────── GAMMALN.PRECISE ──────────────────────────── */
9883
9884/// Returns the natural logarithm of the gamma function using Excel's precise naming variant.
9885///
9886/// # Formula example
9887/// ```excel
9888/// # returns: 3.1780538303479458
9889/// =GAMMALN.PRECISE(5)
9890/// ```
9891///
9892/// ```yaml,sandbox
9893/// title: "Precise log gamma at 5"
9894/// formula: '=GAMMALN.PRECISE(5)'
9895/// expected: 3.1780538303479458
9896/// ```
9897///
9898/// ```yaml,docs
9899/// related:
9900///   - GAMMALN
9901///   - GAMMA
9902///   - LN
9903/// faq:
9904///   - q: "How does GAMMALN.PRECISE differ here?"
9905///     a: "This implementation uses the same core log-gamma calculation as GAMMALN, matching Excel's function naming split."
9906/// ```
9907#[derive(Debug)]
9908pub struct GammaLnPreciseFn;
9909/// [formualizer-docgen:schema:start]
9910/// Name: GAMMALN.PRECISE
9911/// Type: GammaLnPreciseFn
9912/// Min args: 1
9913/// Max args: 1
9914/// Variadic: false
9915/// Signature: GAMMALN.PRECISE(arg1: number@scalar)
9916/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
9917/// Caps: PURE
9918/// [formualizer-docgen:schema:end]
9919impl Function for GammaLnPreciseFn {
9920    func_caps!(PURE);
9921    fn name(&self) -> &'static str {
9922        "GAMMALN.PRECISE"
9923    }
9924    fn min_args(&self) -> usize {
9925        1
9926    }
9927    fn arg_schema(&self) -> &'static [ArgSchema] {
9928        use std::sync::LazyLock;
9929        static SCHEMA: LazyLock<Vec<ArgSchema>> =
9930            LazyLock::new(|| vec![ArgSchema::number_lenient_scalar()]);
9931        &SCHEMA[..]
9932    }
9933    fn eval<'a, 'b, 'c>(
9934        &self,
9935        args: &'c [ArgumentHandle<'a, 'b>],
9936        _ctx: &dyn FunctionContext<'b>,
9937    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
9938        let x = coerce_num(&scalar_like_value(&args[0])?)?;
9939        if x <= 0.0 {
9940            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9941                ExcelError::new_num(),
9942            )));
9943        }
9944        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
9945            ln_gamma(x),
9946        )))
9947    }
9948}
9949
9950pub fn register_builtins() {
9951    use std::sync::Arc;
9952    crate::function_registry::register_function(Arc::new(ForecastLinearFn));
9953    crate::function_registry::register_function(Arc::new(LinestFn));
9954    crate::function_registry::register_function(Arc::new(LARGE));
9955    crate::function_registry::register_function(Arc::new(SMALL));
9956    crate::function_registry::register_function(Arc::new(MEDIAN));
9957    crate::function_registry::register_function(Arc::new(StdevSample));
9958    crate::function_registry::register_function(Arc::new(StdevPop));
9959    crate::function_registry::register_function(Arc::new(VarSample));
9960    crate::function_registry::register_function(Arc::new(VarPop));
9961    crate::function_registry::register_function(Arc::new(PercentileInc));
9962    crate::function_registry::register_function(Arc::new(PercentileExc));
9963    crate::function_registry::register_function(Arc::new(QuartileInc));
9964    crate::function_registry::register_function(Arc::new(QuartileExc));
9965    crate::function_registry::register_function(Arc::new(RankEqFn));
9966    crate::function_registry::register_function(Arc::new(RankAvgFn));
9967    crate::function_registry::register_function(Arc::new(ModeSingleFn));
9968    crate::function_registry::register_function(Arc::new(ModeMultiFn));
9969    crate::function_registry::register_function(Arc::new(ProductFn));
9970    crate::function_registry::register_function(Arc::new(GeomeanFn));
9971    crate::function_registry::register_function(Arc::new(HarmeanFn));
9972    crate::function_registry::register_function(Arc::new(AvedevFn));
9973    crate::function_registry::register_function(Arc::new(DevsqFn));
9974    crate::function_registry::register_function(Arc::new(MaxIfsFn));
9975    crate::function_registry::register_function(Arc::new(MinIfsFn));
9976    crate::function_registry::register_function(Arc::new(TrimmeanFn));
9977    crate::function_registry::register_function(Arc::new(CorrelFn));
9978    crate::function_registry::register_function(Arc::new(SlopeFn));
9979    crate::function_registry::register_function(Arc::new(InterceptFn));
9980    // Covariance and correlation functions
9981    crate::function_registry::register_function(Arc::new(CovariancePFn));
9982    crate::function_registry::register_function(Arc::new(CovarianceSFn));
9983    crate::function_registry::register_function(Arc::new(PearsonFn));
9984    crate::function_registry::register_function(Arc::new(RsqFn));
9985    crate::function_registry::register_function(Arc::new(SteyxFn));
9986    crate::function_registry::register_function(Arc::new(SkewFn));
9987    crate::function_registry::register_function(Arc::new(KurtFn));
9988    crate::function_registry::register_function(Arc::new(FisherFn));
9989    crate::function_registry::register_function(Arc::new(FisherInvFn));
9990    // Statistical distributions
9991    crate::function_registry::register_function(Arc::new(NormSDistFn));
9992    crate::function_registry::register_function(Arc::new(NormSInvFn));
9993    crate::function_registry::register_function(Arc::new(NormDistFn));
9994    crate::function_registry::register_function(Arc::new(NormInvFn));
9995    crate::function_registry::register_function(Arc::new(LognormDistFn));
9996    crate::function_registry::register_function(Arc::new(LognormInvFn));
9997    crate::function_registry::register_function(Arc::new(PhiFn));
9998    crate::function_registry::register_function(Arc::new(GaussFn));
9999    crate::function_registry::register_function(Arc::new(StandardizeFn));
10000    crate::function_registry::register_function(Arc::new(TDistFn));
10001    crate::function_registry::register_function(Arc::new(TInvFn));
10002    crate::function_registry::register_function(Arc::new(ChisqDistFn));
10003    crate::function_registry::register_function(Arc::new(ChisqInvFn));
10004    crate::function_registry::register_function(Arc::new(FDistFn));
10005    crate::function_registry::register_function(Arc::new(FInvFn));
10006    // Discrete distributions
10007    crate::function_registry::register_function(Arc::new(BinomDistFn));
10008    crate::function_registry::register_function(Arc::new(PoissonDistFn));
10009    crate::function_registry::register_function(Arc::new(ExponDistFn));
10010    crate::function_registry::register_function(Arc::new(GammaDistFn));
10011    // Additional distributions
10012    crate::function_registry::register_function(Arc::new(WeibullDistFn));
10013    crate::function_registry::register_function(Arc::new(BetaDistFn));
10014    crate::function_registry::register_function(Arc::new(NegbinomDistFn));
10015    crate::function_registry::register_function(Arc::new(HypgeomDistFn));
10016    // Confidence intervals and hypothesis testing
10017    crate::function_registry::register_function(Arc::new(ConfidenceNormFn));
10018    crate::function_registry::register_function(Arc::new(ConfidenceTFn));
10019    crate::function_registry::register_function(Arc::new(ZTestFn));
10020    // Regression and trend functions
10021    crate::function_registry::register_function(Arc::new(TrendFn));
10022    crate::function_registry::register_function(Arc::new(GrowthFn));
10023    crate::function_registry::register_function(Arc::new(LogestFn));
10024    // Percent rank and frequency functions
10025    crate::function_registry::register_function(Arc::new(PercentRankIncFn));
10026    crate::function_registry::register_function(Arc::new(PercentRankExcFn));
10027    crate::function_registry::register_function(Arc::new(FrequencyFn));
10028    // Hypothesis testing functions
10029    crate::function_registry::register_function(Arc::new(TDist2TFn));
10030    crate::function_registry::register_function(Arc::new(TInv2TFn));
10031    crate::function_registry::register_function(Arc::new(TTestFn));
10032    crate::function_registry::register_function(Arc::new(FTestFn));
10033    crate::function_registry::register_function(Arc::new(ChisqTestFn));
10034    // FZ-PAR-01 batch
10035    crate::function_registry::register_function(Arc::new(AverageAFn));
10036    crate::function_registry::register_function(Arc::new(MaxAFn));
10037    crate::function_registry::register_function(Arc::new(MinAFn));
10038    crate::function_registry::register_function(Arc::new(StdevAFn));
10039    crate::function_registry::register_function(Arc::new(StdevPAFn));
10040    crate::function_registry::register_function(Arc::new(VarAFn));
10041    crate::function_registry::register_function(Arc::new(VarPAFn));
10042    crate::function_registry::register_function(Arc::new(SkewPFn));
10043    crate::function_registry::register_function(Arc::new(TDistRtFn));
10044    crate::function_registry::register_function(Arc::new(ChisqDistRtFn));
10045    crate::function_registry::register_function(Arc::new(ChisqInvRtFn));
10046    crate::function_registry::register_function(Arc::new(FDistRtFn));
10047    crate::function_registry::register_function(Arc::new(FInvRtFn));
10048    crate::function_registry::register_function(Arc::new(BetaInvFn));
10049    crate::function_registry::register_function(Arc::new(BinomDistRangeFn));
10050    crate::function_registry::register_function(Arc::new(BinomInvFn));
10051    crate::function_registry::register_function(Arc::new(GammaFn));
10052    crate::function_registry::register_function(Arc::new(GammaInvFn));
10053    crate::function_registry::register_function(Arc::new(GammaLnFn));
10054    crate::function_registry::register_function(Arc::new(GammaLnPreciseFn));
10055}
10056
10057#[cfg(test)]
10058mod tests_basic_stats {
10059    use super::*;
10060    use crate::test_workbook::TestWorkbook;
10061    use crate::traits::ArgumentHandle;
10062    use formualizer_common::LiteralValue;
10063    use formualizer_parse::parser::{ASTNode, ASTNodeType};
10064    fn interp(wb: &TestWorkbook) -> crate::interpreter::Interpreter<'_> {
10065        wb.interpreter()
10066    }
10067    fn arr(vals: Vec<f64>) -> ASTNode {
10068        ASTNode::new(
10069            ASTNodeType::Literal(LiteralValue::Array(vec![
10070                vals.into_iter().map(LiteralValue::Number).collect(),
10071            ])),
10072            None,
10073        )
10074    }
10075    #[test]
10076    fn median_even() {
10077        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(MEDIAN));
10078        let ctx = interp(&wb);
10079        let node = arr(vec![1.0, 3.0, 5.0, 7.0]);
10080        let f = ctx.context.get_function("", "MEDIAN").unwrap();
10081        let out = f
10082            .dispatch(
10083                &[ArgumentHandle::new(&node, &ctx)],
10084                &ctx.function_context(None),
10085            )
10086            .unwrap();
10087        assert_eq!(out, LiteralValue::Number(4.0));
10088    }
10089    #[test]
10090    fn median_odd() {
10091        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(MEDIAN));
10092        let ctx = interp(&wb);
10093        let node = arr(vec![1.0, 9.0, 5.0]);
10094        let f = ctx.context.get_function("", "MEDIAN").unwrap();
10095        let out = f
10096            .dispatch(
10097                &[ArgumentHandle::new(&node, &ctx)],
10098                &ctx.function_context(None),
10099            )
10100            .unwrap();
10101        assert_eq!(out, LiteralValue::Number(5.0));
10102    }
10103    #[test]
10104    fn large_basic() {
10105        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(LARGE));
10106        let ctx = interp(&wb);
10107        let nums = arr(vec![10.0, 20.0, 30.0]);
10108        let k = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(2.0)), None);
10109        let f = ctx.context.get_function("", "LARGE").unwrap();
10110        let out = f
10111            .dispatch(
10112                &[
10113                    ArgumentHandle::new(&nums, &ctx),
10114                    ArgumentHandle::new(&k, &ctx),
10115                ],
10116                &ctx.function_context(None),
10117            )
10118            .unwrap();
10119        assert_eq!(out, LiteralValue::Number(20.0));
10120    }
10121    #[test]
10122    fn small_basic() {
10123        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(SMALL));
10124        let ctx = interp(&wb);
10125        let nums = arr(vec![10.0, 20.0, 30.0]);
10126        let k = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(2.0)), None);
10127        let f = ctx.context.get_function("", "SMALL").unwrap();
10128        let out = f
10129            .dispatch(
10130                &[
10131                    ArgumentHandle::new(&nums, &ctx),
10132                    ArgumentHandle::new(&k, &ctx),
10133                ],
10134                &ctx.function_context(None),
10135            )
10136            .unwrap();
10137        assert_eq!(out, LiteralValue::Number(20.0));
10138    }
10139    #[test]
10140    fn percentile_inc_quarter() {
10141        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(PercentileInc));
10142        let ctx = interp(&wb);
10143        let nums = arr(vec![1.0, 2.0, 3.0, 4.0]);
10144        let p = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(0.25)), None);
10145        let f = ctx.context.get_function("", "PERCENTILE.INC").unwrap();
10146        match f
10147            .dispatch(
10148                &[
10149                    ArgumentHandle::new(&nums, &ctx),
10150                    ArgumentHandle::new(&p, &ctx),
10151                ],
10152                &ctx.function_context(None),
10153            )
10154            .unwrap()
10155            .into_literal()
10156        {
10157            LiteralValue::Number(v) => assert!((v - 1.75).abs() < 1e-9),
10158            other => panic!("unexpected {other:?}"),
10159        }
10160    }
10161    #[test]
10162    fn rank_eq_descending() {
10163        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(RankEqFn));
10164        let ctx = interp(&wb);
10165        // target 5 among {10,5,1} descending => ranks 1,2,3 => expect 2
10166        let target = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(5.0)), None);
10167        let arr_node = arr(vec![10.0, 5.0, 1.0]);
10168        let f = ctx.context.get_function("", "RANK.EQ").unwrap();
10169        let out = f
10170            .dispatch(
10171                &[
10172                    ArgumentHandle::new(&target, &ctx),
10173                    ArgumentHandle::new(&arr_node, &ctx),
10174                ],
10175                &ctx.function_context(None),
10176            )
10177            .unwrap();
10178        assert_eq!(out, LiteralValue::Number(2.0));
10179    }
10180    #[test]
10181    fn rank_eq_ascending_order_arg() {
10182        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(RankEqFn));
10183        let ctx = interp(&wb);
10184        // ascending order=1: array {1,5,10}; target 5 => rank 2
10185        let target = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(5.0)), None);
10186        let arr_node = arr(vec![1.0, 5.0, 10.0]);
10187        let order = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(1.0)), None);
10188        let f = ctx.context.get_function("", "RANK.EQ").unwrap();
10189        let out = f
10190            .dispatch(
10191                &[
10192                    ArgumentHandle::new(&target, &ctx),
10193                    ArgumentHandle::new(&arr_node, &ctx),
10194                    ArgumentHandle::new(&order, &ctx),
10195                ],
10196                &ctx.function_context(None),
10197            )
10198            .unwrap();
10199        assert_eq!(out, LiteralValue::Number(2.0));
10200    }
10201    #[test]
10202    fn rank_avg_ties() {
10203        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(RankAvgFn));
10204        let ctx = interp(&wb);
10205        // descending array {5,5,1} target 5 positions 1 and 2 avg -> 1.5
10206        let target = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(5.0)), None);
10207        let arr_node = arr(vec![5.0, 5.0, 1.0]);
10208        let f = ctx.context.get_function("", "RANK.AVG").unwrap();
10209        let out = f
10210            .dispatch(
10211                &[
10212                    ArgumentHandle::new(&target, &ctx),
10213                    ArgumentHandle::new(&arr_node, &ctx),
10214                ],
10215                &ctx.function_context(None),
10216            )
10217            .unwrap()
10218            .into_literal();
10219        match out {
10220            LiteralValue::Number(v) => assert!((v - 1.5).abs() < 1e-12),
10221            other => panic!("expected number got {other:?}"),
10222        }
10223    }
10224    #[test]
10225    fn stdev_var_sample_population() {
10226        let wb = TestWorkbook::new()
10227            .with_function(std::sync::Arc::new(StdevSample))
10228            .with_function(std::sync::Arc::new(StdevPop))
10229            .with_function(std::sync::Arc::new(VarSample))
10230            .with_function(std::sync::Arc::new(VarPop));
10231        let ctx = interp(&wb);
10232        let arr_node = arr(vec![2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0]); // variance population = 4, sample = 4.571428...
10233        let stdev_p = ctx.context.get_function("", "STDEV.P").unwrap();
10234        let stdev_s = ctx.context.get_function("", "STDEV.S").unwrap();
10235        let var_p = ctx.context.get_function("", "VAR.P").unwrap();
10236        let var_s = ctx.context.get_function("", "VAR.S").unwrap();
10237        let args = [ArgumentHandle::new(&arr_node, &ctx)];
10238        match var_p
10239            .dispatch(&args, &ctx.function_context(None))
10240            .unwrap()
10241            .into_literal()
10242        {
10243            LiteralValue::Number(v) => assert!((v - 4.0).abs() < 1e-12),
10244            other => panic!("unexpected {other:?}"),
10245        }
10246        match var_s
10247            .dispatch(&args, &ctx.function_context(None))
10248            .unwrap()
10249            .into_literal()
10250        {
10251            LiteralValue::Number(v) => assert!((v - 4.571428571428571).abs() < 1e-9),
10252            other => panic!("unexpected {other:?}"),
10253        }
10254        match stdev_p
10255            .dispatch(&args, &ctx.function_context(None))
10256            .unwrap()
10257            .into_literal()
10258        {
10259            LiteralValue::Number(v) => assert!((v - 2.0).abs() < 1e-12),
10260            other => panic!("unexpected {other:?}"),
10261        }
10262        match stdev_s
10263            .dispatch(&args, &ctx.function_context(None))
10264            .unwrap()
10265            .into_literal()
10266        {
10267            LiteralValue::Number(v) => assert!((v - 2.138089935).abs() < 1e-9),
10268            other => panic!("unexpected {other:?}"),
10269        }
10270    }
10271    #[test]
10272    fn quartile_inc_exc() {
10273        let wb = TestWorkbook::new()
10274            .with_function(std::sync::Arc::new(QuartileInc))
10275            .with_function(std::sync::Arc::new(QuartileExc));
10276        let ctx = interp(&wb);
10277        let arr_node = arr(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
10278        let q1 = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(1.0)), None);
10279        let q2 = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(2.0)), None);
10280        let f_inc = ctx.context.get_function("", "QUARTILE.INC").unwrap();
10281        let f_exc = ctx.context.get_function("", "QUARTILE.EXC").unwrap();
10282        let arg_inc_q1 = [
10283            ArgumentHandle::new(&arr_node, &ctx),
10284            ArgumentHandle::new(&q1, &ctx),
10285        ];
10286        let arg_inc_q2 = [
10287            ArgumentHandle::new(&arr_node, &ctx),
10288            ArgumentHandle::new(&q2, &ctx),
10289        ];
10290        match f_inc
10291            .dispatch(&arg_inc_q1, &ctx.function_context(None))
10292            .unwrap()
10293            .into_literal()
10294        {
10295            LiteralValue::Number(v) => assert!((v - 2.0).abs() < 1e-12),
10296            other => panic!("unexpected {other:?}"),
10297        }
10298        match f_inc
10299            .dispatch(&arg_inc_q2, &ctx.function_context(None))
10300            .unwrap()
10301            .into_literal()
10302        {
10303            LiteralValue::Number(v) => assert!((v - 3.0).abs() < 1e-12),
10304            other => panic!("unexpected {other:?}"),
10305        }
10306        // QUARTILE.EXC Q1 for 5-point set uses exclusive percentile => 1.5
10307        match f_exc
10308            .dispatch(&arg_inc_q1, &ctx.function_context(None))
10309            .unwrap()
10310            .into_literal()
10311        {
10312            LiteralValue::Number(v) => assert!((v - 1.5).abs() < 1e-12),
10313            other => panic!("unexpected {other:?}"),
10314        }
10315        match f_exc
10316            .dispatch(&arg_inc_q2, &ctx.function_context(None))
10317            .unwrap()
10318            .into_literal()
10319        {
10320            LiteralValue::Number(v) => assert!((v - 3.0).abs() < 1e-12),
10321            other => panic!("unexpected {other:?}"),
10322        }
10323    }
10324
10325    // --- eval()/dispatch equivalence tests for variance / stdev ---
10326    #[test]
10327    fn fold_equivalence_var_stdev() {
10328        use crate::function::Function as _; // trait import
10329        let wb = TestWorkbook::new()
10330            .with_function(std::sync::Arc::new(VarSample))
10331            .with_function(std::sync::Arc::new(VarPop))
10332            .with_function(std::sync::Arc::new(StdevSample))
10333            .with_function(std::sync::Arc::new(StdevPop));
10334        let ctx = interp(&wb);
10335        let arr_node = arr(vec![1.0, 2.0, 5.0, 5.0, 9.0]);
10336        let args = [ArgumentHandle::new(&arr_node, &ctx)];
10337
10338        let var_s_fn = VarSample; // concrete instance to call eval()
10339        let var_p_fn = VarPop;
10340        let stdev_s_fn = StdevSample;
10341        let stdev_p_fn = StdevPop;
10342
10343        let fctx = ctx.function_context(None);
10344        // Dispatch results (will use fold path)
10345        let disp_var_s = ctx
10346            .context
10347            .get_function("", "VAR.S")
10348            .unwrap()
10349            .dispatch(&args, &fctx)
10350            .unwrap()
10351            .into_literal();
10352        let disp_var_p = ctx
10353            .context
10354            .get_function("", "VAR.P")
10355            .unwrap()
10356            .dispatch(&args, &fctx)
10357            .unwrap()
10358            .into_literal();
10359        let disp_stdev_s = ctx
10360            .context
10361            .get_function("", "STDEV.S")
10362            .unwrap()
10363            .dispatch(&args, &fctx)
10364            .unwrap()
10365            .into_literal();
10366        let disp_stdev_p = ctx
10367            .context
10368            .get_function("", "STDEV.P")
10369            .unwrap()
10370            .dispatch(&args, &fctx)
10371            .unwrap()
10372            .into_literal();
10373
10374        // Scalar path results
10375        let scalar_var_s = var_s_fn.eval(&args, &fctx).unwrap().into_literal();
10376        let scalar_var_p = var_p_fn.eval(&args, &fctx).unwrap().into_literal();
10377        let scalar_stdev_s = stdev_s_fn.eval(&args, &fctx).unwrap().into_literal();
10378        let scalar_stdev_p = stdev_p_fn.eval(&args, &fctx).unwrap().into_literal();
10379
10380        fn assert_close(a: &LiteralValue, b: &LiteralValue) {
10381            match (a, b) {
10382                (LiteralValue::Number(x), LiteralValue::Number(y)) => {
10383                    assert!((x - y).abs() < 1e-12, "mismatch {x} vs {y}")
10384                }
10385                _ => assert_eq!(a, b),
10386            }
10387        }
10388        assert_close(&disp_var_s, &scalar_var_s);
10389        assert_close(&disp_var_p, &scalar_var_p);
10390        assert_close(&disp_stdev_s, &scalar_stdev_s);
10391        assert_close(&disp_stdev_p, &scalar_stdev_p);
10392    }
10393
10394    #[test]
10395    fn fold_equivalence_edge_cases() {
10396        use crate::function::Function as _;
10397        let wb = TestWorkbook::new()
10398            .with_function(std::sync::Arc::new(VarSample))
10399            .with_function(std::sync::Arc::new(VarPop))
10400            .with_function(std::sync::Arc::new(StdevSample))
10401            .with_function(std::sync::Arc::new(StdevPop));
10402        let ctx = interp(&wb);
10403        // Single numeric element -> sample variance/div0, population variance 0
10404        let single = arr(vec![42.0]);
10405        let args_single = [ArgumentHandle::new(&single, &ctx)];
10406        let fctx = ctx.function_context(None);
10407        let disp_var_s = ctx
10408            .context
10409            .get_function("", "VAR.S")
10410            .unwrap()
10411            .dispatch(&args_single, &fctx)
10412            .unwrap();
10413        let scalar_var_s = VarSample.eval(&args_single, &fctx).unwrap().into_literal();
10414        assert_eq!(disp_var_s, scalar_var_s);
10415        let disp_var_p = ctx
10416            .context
10417            .get_function("", "VAR.P")
10418            .unwrap()
10419            .dispatch(&args_single, &fctx)
10420            .unwrap();
10421        let scalar_var_p = VarPop.eval(&args_single, &fctx).unwrap().into_literal();
10422        assert_eq!(disp_var_p, scalar_var_p);
10423        let disp_stdev_p = ctx
10424            .context
10425            .get_function("", "STDEV.P")
10426            .unwrap()
10427            .dispatch(&args_single, &fctx)
10428            .unwrap();
10429        let scalar_stdev_p = StdevPop.eval(&args_single, &fctx).unwrap().into_literal();
10430        assert_eq!(disp_stdev_p, scalar_stdev_p);
10431        let disp_stdev_s = ctx
10432            .context
10433            .get_function("", "STDEV.S")
10434            .unwrap()
10435            .dispatch(&args_single, &fctx)
10436            .unwrap();
10437        let scalar_stdev_s = StdevSample
10438            .eval(&args_single, &fctx)
10439            .unwrap()
10440            .into_literal();
10441        assert_eq!(disp_stdev_s, scalar_stdev_s);
10442    }
10443
10444    #[test]
10445    fn legacy_aliases_match_modern() {
10446        let wb = TestWorkbook::new()
10447            .with_function(std::sync::Arc::new(PercentileInc))
10448            .with_function(std::sync::Arc::new(QuartileInc))
10449            .with_function(std::sync::Arc::new(RankEqFn));
10450        let ctx = interp(&wb);
10451        let arr_node = arr(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
10452        let p = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(0.4)), None);
10453        let q2 = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(2.0)), None);
10454        let target = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(4.0)), None);
10455        let args_p = [
10456            ArgumentHandle::new(&arr_node, &ctx),
10457            ArgumentHandle::new(&p, &ctx),
10458        ];
10459        let args_q = [
10460            ArgumentHandle::new(&arr_node, &ctx),
10461            ArgumentHandle::new(&q2, &ctx),
10462        ];
10463        let args_rank = [
10464            ArgumentHandle::new(&target, &ctx),
10465            ArgumentHandle::new(&arr_node, &ctx),
10466        ];
10467        let modern_p = ctx
10468            .context
10469            .get_function("", "PERCENTILE.INC")
10470            .unwrap()
10471            .dispatch(&args_p, &ctx.function_context(None))
10472            .unwrap()
10473            .into_literal();
10474        let legacy_p = ctx
10475            .context
10476            .get_function("", "PERCENTILE")
10477            .unwrap()
10478            .dispatch(&args_p, &ctx.function_context(None))
10479            .unwrap()
10480            .into_literal();
10481        assert_eq!(modern_p, legacy_p);
10482        let modern_q = ctx
10483            .context
10484            .get_function("", "QUARTILE.INC")
10485            .unwrap()
10486            .dispatch(&args_q, &ctx.function_context(None))
10487            .unwrap()
10488            .into_literal();
10489        let legacy_q = ctx
10490            .context
10491            .get_function("", "QUARTILE")
10492            .unwrap()
10493            .dispatch(&args_q, &ctx.function_context(None))
10494            .unwrap()
10495            .into_literal();
10496        assert_eq!(modern_q, legacy_q);
10497        let modern_rank = ctx
10498            .context
10499            .get_function("", "RANK.EQ")
10500            .unwrap()
10501            .dispatch(&args_rank, &ctx.function_context(None))
10502            .unwrap()
10503            .into_literal();
10504        let legacy_rank = ctx
10505            .context
10506            .get_function("", "RANK")
10507            .unwrap()
10508            .dispatch(&args_rank, &ctx.function_context(None))
10509            .unwrap()
10510            .into_literal();
10511        assert_eq!(modern_rank, legacy_rank);
10512    }
10513
10514    #[test]
10515    fn mode_single_basic_and_alias() {
10516        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(ModeSingleFn));
10517        let ctx = interp(&wb);
10518        let arr_node = arr(vec![5.0, 2.0, 2.0, 3.0, 3.0, 3.0]);
10519        let args = [ArgumentHandle::new(&arr_node, &ctx)];
10520        let mode_sngl = ctx
10521            .context
10522            .get_function("", "MODE.SNGL")
10523            .unwrap()
10524            .dispatch(&args, &ctx.function_context(None))
10525            .unwrap()
10526            .into_literal();
10527        assert_eq!(mode_sngl, LiteralValue::Number(3.0));
10528        let mode_alias = ctx
10529            .context
10530            .get_function("", "MODE")
10531            .unwrap()
10532            .dispatch(&args, &ctx.function_context(None))
10533            .unwrap()
10534            .into_literal();
10535        assert_eq!(mode_alias, mode_sngl);
10536    }
10537
10538    #[test]
10539    fn mode_single_no_duplicates() {
10540        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(ModeSingleFn));
10541        let ctx = interp(&wb);
10542        let arr_node = arr(vec![1.0, 2.0, 3.0]);
10543        let args = [ArgumentHandle::new(&arr_node, &ctx)];
10544        let out = ctx
10545            .context
10546            .get_function("", "MODE.SNGL")
10547            .unwrap()
10548            .dispatch(&args, &ctx.function_context(None))
10549            .unwrap()
10550            .into_literal();
10551        match out {
10552            LiteralValue::Error(e) => assert!(e.to_string().contains("#N/A")),
10553            _ => panic!("expected #N/A"),
10554        }
10555    }
10556
10557    #[test]
10558    fn mode_multi_basic() {
10559        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(ModeMultiFn));
10560        let ctx = interp(&wb);
10561        let arr_node = arr(vec![2.0, 3.0, 2.0, 4.0, 3.0, 5.0, 2.0, 3.0]);
10562        let args = [ArgumentHandle::new(&arr_node, &ctx)];
10563        let out = ctx
10564            .context
10565            .get_function("", "MODE.MULT")
10566            .unwrap()
10567            .dispatch(&args, &ctx.function_context(None))
10568            .unwrap()
10569            .into_literal();
10570        let expected = LiteralValue::Array(vec![
10571            vec![LiteralValue::Number(2.0)],
10572            vec![LiteralValue::Number(3.0)],
10573        ]);
10574        assert_eq!(out, expected);
10575    }
10576
10577    #[test]
10578    fn large_small_fold_vs_scalar() {
10579        let wb = TestWorkbook::new()
10580            .with_function(std::sync::Arc::new(LARGE))
10581            .with_function(std::sync::Arc::new(SMALL));
10582        let ctx = interp(&wb);
10583        let arr_node = arr(vec![10.0, 5.0, 7.0, 12.0, 9.0]);
10584        let k_node = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(2.0)), None);
10585        let args = [
10586            ArgumentHandle::new(&arr_node, &ctx),
10587            ArgumentHandle::new(&k_node, &ctx),
10588        ];
10589        let f_large = ctx.context.get_function("", "LARGE").unwrap();
10590        let disp_large = f_large
10591            .dispatch(&args, &ctx.function_context(None))
10592            .unwrap()
10593            .into_literal();
10594        let scalar_large = LARGE
10595            .eval(&args, &ctx.function_context(None))
10596            .unwrap()
10597            .into_literal();
10598        assert_eq!(disp_large, scalar_large);
10599        let f_small = ctx.context.get_function("", "SMALL").unwrap();
10600        let disp_small = f_small
10601            .dispatch(&args, &ctx.function_context(None))
10602            .unwrap()
10603            .into_literal();
10604        let scalar_small = SMALL
10605            .eval(&args, &ctx.function_context(None))
10606            .unwrap()
10607            .into_literal();
10608        assert_eq!(disp_small, scalar_small);
10609    }
10610
10611    #[test]
10612    fn mode_fold_vs_scalar() {
10613        let wb = TestWorkbook::new()
10614            .with_function(std::sync::Arc::new(ModeSingleFn))
10615            .with_function(std::sync::Arc::new(ModeMultiFn));
10616        let ctx = interp(&wb);
10617        let arr_node = arr(vec![2.0, 3.0, 2.0, 4.0, 3.0, 3.0, 2.0]);
10618        let args = [ArgumentHandle::new(&arr_node, &ctx)];
10619        let f_single = ctx.context.get_function("", "MODE.SNGL").unwrap();
10620        let disp_single = f_single
10621            .dispatch(&args, &ctx.function_context(None))
10622            .unwrap()
10623            .into_literal();
10624        let scalar_single = ModeSingleFn
10625            .eval(&args, &ctx.function_context(None))
10626            .unwrap()
10627            .into_literal();
10628        assert_eq!(disp_single, scalar_single);
10629        let f_multi = ctx.context.get_function("", "MODE.MULT").unwrap();
10630        let disp_multi = f_multi
10631            .dispatch(&args, &ctx.function_context(None))
10632            .unwrap()
10633            .into_literal();
10634        let scalar_multi = ModeMultiFn
10635            .eval(&args, &ctx.function_context(None))
10636            .unwrap()
10637            .into_literal();
10638        assert_eq!(disp_multi, scalar_multi);
10639    }
10640
10641    #[test]
10642    fn median_fold_vs_scalar_even() {
10643        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(MEDIAN));
10644        let ctx = interp(&wb);
10645        let arr_node = arr(vec![7.0, 1.0, 9.0, 5.0]); // sorted: 1,5,7,9 median=(5+7)/2=6
10646        let args = [ArgumentHandle::new(&arr_node, &ctx)];
10647        let f_med = ctx.context.get_function("", "MEDIAN").unwrap();
10648        let disp = f_med
10649            .dispatch(&args, &ctx.function_context(None))
10650            .unwrap()
10651            .into_literal();
10652        let scalar = MEDIAN
10653            .eval(&args, &ctx.function_context(None))
10654            .unwrap()
10655            .into_literal();
10656        assert_eq!(disp, scalar);
10657        assert_eq!(disp, LiteralValue::Number(6.0));
10658    }
10659
10660    #[test]
10661    fn median_fold_vs_scalar_odd() {
10662        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(MEDIAN));
10663        let ctx = interp(&wb);
10664        let arr_node = arr(vec![9.0, 2.0, 5.0]); // sorted 2,5,9 median=5
10665        let args = [ArgumentHandle::new(&arr_node, &ctx)];
10666        let f_med = ctx.context.get_function("", "MEDIAN").unwrap();
10667        let disp = f_med
10668            .dispatch(&args, &ctx.function_context(None))
10669            .unwrap()
10670            .into_literal();
10671        let scalar = MEDIAN
10672            .eval(&args, &ctx.function_context(None))
10673            .unwrap()
10674            .into_literal();
10675        assert_eq!(disp, scalar);
10676        assert_eq!(disp, LiteralValue::Number(5.0));
10677    }
10678
10679    // Newly added edge case tests for statistical semantics.
10680    #[test]
10681    fn percentile_inc_edges() {
10682        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(PercentileInc));
10683        let ctx = interp(&wb);
10684        let arr_node = arr(vec![10.0, 20.0, 30.0, 40.0]);
10685        let p0 = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(0.0)), None);
10686        let p1 = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(1.0)), None);
10687        let f = ctx.context.get_function("", "PERCENTILE.INC").unwrap();
10688        let args0 = [
10689            ArgumentHandle::new(&arr_node, &ctx),
10690            ArgumentHandle::new(&p0, &ctx),
10691        ];
10692        let args1 = [
10693            ArgumentHandle::new(&arr_node, &ctx),
10694            ArgumentHandle::new(&p1, &ctx),
10695        ];
10696        assert_eq!(
10697            f.dispatch(&args0, &ctx.function_context(None))
10698                .unwrap()
10699                .into_literal(),
10700            LiteralValue::Number(10.0)
10701        );
10702        assert_eq!(
10703            f.dispatch(&args1, &ctx.function_context(None))
10704                .unwrap()
10705                .into_literal(),
10706            LiteralValue::Number(40.0)
10707        );
10708    }
10709
10710    #[test]
10711    fn percentile_exc_invalid() {
10712        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(PercentileExc));
10713        let ctx = interp(&wb);
10714        let arr_node = arr(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
10715        let p_bad0 = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(0.0)), None);
10716        let p_bad1 = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(1.0)), None);
10717        let f = ctx.context.get_function("", "PERCENTILE.EXC").unwrap();
10718        for bad in [&p_bad0, &p_bad1] {
10719            let args = [
10720                ArgumentHandle::new(&arr_node, &ctx),
10721                ArgumentHandle::new(bad, &ctx),
10722            ];
10723            match f
10724                .dispatch(&args, &ctx.function_context(None))
10725                .unwrap()
10726                .into_literal()
10727            {
10728                LiteralValue::Error(e) => assert!(e.to_string().contains("#NUM!")),
10729                other => panic!("expected #NUM! got {other:?}"),
10730            }
10731        }
10732    }
10733
10734    #[test]
10735    fn quartile_invalids() {
10736        let wb = TestWorkbook::new()
10737            .with_function(std::sync::Arc::new(QuartileInc))
10738            .with_function(std::sync::Arc::new(QuartileExc));
10739        let ctx = interp(&wb);
10740        let arr_node = arr(vec![1.0, 2.0, 3.0]);
10741        // QUARTILE.INC invalid q=5
10742        let q5 = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(5.0)), None);
10743        let args_bad_inc = [
10744            ArgumentHandle::new(&arr_node, &ctx),
10745            ArgumentHandle::new(&q5, &ctx),
10746        ];
10747        match ctx
10748            .context
10749            .get_function("", "QUARTILE.INC")
10750            .unwrap()
10751            .dispatch(&args_bad_inc, &ctx.function_context(None))
10752            .unwrap()
10753            .into_literal()
10754        {
10755            LiteralValue::Error(e) => assert!(e.to_string().contains("#NUM!")),
10756            other => panic!("expected #NUM! {other:?}"),
10757        }
10758        // QUARTILE.EXC invalid q=0
10759        let q0 = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(0.0)), None);
10760        let args_bad_exc = [
10761            ArgumentHandle::new(&arr_node, &ctx),
10762            ArgumentHandle::new(&q0, &ctx),
10763        ];
10764        match ctx
10765            .context
10766            .get_function("", "QUARTILE.EXC")
10767            .unwrap()
10768            .dispatch(&args_bad_exc, &ctx.function_context(None))
10769            .unwrap()
10770            .into_literal()
10771        {
10772            LiteralValue::Error(e) => assert!(e.to_string().contains("#NUM!")),
10773            other => panic!("expected #NUM! {other:?}"),
10774        }
10775    }
10776
10777    #[test]
10778    fn rank_target_not_found() {
10779        let wb = TestWorkbook::new()
10780            .with_function(std::sync::Arc::new(RankEqFn))
10781            .with_function(std::sync::Arc::new(RankAvgFn));
10782        let ctx = interp(&wb);
10783        let arr_node = arr(vec![1.0, 2.0, 3.0]);
10784        let target = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(4.0)), None);
10785        let args = [
10786            ArgumentHandle::new(&target, &ctx),
10787            ArgumentHandle::new(&arr_node, &ctx),
10788        ];
10789        for name in ["RANK.EQ", "RANK.AVG"] {
10790            match ctx
10791                .context
10792                .get_function("", name)
10793                .unwrap()
10794                .dispatch(&args, &ctx.function_context(None))
10795                .unwrap()
10796                .into_literal()
10797            {
10798                LiteralValue::Error(e) => assert!(e.to_string().contains("#N/A")),
10799                other => panic!("expected #N/A {other:?}"),
10800            }
10801        }
10802    }
10803
10804    #[test]
10805    fn mode_mult_ordering() {
10806        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(ModeMultiFn));
10807        let ctx = interp(&wb);
10808        // Two modes with same frequency; ensure ascending ordering in array result
10809        let arr_node = arr(vec![5.0, 2.0, 2.0, 5.0, 3.0, 7.0, 5.0, 2.0]); // 2 and 5 appear 4 times each
10810        let args = [ArgumentHandle::new(&arr_node, &ctx)];
10811        let out = ctx
10812            .context
10813            .get_function("", "MODE.MULT")
10814            .unwrap()
10815            .dispatch(&args, &ctx.function_context(None))
10816            .unwrap()
10817            .into_literal();
10818        match out {
10819            LiteralValue::Array(rows) => {
10820                let vals: Vec<f64> = rows
10821                    .into_iter()
10822                    .map(|r| {
10823                        if let LiteralValue::Number(n) = r[0] {
10824                            n
10825                        } else {
10826                            panic!("expected number")
10827                        }
10828                    })
10829                    .collect();
10830                assert_eq!(vals, vec![2.0, 5.0]);
10831            }
10832            other => panic!("expected array {other:?}"),
10833        }
10834    }
10835
10836    #[test]
10837    fn boolean_and_text_in_range_are_ignored() {
10838        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(StdevPop));
10839        let ctx = interp(&wb);
10840        let mixed = ASTNode::new(
10841            ASTNodeType::Literal(LiteralValue::Array(vec![vec![
10842                LiteralValue::Number(1.0),
10843                LiteralValue::Text("ABC".into()),
10844                LiteralValue::Boolean(true),
10845                LiteralValue::Number(4.0),
10846            ]])),
10847            None,
10848        );
10849        let f = ctx.context.get_function("", "STDEV.P").unwrap();
10850        let out = f
10851            .dispatch(
10852                &[ArgumentHandle::new(&mixed, &ctx)],
10853                &ctx.function_context(None),
10854            )
10855            .unwrap()
10856            .into_literal();
10857        // NOTE: Inline array literal is treated as a direct scalar argument (not a range reference),
10858        // so boolean TRUE is coerced to 1. Dataset becomes {1,1,4}; population stdev = sqrt(6/3)=sqrt(2).
10859        match out {
10860            LiteralValue::Number(v) => {
10861                assert!((v - 2f64.sqrt()).abs() < 1e-12, "expected sqrt(2) got {v}")
10862            }
10863            other => panic!("unexpected {other:?}"),
10864        }
10865    }
10866
10867    #[test]
10868    fn boolean_direct_arg_coerces() {
10869        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(StdevPop));
10870        let ctx = interp(&wb);
10871        let one = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(1.0)), None);
10872        let t = ASTNode::new(ASTNodeType::Literal(LiteralValue::Boolean(true)), None);
10873        let f = ctx.context.get_function("", "STDEV.P").unwrap();
10874        let args = [
10875            ArgumentHandle::new(&one, &ctx),
10876            ArgumentHandle::new(&t, &ctx),
10877        ];
10878        let out = f
10879            .dispatch(&args, &ctx.function_context(None))
10880            .unwrap()
10881            .into_literal();
10882        assert_eq!(out, LiteralValue::Number(0.0));
10883    }
10884}