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 materialize numeric values into a Vec<f64>. Functions that need only one or two order
9//!   statistics (LARGE, SMALL, MEDIAN, PERCENTILE.INC/.EXC, QUARTILE.INC/.EXC) use quickselect
10//!   (`select_nth_unstable_by`) instead of a full sort. Functions that need the complete sorted
11//!   order keep the sort: RANK.EQ/RANK.AVG (positional scan), MODE.SNGL/MODE.MULT (run-length
12//!   over sorted order), TRIMMEAN (f64 summation order over the sorted middle slice must stay
13//!   bit-identical), PERCENTRANK.INC/.EXC (interpolating scan), FREQUENCY (sorted bins).
14//! - Text/boolean coercion nuance: For Excel statistical functions, values coming from range
15//!   references should ignore text and logical values (they are skipped), while direct scalar
16//!   arguments still coerce (e.g. =STDEV(1,TRUE) treats TRUE as 1). This file now implements that
17//!   distinction. TODO(excel-nuance): refine numeric text literal vs non‑numeric text handling.
18//! - Errors encountered in any argument propagate immediately.
19//! - Empty numeric sets produce Excel-specific errors (#NUM! for LARGE/SMALL, #N/A for rank target
20//!   out of range, #DIV/0! for STDEV/VAR sample with n < 2, etc.).
21
22use super::super::builtins::utils::{ARG_RANGE_NUM_LENIENT_ONE, coerce_num};
23use crate::args::ArgSchema;
24use crate::function::Function;
25use crate::function_contract::FunctionDependencyContract;
26use crate::traits::{ArgumentHandle, FunctionContext};
27use formualizer_common::{ExcelError, LiteralValue};
28// use std::collections::BTreeMap; // removed unused import
29use formualizer_macros::func_caps;
30
31fn scalar_like_value(arg: &ArgumentHandle<'_, '_>) -> Result<LiteralValue, ExcelError> {
32    Ok(match arg.value()? {
33        crate::traits::CalcValue::Scalar(v) => v,
34        crate::traits::CalcValue::Range(rv) => rv.get_cell(0, 0),
35        crate::traits::CalcValue::Callable(_) => LiteralValue::Error(
36            ExcelError::new(formualizer_common::ExcelErrorKind::Calc)
37                .with_message("LAMBDA value must be invoked"),
38        ),
39    })
40}
41
42/// Collect numeric inputs applying Excel statistical semantics:
43/// - Range references: include only numeric cells; skip text, logical, blank. Errors propagate.
44/// - Direct scalar arguments: attempt numeric coercion (so TRUE/FALSE, numeric text are included if
45///   coerce_num succeeds). Non-numeric text is ignored (Excel would treat a direct non-numeric text
46///   argument as #VALUE! in some contexts; covered by TODO for finer parity).
47fn collect_numeric_stats(args: &[ArgumentHandle]) -> Result<Vec<f64>, ExcelError> {
48    let mut out = Vec::new();
49    for a in args {
50        // Special-case: inline array literal argument should be treated like a list of direct scalar
51        // arguments (not a by-ref range). This allows boolean/text coercion per element akin to
52        // passing multiple scalars to the function.
53        if let Some(arr) = a.inline_array_literal()? {
54            for row in arr.into_iter() {
55                for cell in row.into_iter() {
56                    match cell {
57                        LiteralValue::Error(e) => return Err(e),
58                        other => {
59                            if let Ok(n) = coerce_num(&other) {
60                                out.push(n);
61                            }
62                        }
63                    }
64                }
65            }
66            continue;
67        }
68
69        if let Ok(view) = a.range_view() {
70            view.for_each_cell(&mut |v| {
71                match v {
72                    LiteralValue::Error(e) => return Err(e.clone()),
73                    LiteralValue::Number(n) => out.push(*n),
74                    LiteralValue::Int(i) => out.push(*i as f64),
75                    _ => {}
76                }
77                Ok(())
78            })?;
79        } else {
80            let v = scalar_like_value(a)?;
81            match v {
82                LiteralValue::Error(e) => return Err(e),
83                other => {
84                    if let Ok(n) = coerce_num(&other) {
85                        out.push(n);
86                    }
87                }
88            }
89        }
90    }
91    Ok(out)
92}
93
94/* ─────────────── order-statistic selection (quickselect) ───────────────
95 *
96 * LARGE/SMALL/MEDIAN and the PERCENTILE/QUARTILE family need at most two
97 * adjacent order statistics, so a full O(n log n) sort is wasted work;
98 * `select_nth_unstable_by` (quickselect) finds them in expected O(n).
99 *
100 * The comparator is byte-for-byte the one used by the full sorts it
101 * replaces (`partial_cmp().unwrap()`): `collect_numeric_stats` only yields
102 * coerced finite numerics, and a NaN would have panicked the old sort the
103 * same way. Ties are exact duplicates for f64s compared `Equal` (modulo
104 * the ±0.0 sign bit), so the selected element is bit-identical to the
105 * sorted element at the same index.
106 */
107
108/// k-th order statistic (0-based, ascending). Reorders `nums` in place.
109fn nth_smallest(nums: &mut [f64], k: usize) -> f64 {
110    let (_, kth, _) = nums.select_nth_unstable_by(k, |a, b| a.partial_cmp(b).unwrap());
111    *kth
112}
113
114/// The adjacent order statistics (k, k+1) ascending: one quickselect for k,
115/// then the (k+1)-th is the minimum of the right partition.
116/// Requires `k + 1 < nums.len()`.
117fn adjacent_smallest(nums: &mut [f64], k: usize) -> (f64, f64) {
118    let (_, kth, right) = nums.select_nth_unstable_by(k, |a, b| a.partial_cmp(b).unwrap());
119    let kth = *kth;
120    let mut next = right[0];
121    for &v in &right[1..] {
122        if v < next {
123            next = v;
124        }
125    }
126    (kth, next)
127}
128
129/// PERCENTILE.INC over unsorted data (reorders `nums`): rank = p*(n-1) on
130/// the ascending order needs at most the two adjacent order statistics
131/// around the rank. The interpolation formula is unchanged from the old
132/// full-sort implementation.
133fn percentile_inc(nums: &mut [f64], p: f64) -> Result<f64, ExcelError> {
134    if nums.is_empty() {
135        return Err(ExcelError::new_num());
136    }
137    if !(0.0..=1.0).contains(&p) {
138        return Err(ExcelError::new_num());
139    }
140    if nums.len() == 1 {
141        return Ok(nums[0]);
142    }
143    let n = nums.len() as f64;
144    let rank = p * (n - 1.0); // 0-based rank
145    let lo = rank.floor() as usize;
146    let hi = rank.ceil() as usize;
147    if lo == hi {
148        return Ok(nth_smallest(nums, lo));
149    }
150    // hi == lo + 1 whenever rank is fractional.
151    let frac = rank - (lo as f64);
152    let (lo_v, hi_v) = adjacent_smallest(nums, lo);
153    Ok(lo_v + (hi_v - lo_v) * frac)
154}
155
156/// PERCENTILE.EXC over unsorted data (reorders `nums`); (n+1) rank basis,
157/// invalid when rank < 1 or > n. Same selection strategy as
158/// [`percentile_inc`]; interpolation formula unchanged.
159fn percentile_exc(nums: &mut [f64], p: f64) -> Result<f64, ExcelError> {
160    if nums.is_empty() {
161        return Err(ExcelError::new_num());
162    }
163    if !(0.0..=1.0).contains(&p) || p <= 0.0 || p >= 1.0 {
164        return Err(ExcelError::new_num());
165    }
166    let n = nums.len() as f64;
167    let rank = p * (n + 1.0); // 1..n domain
168    if rank < 1.0 || rank > n {
169        return Err(ExcelError::new_num());
170    }
171    let lo = rank.floor();
172    let hi = rank.ceil();
173    if (lo - hi).abs() < f64::EPSILON {
174        return Ok(nth_smallest(nums, (lo as usize) - 1));
175    }
176    // hi_idx == lo_idx + 1 whenever rank is fractional.
177    let frac = rank - lo;
178    let lo_idx = (lo as usize) - 1;
179    let (lo_v, hi_v) = adjacent_smallest(nums, lo_idx);
180    Ok(lo_v + (hi_v - lo_v) * frac)
181}
182
183/// Returns the rank position of a number within a data set, with ties sharing the same rank.
184///
185/// `RANK.EQ` defaults to descending order (largest value is rank 1), and can switch to ascending
186/// order when `order` is non-zero.
187///
188/// # Remarks
189/// - Omitting `order`, or setting `order` to `0`, ranks values in descending order.
190/// - Any non-zero `order` ranks values in ascending order.
191/// - Tied values receive the same rank (the first matching position in the sorted list).
192/// - Returns `#N/A` if `number` is not found in `ref`.
193///
194/// # Examples
195///
196/// ```yaml,sandbox
197/// title: "Descending rank with direct values"
198/// formula: "=RANK.EQ(7,{10,7,4,2})"
199/// expected: 2
200/// ```
201///
202/// ```yaml,sandbox
203/// title: "Ascending rank with ties in a range"
204/// grid:
205///   A1: 50
206///   A2: 20
207///   A3: 20
208///   A4: 10
209///   A5: 5
210/// formula: "=RANK.EQ(A2,A1:A5,1)"
211/// expected: 3
212/// ```
213///
214/// ```yaml,docs
215/// related:
216///   - RANK.AVG
217///   - LARGE
218///   - SMALL
219/// faq:
220///   - q: "When does RANK.EQ return #N/A?"
221///     a: "It returns #N/A when the target number does not appear in the reference set."
222/// ```
223#[derive(Debug)]
224pub struct RankEqFn;
225/// [formualizer-docgen:schema:start]
226/// Name: RANK.EQ
227/// Type: RankEqFn
228/// Min args: 2
229/// Max args: variadic
230/// Variadic: true
231/// Signature: RANK.EQ(arg1...: number@range)
232/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
233/// Caps: PURE, NUMERIC_ONLY
234/// [formualizer-docgen:schema:end]
235impl Function for RankEqFn {
236    func_caps!(PURE, NUMERIC_ONLY);
237    fn name(&self) -> &'static str {
238        "RANK.EQ"
239    }
240    fn aliases(&self) -> &'static [&'static str] {
241        &["RANK"]
242    }
243    fn min_args(&self) -> usize {
244        2
245    }
246    fn variadic(&self) -> bool {
247        true
248    } // allow optional order
249    fn arg_schema(&self) -> &'static [ArgSchema] {
250        &ARG_RANGE_NUM_LENIENT_ONE[..]
251    }
252    fn eval<'a, 'b, 'c>(
253        &self,
254        args: &'c [ArgumentHandle<'a, 'b>],
255        _ctx: &dyn FunctionContext<'b>,
256    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
257        if args.len() < 2 {
258            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
259                ExcelError::new_na(),
260            )));
261        }
262        let target = match coerce_num(&args[0].value()?.into_literal()) {
263            Ok(n) => n,
264            Err(_) => {
265                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
266                    ExcelError::new_na(),
267                )));
268            }
269        };
270        // optional order arg at end if 3 args
271        let order = if args.len() >= 3 {
272            coerce_num(&args[2].value()?.into_literal()).unwrap_or(0.0)
273        } else {
274            0.0
275        };
276        let nums = collect_numeric_stats(&args[1..2])?; // only one ref range per Excel spec
277        if nums.is_empty() {
278            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
279                ExcelError::new_na(),
280            )));
281        }
282        let mut sorted = nums; // copy
283        if order.abs() < 1e-12 {
284            // descending
285            sorted.sort_by(|a, b| b.partial_cmp(a).unwrap());
286        } else {
287            // ascending
288            sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
289        }
290        for (i, &v) in sorted.iter().enumerate() {
291            if (v - target).abs() < 1e-12 {
292                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
293                    (i + 1) as f64,
294                )));
295            }
296        }
297        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
298            ExcelError::new_na(),
299        )))
300    }
301}
302
303/// Returns the rank position of a number, averaging the rank positions for ties.
304///
305/// Use `RANK.AVG` when tied values should share the average of their occupied rank positions.
306///
307/// # Remarks
308/// - Omitting `order`, or setting `order` to `0`, ranks values in descending order.
309/// - Any non-zero `order` ranks values in ascending order.
310/// - If `number` appears multiple times, the function returns the mean of those rank positions.
311/// - Returns `#N/A` if `number` is not found in `ref`.
312///
313/// # Examples
314///
315/// ```yaml,sandbox
316/// title: "Average rank for tied values"
317/// formula: "=RANK.AVG(20,{30,20,20,10})"
318/// expected: 2.5
319/// ```
320///
321/// ```yaml,sandbox
322/// title: "Ascending average rank from a range"
323/// grid:
324///   A1: 50
325///   A2: 20
326///   A3: 20
327///   A4: 10
328///   A5: 5
329/// formula: "=RANK.AVG(A2,A1:A5,1)"
330/// expected: 3.5
331/// ```
332///
333/// ```yaml,docs
334/// related:
335///   - RANK.EQ
336///   - LARGE
337///   - SMALL
338/// faq:
339///   - q: "How are ties handled by RANK.AVG?"
340///     a: "All tied occurrences share the average of their rank positions."
341/// ```
342#[derive(Debug)]
343pub struct RankAvgFn;
344/// [formualizer-docgen:schema:start]
345/// Name: RANK.AVG
346/// Type: RankAvgFn
347/// Min args: 2
348/// Max args: variadic
349/// Variadic: true
350/// Signature: RANK.AVG(arg1...: number@range)
351/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
352/// Caps: PURE, NUMERIC_ONLY
353/// [formualizer-docgen:schema:end]
354impl Function for RankAvgFn {
355    func_caps!(PURE, NUMERIC_ONLY);
356    fn name(&self) -> &'static str {
357        "RANK.AVG"
358    }
359    fn min_args(&self) -> usize {
360        2
361    }
362    fn variadic(&self) -> bool {
363        true
364    }
365    fn arg_schema(&self) -> &'static [ArgSchema] {
366        &ARG_RANGE_NUM_LENIENT_ONE[..]
367    }
368    fn eval<'a, 'b, 'c>(
369        &self,
370        args: &'c [ArgumentHandle<'a, 'b>],
371        _ctx: &dyn FunctionContext<'b>,
372    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
373        if args.len() < 2 {
374            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
375                ExcelError::new_na(),
376            )));
377        }
378        let t0 = scalar_like_value(&args[0])?;
379        let target = match coerce_num(&t0) {
380            Ok(n) => n,
381            Err(_) => {
382                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
383                    ExcelError::new_na(),
384                )));
385            }
386        };
387        let order = if args.len() >= 3 {
388            let ord = scalar_like_value(&args[2])?;
389            coerce_num(&ord).unwrap_or(0.0)
390        } else {
391            0.0
392        };
393        let nums = collect_numeric_stats(&args[1..2])?;
394        if nums.is_empty() {
395            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
396                ExcelError::new_na(),
397            )));
398        }
399        let mut sorted = nums;
400        if order.abs() < 1e-12 {
401            sorted.sort_by(|a, b| b.partial_cmp(a).unwrap());
402        } else {
403            sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
404        }
405        let mut positions = Vec::new();
406        for (i, &v) in sorted.iter().enumerate() {
407            if (v - target).abs() < 1e-12 {
408                positions.push(i + 1);
409            }
410        }
411        if positions.is_empty() {
412            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
413                ExcelError::new_na(),
414            )));
415        }
416        let avg = positions.iter().copied().sum::<usize>() as f64 / positions.len() as f64;
417        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(avg)))
418    }
419}
420
421/// Returns the k-th largest value in a data set.
422///
423/// `LARGE` is useful for top-N analysis, such as highest score, second-highest sale, or third-best
424/// result.
425///
426/// # Remarks
427/// - `k` must be at least `1`.
428/// - Returns `#NUM!` if `k` is greater than the count of numeric values.
429/// - Non-numeric values in referenced ranges are ignored.
430///
431/// # Examples
432///
433/// ```yaml,sandbox
434/// title: "Second-largest from direct values"
435/// formula: "=LARGE({4,9,1,7},2)"
436/// expected: 7
437/// ```
438///
439/// ```yaml,sandbox
440/// title: "Third-largest from a range"
441/// grid:
442///   A1: 3
443///   A2: 12
444///   A3: 8
445///   A4: 5
446/// formula: "=LARGE(A1:A4,3)"
447/// expected: 5
448/// ```
449///
450/// ```yaml,docs
451/// related:
452///   - SMALL
453///   - MAX
454///   - RANK.EQ
455/// faq:
456///   - q: "When does LARGE return #NUM!?"
457///     a: "It returns #NUM! when k < 1, k exceeds numeric count, or no numeric values exist."
458/// ```
459#[derive(Debug)]
460pub struct LARGE;
461/// [formualizer-docgen:schema:start]
462/// Name: LARGE
463/// Type: LARGE
464/// Min args: 2
465/// Max args: variadic
466/// Variadic: true
467/// Signature: LARGE(arg1...: number@range)
468/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
469/// Caps: PURE, REDUCTION, NUMERIC_ONLY
470/// [formualizer-docgen:schema:end]
471impl Function for LARGE {
472    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
473    fn name(&self) -> &'static str {
474        "LARGE"
475    }
476    fn min_args(&self) -> usize {
477        2
478    }
479    fn variadic(&self) -> bool {
480        true
481    }
482    fn arg_schema(&self) -> &'static [ArgSchema] {
483        &ARG_RANGE_NUM_LENIENT_ONE[..]
484    }
485    fn eval<'a, 'b, 'c>(
486        &self,
487        args: &'c [ArgumentHandle<'a, 'b>],
488        _ctx: &dyn FunctionContext<'b>,
489    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
490        if args.len() < 2 {
491            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
492                ExcelError::new_num(),
493            )));
494        }
495        let k = match coerce_num(&args.last().unwrap().value()?.into_literal()) {
496            Ok(n) => n,
497            Err(_) => {
498                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
499                    ExcelError::new_num(),
500                )));
501            }
502        };
503        let k = k as i64;
504        if k < 1 {
505            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
506                ExcelError::new_num(),
507            )));
508        }
509        let mut nums = collect_numeric_stats(&args[..args.len() - 1])?;
510        if nums.is_empty() || k as usize > nums.len() {
511            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
512                ExcelError::new_num(),
513            )));
514        }
515        // k-th largest == (n-k)-th smallest: quickselect instead of full sort.
516        let idx = nums.len() - k as usize;
517        let v = nth_smallest(&mut nums, idx);
518        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(v)))
519    }
520}
521
522/// Returns the k-th smallest value in a data set.
523///
524/// `SMALL` is often used to find low outliers, minimum thresholds, or bottom-N values.
525///
526/// # Remarks
527/// - `k` must be at least `1`.
528/// - Returns `#NUM!` if `k` is greater than the count of numeric values.
529/// - Non-numeric values in referenced ranges are ignored.
530///
531/// # Examples
532///
533/// ```yaml,sandbox
534/// title: "Second-smallest from direct values"
535/// formula: "=SMALL({4,9,1,7},2)"
536/// expected: 4
537/// ```
538///
539/// ```yaml,sandbox
540/// title: "Third-smallest from a range"
541/// grid:
542///   A1: 3
543///   A2: 12
544///   A3: 8
545///   A4: 5
546/// formula: "=SMALL(A1:A4,3)"
547/// expected: 8
548/// ```
549///
550/// ```yaml,docs
551/// related:
552///   - LARGE
553///   - MIN
554///   - RANK.EQ
555/// faq:
556///   - q: "Does SMALL include text in referenced ranges?"
557///     a: "No. Non-numeric range values are ignored when selecting the k-th smallest value."
558/// ```
559#[derive(Debug)]
560pub struct SMALL;
561/// [formualizer-docgen:schema:start]
562/// Name: SMALL
563/// Type: SMALL
564/// Min args: 2
565/// Max args: variadic
566/// Variadic: true
567/// Signature: SMALL(arg1...: number@range)
568/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
569/// Caps: PURE, REDUCTION, NUMERIC_ONLY
570/// [formualizer-docgen:schema:end]
571impl Function for SMALL {
572    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
573    fn name(&self) -> &'static str {
574        "SMALL"
575    }
576    fn min_args(&self) -> usize {
577        2
578    }
579    fn variadic(&self) -> bool {
580        true
581    }
582    fn arg_schema(&self) -> &'static [ArgSchema] {
583        &ARG_RANGE_NUM_LENIENT_ONE[..]
584    }
585    fn eval<'a, 'b, 'c>(
586        &self,
587        args: &'c [ArgumentHandle<'a, 'b>],
588        _ctx: &dyn FunctionContext<'b>,
589    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
590        if args.len() < 2 {
591            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
592                ExcelError::new_num(),
593            )));
594        }
595        let k = match coerce_num(&args.last().unwrap().value()?.into_literal()) {
596            Ok(n) => n,
597            Err(_) => {
598                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
599                    ExcelError::new_num(),
600                )));
601            }
602        };
603        let k = k as i64;
604        if k < 1 {
605            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
606                ExcelError::new_num(),
607            )));
608        }
609        let mut nums = collect_numeric_stats(&args[..args.len() - 1])?;
610        if nums.is_empty() || k as usize > nums.len() {
611            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
612                ExcelError::new_num(),
613            )));
614        }
615        // k-th smallest: quickselect instead of full sort.
616        let v = nth_smallest(&mut nums, k as usize - 1);
617        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(v)))
618    }
619}
620
621/// Returns the middle value of a numeric data set.
622///
623/// For an even number of values, `MEDIAN` returns the average of the two center values.
624///
625/// # Remarks
626/// - Ignores non-numeric values in referenced ranges.
627/// - Returns `#NUM!` when no numeric values are available.
628/// - Supports both scalar arguments and range inputs.
629///
630/// # Examples
631///
632/// ```yaml,sandbox
633/// title: "Median of an odd-sized set"
634/// formula: "=MEDIAN(1,3,8)"
635/// expected: 3
636/// ```
637///
638/// ```yaml,sandbox
639/// title: "Median of an even-sized range"
640/// grid:
641///   A1: 1
642///   A2: 2
643///   A3: 10
644///   A4: 12
645/// formula: "=MEDIAN(A1:A4)"
646/// expected: 6
647/// ```
648///
649/// ```yaml,docs
650/// related:
651///   - AVERAGE
652///   - MODE.SNGL
653///   - QUARTILE.INC
654/// faq:
655///   - q: "When does MEDIAN return #NUM!?"
656///     a: "MEDIAN returns #NUM! when no numeric values are available after filtering/coercion."
657/// ```
658#[derive(Debug)]
659pub struct MEDIAN;
660/// [formualizer-docgen:schema:start]
661/// Name: MEDIAN
662/// Type: MEDIAN
663/// Min args: 1
664/// Max args: variadic
665/// Variadic: true
666/// Signature: MEDIAN(arg1...: number@range)
667/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
668/// Caps: PURE, REDUCTION, NUMERIC_ONLY
669/// [formualizer-docgen:schema:end]
670impl Function for MEDIAN {
671    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
672    fn name(&self) -> &'static str {
673        "MEDIAN"
674    }
675    fn min_args(&self) -> usize {
676        1
677    }
678    fn variadic(&self) -> bool {
679        true
680    }
681    fn arg_schema(&self) -> &'static [ArgSchema] {
682        &ARG_RANGE_NUM_LENIENT_ONE[..]
683    }
684    fn eval<'a, 'b, 'c>(
685        &self,
686        args: &'c [ArgumentHandle<'a, 'b>],
687        _ctx: &dyn FunctionContext<'b>,
688    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
689        let mut nums = collect_numeric_stats(args)?;
690        if nums.is_empty() {
691            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
692                ExcelError::new_num(),
693            )));
694        }
695        // Middle one/two order statistics: quickselect instead of full sort.
696        let n = nums.len();
697        let mid = n / 2;
698        let med = if n % 2 == 1 {
699            nth_smallest(&mut nums, mid)
700        } else {
701            let (lo, hi) = adjacent_smallest(&mut nums, mid - 1);
702            (lo + hi) / 2.0
703        };
704        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(med)))
705    }
706}
707
708/// Estimates sample standard deviation using `n-1` in the denominator.
709///
710/// `STDEV.S` measures spread when your values represent a sample of a larger population.
711///
712/// # Remarks
713/// - Requires at least two numeric values.
714/// - Returns `#DIV/0!` when fewer than two numeric values are provided.
715/// - Non-numeric values in referenced ranges are ignored.
716///
717/// # Examples
718///
719/// ```yaml,sandbox
720/// title: "Sample standard deviation from scalar arguments"
721/// formula: "=STDEV.S(2,4,6)"
722/// expected: 2
723/// ```
724///
725/// ```yaml,sandbox
726/// title: "Sample standard deviation from a range"
727/// grid:
728///   A1: 5
729///   A2: 7
730///   A3: 9
731/// formula: "=STDEV.S(A1:A3)"
732/// expected: 2
733/// ```
734///
735/// ```yaml,docs
736/// related:
737///   - STDEV.P
738///   - VAR.S
739///   - VAR.P
740/// faq:
741///   - q: "Why does STDEV.S return #DIV/0!?"
742///     a: "Sample standard deviation needs at least two numeric values."
743/// ```
744#[derive(Debug)]
745pub struct StdevSample; // sample
746/// [formualizer-docgen:schema:start]
747/// Name: STDEV.S
748/// Type: StdevSample
749/// Min args: 1
750/// Max args: variadic
751/// Variadic: true
752/// Signature: STDEV.S(arg1...: number@range)
753/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
754/// Caps: PURE, REDUCTION, NUMERIC_ONLY, STREAM_OK
755/// [formualizer-docgen:schema:end]
756impl Function for StdevSample {
757    func_caps!(PURE, NUMERIC_ONLY, REDUCTION, STREAM_OK);
758    fn name(&self) -> &'static str {
759        "STDEV.S"
760    }
761    fn aliases(&self) -> &'static [&'static str] {
762        &["STDEV"]
763    }
764    fn min_args(&self) -> usize {
765        1
766    }
767    fn variadic(&self) -> bool {
768        true
769    }
770    fn arg_schema(&self) -> &'static [ArgSchema] {
771        &ARG_RANGE_NUM_LENIENT_ONE[..]
772    }
773    fn eval<'a, 'b, 'c>(
774        &self,
775        args: &'c [ArgumentHandle<'a, 'b>],
776        _ctx: &dyn FunctionContext<'b>,
777    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
778        let nums = collect_numeric_stats(args)?;
779        let n = nums.len();
780        if n < 2 {
781            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
782                ExcelError::from_error_string("#DIV/0!"),
783            )));
784        }
785        let mean = nums.iter().sum::<f64>() / (n as f64);
786        let mut ss = 0.0;
787        for &v in &nums {
788            let d = v - mean;
789            ss += d * d;
790        }
791        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
792            (ss / ((n - 1) as f64)).sqrt(),
793        )))
794    }
795}
796
797/// Returns population standard deviation using `n` in the denominator.
798///
799/// Use `STDEV.P` when your values represent the entire population, not a sample.
800///
801/// # Remarks
802/// - Requires at least one numeric value.
803/// - Returns `#DIV/0!` when no numeric values are provided.
804/// - Non-numeric values in referenced ranges are ignored.
805///
806/// # Examples
807///
808/// ```yaml,sandbox
809/// title: "Population standard deviation from scalar arguments"
810/// formula: "=STDEV.P(2,4,6)"
811/// expected: 1.632993161855452
812/// ```
813///
814/// ```yaml,sandbox
815/// title: "Population standard deviation from a range"
816/// grid:
817///   A1: 1
818///   A2: 2
819///   A3: 3
820/// formula: "=STDEV.P(A1:A3)"
821/// expected: 0.816496580927726
822/// ```
823///
824/// ```yaml,docs
825/// related:
826///   - STDEV.S
827///   - VAR.P
828///   - VAR.S
829/// faq:
830///   - q: "When does STDEV.P return #DIV/0!?"
831///     a: "It returns #DIV/0! when no numeric values are provided."
832/// ```
833#[derive(Debug)]
834pub struct StdevPop; // population
835/// [formualizer-docgen:schema:start]
836/// Name: STDEV.P
837/// Type: StdevPop
838/// Min args: 1
839/// Max args: variadic
840/// Variadic: true
841/// Signature: STDEV.P(arg1...: number@range)
842/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
843/// Caps: PURE, REDUCTION, NUMERIC_ONLY, STREAM_OK
844/// [formualizer-docgen:schema:end]
845impl Function for StdevPop {
846    func_caps!(PURE, NUMERIC_ONLY, REDUCTION, STREAM_OK);
847    fn name(&self) -> &'static str {
848        "STDEV.P"
849    }
850    fn aliases(&self) -> &'static [&'static str] {
851        &["STDEVP"]
852    }
853    fn min_args(&self) -> usize {
854        1
855    }
856    fn variadic(&self) -> bool {
857        true
858    }
859    fn arg_schema(&self) -> &'static [ArgSchema] {
860        &ARG_RANGE_NUM_LENIENT_ONE[..]
861    }
862    fn eval<'a, 'b, 'c>(
863        &self,
864        args: &'c [ArgumentHandle<'a, 'b>],
865        _ctx: &dyn FunctionContext<'b>,
866    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
867        let nums = collect_numeric_stats(args)?;
868        let n = nums.len();
869        if n == 0 {
870            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
871                ExcelError::from_error_string("#DIV/0!"),
872            )));
873        }
874        let mean = nums.iter().sum::<f64>() / (n as f64);
875        let mut ss = 0.0;
876        for &v in &nums {
877            let d = v - mean;
878            ss += d * d;
879        }
880        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
881            (ss / (n as f64)).sqrt(),
882        )))
883    }
884}
885
886/// Estimates sample variance using `n-1` in the denominator.
887///
888/// `VAR.S` is the squared counterpart of `STDEV.S` for sample-based variability.
889///
890/// # Remarks
891/// - Requires at least two numeric values.
892/// - Returns `#DIV/0!` when fewer than two numeric values are provided.
893/// - Non-numeric values in referenced ranges are ignored.
894///
895/// # Examples
896///
897/// ```yaml,sandbox
898/// title: "Sample variance from scalar arguments"
899/// formula: "=VAR.S(2,4,6)"
900/// expected: 4
901/// ```
902///
903/// ```yaml,sandbox
904/// title: "Sample variance from a range"
905/// grid:
906///   A1: 1
907///   A2: 2
908///   A3: 3
909/// formula: "=VAR.S(A1:A3)"
910/// expected: 1
911/// ```
912///
913/// ```yaml,docs
914/// related:
915///   - VAR.P
916///   - STDEV.S
917///   - STDEV.P
918/// faq:
919///   - q: "Why does VAR.S return #DIV/0!?"
920///     a: "Sample variance requires at least two numeric observations."
921/// ```
922#[derive(Debug)]
923pub struct VarSample; // sample variance
924/// [formualizer-docgen:schema:start]
925/// Name: VAR.S
926/// Type: VarSample
927/// Min args: 1
928/// Max args: variadic
929/// Variadic: true
930/// Signature: VAR.S(arg1...: number@range)
931/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
932/// Caps: PURE, REDUCTION, NUMERIC_ONLY, STREAM_OK
933/// [formualizer-docgen:schema:end]
934impl Function for VarSample {
935    func_caps!(PURE, NUMERIC_ONLY, REDUCTION, STREAM_OK);
936    fn name(&self) -> &'static str {
937        "VAR.S"
938    }
939    fn aliases(&self) -> &'static [&'static str] {
940        &["VAR"]
941    }
942    fn min_args(&self) -> usize {
943        1
944    }
945    fn variadic(&self) -> bool {
946        true
947    }
948    fn arg_schema(&self) -> &'static [ArgSchema] {
949        &ARG_RANGE_NUM_LENIENT_ONE[..]
950    }
951    fn eval<'a, 'b, 'c>(
952        &self,
953        args: &'c [ArgumentHandle<'a, 'b>],
954        _ctx: &dyn FunctionContext<'b>,
955    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
956        let nums = collect_numeric_stats(args)?;
957        let n = nums.len();
958        if n < 2 {
959            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
960                ExcelError::from_error_string("#DIV/0!"),
961            )));
962        }
963        let mean = nums.iter().sum::<f64>() / (n as f64);
964        let mut ss = 0.0;
965        for &v in &nums {
966            let d = v - mean;
967            ss += d * d;
968        }
969        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
970            ss / ((n - 1) as f64),
971        )))
972    }
973}
974
975/// Returns population variance using `n` in the denominator.
976///
977/// `VAR.P` describes dispersion for a complete population of numeric values.
978///
979/// # Remarks
980/// - Requires at least one numeric value.
981/// - Returns `#DIV/0!` when no numeric values are provided.
982/// - Non-numeric values in referenced ranges are ignored.
983///
984/// # Examples
985///
986/// ```yaml,sandbox
987/// title: "Population variance from scalar arguments"
988/// formula: "=VAR.P(2,4,6)"
989/// expected: 2.6666666666666665
990/// ```
991///
992/// ```yaml,sandbox
993/// title: "Population variance from a range"
994/// grid:
995///   A1: 1
996///   A2: 2
997///   A3: 3
998/// formula: "=VAR.P(A1:A3)"
999/// expected: 0.6666666666666666
1000/// ```
1001///
1002/// ```yaml,docs
1003/// related:
1004///   - VAR.S
1005///   - STDEV.P
1006///   - STDEV.S
1007/// faq:
1008///   - q: "What is the denominator difference vs VAR.S?"
1009///     a: "VAR.P divides by n, while VAR.S divides by n-1."
1010/// ```
1011#[derive(Debug)]
1012pub struct VarPop; // population variance
1013/// [formualizer-docgen:schema:start]
1014/// Name: VAR.P
1015/// Type: VarPop
1016/// Min args: 1
1017/// Max args: variadic
1018/// Variadic: true
1019/// Signature: VAR.P(arg1...: number@range)
1020/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1021/// Caps: PURE, REDUCTION, NUMERIC_ONLY, STREAM_OK
1022/// [formualizer-docgen:schema:end]
1023impl Function for VarPop {
1024    func_caps!(PURE, NUMERIC_ONLY, REDUCTION, STREAM_OK);
1025    fn name(&self) -> &'static str {
1026        "VAR.P"
1027    }
1028    fn aliases(&self) -> &'static [&'static str] {
1029        &["VARP"]
1030    }
1031    fn min_args(&self) -> usize {
1032        1
1033    }
1034    fn variadic(&self) -> bool {
1035        true
1036    }
1037    fn arg_schema(&self) -> &'static [ArgSchema] {
1038        &ARG_RANGE_NUM_LENIENT_ONE[..]
1039    }
1040    fn eval<'a, 'b, 'c>(
1041        &self,
1042        args: &'c [ArgumentHandle<'a, 'b>],
1043        _ctx: &dyn FunctionContext<'b>,
1044    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1045        let nums = collect_numeric_stats(args)?;
1046        let n = nums.len();
1047        if n == 0 {
1048            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1049                ExcelError::from_error_string("#DIV/0!"),
1050            )));
1051        }
1052        let mean = nums.iter().sum::<f64>() / (n as f64);
1053        let mut ss = 0.0;
1054        for &v in &nums {
1055            let d = v - mean;
1056            ss += d * d;
1057        }
1058        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
1059            ss / (n as f64),
1060        )))
1061    }
1062}
1063
1064// MODE.SNGL (alias MODE) and MODE.MULT
1065/// Returns the most frequently occurring value in a data set.
1066///
1067/// `MODE.SNGL` returns a single mode value and reports `#N/A` if no value repeats.
1068///
1069/// # Remarks
1070/// - Returns the first mode encountered after sorting when frequencies tie.
1071/// - Returns `#N/A` when every numeric value appears only once.
1072/// - Alias `MODE` is supported.
1073///
1074/// # Examples
1075///
1076/// ```yaml,sandbox
1077/// title: "Single mode from scalar arguments"
1078/// formula: "=MODE.SNGL(1,2,2,3)"
1079/// expected: 2
1080/// ```
1081///
1082/// ```yaml,sandbox
1083/// title: "Single mode from a range"
1084/// grid:
1085///   A1: 4
1086///   A2: 4
1087///   A3: 6
1088///   A4: 6
1089///   A5: 6
1090/// formula: "=MODE.SNGL(A1:A5)"
1091/// expected: 6
1092/// ```
1093///
1094/// ```yaml,docs
1095/// related:
1096///   - MODE.MULT
1097///   - MEDIAN
1098///   - AVERAGE
1099/// faq:
1100///   - q: "When does MODE.SNGL return #N/A?"
1101///     a: "It returns #N/A when no value repeats in the numeric dataset."
1102/// ```
1103#[derive(Debug)]
1104pub struct ModeSingleFn;
1105/// [formualizer-docgen:schema:start]
1106/// Name: MODE.SNGL
1107/// Type: ModeSingleFn
1108/// Min args: 1
1109/// Max args: variadic
1110/// Variadic: true
1111/// Signature: MODE.SNGL(arg1...: number@range)
1112/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1113/// Caps: PURE, REDUCTION, NUMERIC_ONLY
1114/// [formualizer-docgen:schema:end]
1115impl Function for ModeSingleFn {
1116    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
1117    fn name(&self) -> &'static str {
1118        "MODE.SNGL"
1119    }
1120    fn aliases(&self) -> &'static [&'static str] {
1121        &["MODE"]
1122    }
1123    fn min_args(&self) -> usize {
1124        1
1125    }
1126    fn variadic(&self) -> bool {
1127        true
1128    }
1129    fn arg_schema(&self) -> &'static [ArgSchema] {
1130        &ARG_RANGE_NUM_LENIENT_ONE[..]
1131    }
1132    fn eval<'a, 'b, 'c>(
1133        &self,
1134        args: &'c [ArgumentHandle<'a, 'b>],
1135        _ctx: &dyn FunctionContext<'b>,
1136    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1137        let mut nums = collect_numeric_stats(args)?;
1138        if nums.is_empty() {
1139            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1140                ExcelError::new_na(),
1141            )));
1142        }
1143        nums.sort_by(|a, b| a.partial_cmp(b).unwrap());
1144        let mut best_val = nums[0];
1145        let mut best_cnt = 1usize;
1146        let mut cur_val = nums[0];
1147        let mut cur_cnt = 1usize;
1148        for &v in &nums[1..] {
1149            if (v - cur_val).abs() < 1e-12 {
1150                cur_cnt += 1;
1151            } else {
1152                if cur_cnt > best_cnt {
1153                    best_cnt = cur_cnt;
1154                    best_val = cur_val;
1155                }
1156                cur_val = v;
1157                cur_cnt = 1;
1158            }
1159        }
1160        if cur_cnt > best_cnt {
1161            best_cnt = cur_cnt;
1162            best_val = cur_val;
1163        }
1164        if best_cnt <= 1 {
1165            Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1166                ExcelError::new_na(),
1167            )))
1168        } else {
1169            Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
1170                best_val,
1171            )))
1172        }
1173    }
1174}
1175
1176/// Returns all modal values as a vertical array.
1177///
1178/// Use `MODE.MULT` when a data set can have multiple values with the same highest frequency.
1179///
1180/// # Remarks
1181/// - Returns each tied mode as a separate row in the result array.
1182/// - Returns `#N/A` when every numeric value appears only once.
1183/// - Non-numeric values in referenced ranges are ignored.
1184///
1185/// # Examples
1186///
1187/// ```yaml,sandbox
1188/// title: "Multiple modes from direct values"
1189/// formula: "=MODE.MULT({1,2,2,3,3,4})"
1190/// expected:
1191///   - [2]
1192///   - [3]
1193/// ```
1194///
1195/// ```yaml,sandbox
1196/// title: "Single repeated mode still returns an array"
1197/// grid:
1198///   A1: 5
1199///   A2: 5
1200///   A3: 2
1201///   A4: 1
1202/// formula: "=MODE.MULT(A1:A4)"
1203/// expected:
1204///   - [5]
1205/// ```
1206///
1207/// ```yaml,docs
1208/// related:
1209///   - MODE.SNGL
1210///   - FREQUENCY
1211///   - MEDIAN
1212/// faq:
1213///   - q: "Why can MODE.MULT return an array result?"
1214///     a: "It emits every value tied for highest frequency as separate rows."
1215/// ```
1216#[derive(Debug)]
1217pub struct ModeMultiFn;
1218/// [formualizer-docgen:schema:start]
1219/// Name: MODE.MULT
1220/// Type: ModeMultiFn
1221/// Min args: 1
1222/// Max args: variadic
1223/// Variadic: true
1224/// Signature: MODE.MULT(arg1...: number@range)
1225/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1226/// Caps: PURE, REDUCTION, NUMERIC_ONLY
1227/// [formualizer-docgen:schema:end]
1228impl Function for ModeMultiFn {
1229    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
1230    fn name(&self) -> &'static str {
1231        "MODE.MULT"
1232    }
1233    fn min_args(&self) -> usize {
1234        1
1235    }
1236    fn variadic(&self) -> bool {
1237        true
1238    }
1239    fn arg_schema(&self) -> &'static [ArgSchema] {
1240        &ARG_RANGE_NUM_LENIENT_ONE[..]
1241    }
1242    fn eval<'a, 'b, 'c>(
1243        &self,
1244        args: &'c [ArgumentHandle<'a, 'b>],
1245        _ctx: &dyn FunctionContext<'b>,
1246    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1247        let mut nums = collect_numeric_stats(args)?;
1248        if nums.is_empty() {
1249            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1250                ExcelError::new_na(),
1251            )));
1252        }
1253        nums.sort_by(|a, b| a.partial_cmp(b).unwrap());
1254        let mut runs: Vec<(f64, usize)> = Vec::new();
1255        let mut cur_val = nums[0];
1256        let mut cur_cnt = 1usize;
1257        for &v in &nums[1..] {
1258            if (v - cur_val).abs() < 1e-12 {
1259                cur_cnt += 1;
1260            } else {
1261                runs.push((cur_val, cur_cnt));
1262                cur_val = v;
1263                cur_cnt = 1;
1264            }
1265        }
1266        runs.push((cur_val, cur_cnt));
1267        let max_freq = runs.iter().map(|r| r.1).max().unwrap_or(0);
1268        if max_freq <= 1 {
1269            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1270                ExcelError::new_na(),
1271            )));
1272        }
1273        let rows: Vec<Vec<LiteralValue>> = runs
1274            .into_iter()
1275            .filter(|&(_, c)| c == max_freq)
1276            .map(|(v, _)| vec![LiteralValue::Number(v)])
1277            .collect();
1278        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Array(rows)))
1279    }
1280}
1281
1282/// Returns the k-th percentile of a data set using inclusive interpolation.
1283///
1284/// `PERCENTILE.INC` accepts percentile values from `0` through `1` and interpolates between
1285/// sorted values as needed.
1286///
1287/// # Remarks
1288/// - `k` must be in the inclusive range `[0, 1]`.
1289/// - Returns `#NUM!` for empty numeric input or invalid percentile arguments.
1290/// - Alias `PERCENTILE` is supported.
1291///
1292/// # Examples
1293///
1294/// ```yaml,sandbox
1295/// title: "Inclusive 25th percentile from direct values"
1296/// formula: "=PERCENTILE.INC({1,2,3,4,5},0.25)"
1297/// expected: 2
1298/// ```
1299///
1300/// ```yaml,sandbox
1301/// title: "Inclusive median-style interpolation from a range"
1302/// grid:
1303///   A1: 10
1304///   A2: 20
1305///   A3: 30
1306///   A4: 40
1307/// formula: "=PERCENTILE.INC(A1:A4,0.5)"
1308/// expected: 25
1309/// ```
1310///
1311/// ```yaml,docs
1312/// related:
1313///   - PERCENTILE.EXC
1314///   - QUARTILE.INC
1315///   - PERCENTRANK.INC
1316/// faq:
1317///   - q: "What k range is valid for PERCENTILE.INC?"
1318///     a: "k must be between 0 and 1 inclusive; outside that range returns #NUM!."
1319/// ```
1320#[derive(Debug)]
1321pub struct PercentileInc; // inclusive
1322/// [formualizer-docgen:schema:start]
1323/// Name: PERCENTILE.INC
1324/// Type: PercentileInc
1325/// Min args: 2
1326/// Max args: variadic
1327/// Variadic: true
1328/// Signature: PERCENTILE.INC(arg1...: number@range)
1329/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1330/// Caps: PURE, NUMERIC_ONLY
1331/// [formualizer-docgen:schema:end]
1332impl Function for PercentileInc {
1333    func_caps!(PURE, NUMERIC_ONLY);
1334    fn name(&self) -> &'static str {
1335        "PERCENTILE.INC"
1336    }
1337    fn aliases(&self) -> &'static [&'static str] {
1338        &["PERCENTILE"]
1339    }
1340    fn min_args(&self) -> usize {
1341        2
1342    }
1343    fn variadic(&self) -> bool {
1344        true
1345    }
1346    fn arg_schema(&self) -> &'static [ArgSchema] {
1347        &ARG_RANGE_NUM_LENIENT_ONE[..]
1348    }
1349    fn eval<'a, 'b, 'c>(
1350        &self,
1351        args: &'c [ArgumentHandle<'a, 'b>],
1352        _ctx: &dyn FunctionContext<'b>,
1353    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1354        if args.len() < 2 {
1355            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1356                ExcelError::new_num(),
1357            )));
1358        }
1359        let pv = scalar_like_value(args.last().unwrap())?;
1360        let p = match coerce_num(&pv) {
1361            Ok(n) => n,
1362            Err(_) => {
1363                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1364                    ExcelError::new_num(),
1365                )));
1366            }
1367        };
1368        let mut nums = collect_numeric_stats(&args[..args.len() - 1])?;
1369        if nums.is_empty() {
1370            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1371                ExcelError::new_num(),
1372            )));
1373        }
1374        match percentile_inc(&mut nums, p) {
1375            Ok(v) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(v))),
1376            Err(e) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
1377        }
1378    }
1379}
1380
1381/// Returns the k-th percentile of a data set using exclusive interpolation.
1382///
1383/// `PERCENTILE.EXC` uses the `n+1` rank basis and excludes the exact endpoints `0` and `1`.
1384///
1385/// # Remarks
1386/// - `k` must satisfy `0 < k < 1`.
1387/// - Returns `#NUM!` when the percentile falls outside the valid rank range for the data size.
1388/// - Returns `#NUM!` for empty numeric input.
1389///
1390/// # Examples
1391///
1392/// ```yaml,sandbox
1393/// title: "Exclusive 25th percentile from direct values"
1394/// formula: "=PERCENTILE.EXC({1,2,3,4,5},0.25)"
1395/// expected: 1.5
1396/// ```
1397///
1398/// ```yaml,sandbox
1399/// title: "Exclusive percentile from a range"
1400/// grid:
1401///   A1: 10
1402///   A2: 20
1403///   A3: 30
1404///   A4: 40
1405///   A5: 50
1406/// formula: "=PERCENTILE.EXC(A1:A5,0.6)"
1407/// expected: 36
1408/// ```
1409///
1410/// ```yaml,docs
1411/// related:
1412///   - PERCENTILE.INC
1413///   - QUARTILE.EXC
1414///   - PERCENTRANK.EXC
1415/// faq:
1416///   - q: "Why does PERCENTILE.EXC reject k=0 or k=1?"
1417///     a: "Exclusive percentile uses the n+1 basis and requires strictly 0 < k < 1."
1418/// ```
1419#[derive(Debug)]
1420pub struct PercentileExc; // exclusive
1421/// [formualizer-docgen:schema:start]
1422/// Name: PERCENTILE.EXC
1423/// Type: PercentileExc
1424/// Min args: 2
1425/// Max args: variadic
1426/// Variadic: true
1427/// Signature: PERCENTILE.EXC(arg1...: number@range)
1428/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1429/// Caps: PURE, NUMERIC_ONLY
1430/// [formualizer-docgen:schema:end]
1431impl Function for PercentileExc {
1432    func_caps!(PURE, NUMERIC_ONLY);
1433    fn name(&self) -> &'static str {
1434        "PERCENTILE.EXC"
1435    }
1436    fn min_args(&self) -> usize {
1437        2
1438    }
1439    fn variadic(&self) -> bool {
1440        true
1441    }
1442    fn arg_schema(&self) -> &'static [ArgSchema] {
1443        &ARG_RANGE_NUM_LENIENT_ONE[..]
1444    }
1445    fn eval<'a, 'b, 'c>(
1446        &self,
1447        args: &'c [ArgumentHandle<'a, 'b>],
1448        _ctx: &dyn FunctionContext<'b>,
1449    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1450        if args.len() < 2 {
1451            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1452                ExcelError::new_num(),
1453            )));
1454        }
1455        let pv = scalar_like_value(args.last().unwrap())?;
1456        let p = match coerce_num(&pv) {
1457            Ok(n) => n,
1458            Err(_) => {
1459                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1460                    ExcelError::new_num(),
1461                )));
1462            }
1463        };
1464        let mut nums = collect_numeric_stats(&args[..args.len() - 1])?;
1465        if nums.is_empty() {
1466            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1467                ExcelError::new_num(),
1468            )));
1469        }
1470        match percentile_exc(&mut nums, p) {
1471            Ok(v) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(v))),
1472            Err(e) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
1473        }
1474    }
1475}
1476
1477/// Returns an inclusive quartile value for a data set.
1478///
1479/// `QUARTILE.INC` maps quartile index `0..4` onto minimum, quartiles, median, and maximum.
1480///
1481/// # Remarks
1482/// - Valid quartile index values are `0`, `1`, `2`, `3`, and `4`.
1483/// - Uses inclusive percentile logic for quartiles `1` through `3`.
1484/// - Returns `#NUM!` for invalid quartile index values or empty numeric input.
1485/// - Alias `QUARTILE` is supported.
1486///
1487/// # Examples
1488///
1489/// ```yaml,sandbox
1490/// title: "First quartile from direct values"
1491/// formula: "=QUARTILE.INC({1,2,3,4,5},1)"
1492/// expected: 2
1493/// ```
1494///
1495/// ```yaml,sandbox
1496/// title: "Third quartile from a range"
1497/// grid:
1498///   A1: 10
1499///   A2: 20
1500///   A3: 30
1501///   A4: 40
1502/// formula: "=QUARTILE.INC(A1:A4,3)"
1503/// expected: 32.5
1504/// ```
1505///
1506/// ```yaml,docs
1507/// related:
1508///   - QUARTILE.EXC
1509///   - PERCENTILE.INC
1510///   - MEDIAN
1511/// faq:
1512///   - q: "Which quartile numbers are valid for QUARTILE.INC?"
1513///     a: "Only 0 through 4 are valid; other quartile indices return #NUM!."
1514/// ```
1515#[derive(Debug)]
1516pub struct QuartileInc; // quartile inclusive
1517/// [formualizer-docgen:schema:start]
1518/// Name: QUARTILE.INC
1519/// Type: QuartileInc
1520/// Min args: 2
1521/// Max args: variadic
1522/// Variadic: true
1523/// Signature: QUARTILE.INC(arg1...: number@range)
1524/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1525/// Caps: PURE, NUMERIC_ONLY
1526/// [formualizer-docgen:schema:end]
1527impl Function for QuartileInc {
1528    func_caps!(PURE, NUMERIC_ONLY);
1529    fn name(&self) -> &'static str {
1530        "QUARTILE.INC"
1531    }
1532    fn aliases(&self) -> &'static [&'static str] {
1533        &["QUARTILE"]
1534    }
1535    fn min_args(&self) -> usize {
1536        2
1537    }
1538    fn variadic(&self) -> bool {
1539        true
1540    }
1541    fn arg_schema(&self) -> &'static [ArgSchema] {
1542        &ARG_RANGE_NUM_LENIENT_ONE[..]
1543    }
1544    fn eval<'a, 'b, 'c>(
1545        &self,
1546        args: &'c [ArgumentHandle<'a, 'b>],
1547        _ctx: &dyn FunctionContext<'b>,
1548    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1549        if args.len() < 2 {
1550            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1551                ExcelError::new_num(),
1552            )));
1553        }
1554        let qv = scalar_like_value(args.last().unwrap())?;
1555        let q = match coerce_num(&qv) {
1556            Ok(n) => n,
1557            Err(_) => {
1558                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1559                    ExcelError::new_num(),
1560                )));
1561            }
1562        };
1563        let q_i = q as i64;
1564        if !(0..=4).contains(&q_i) {
1565            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1566                ExcelError::new_num(),
1567            )));
1568        }
1569        let mut nums = collect_numeric_stats(&args[..args.len() - 1])?;
1570        if nums.is_empty() {
1571            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1572                ExcelError::new_num(),
1573            )));
1574        }
1575        let p = match q_i {
1576            0 => {
1577                let v = nth_smallest(&mut nums, 0);
1578                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(v)));
1579            }
1580            4 => {
1581                let idx = nums.len() - 1;
1582                let v = nth_smallest(&mut nums, idx);
1583                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(v)));
1584            }
1585            1 => 0.25,
1586            2 => 0.5,
1587            3 => 0.75,
1588            _ => {
1589                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1590                    ExcelError::new_num(),
1591                )));
1592            }
1593        };
1594        match percentile_inc(&mut nums, p) {
1595            Ok(v) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(v))),
1596            Err(e) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
1597        }
1598    }
1599}
1600
1601/// Returns an exclusive quartile value for a data set.
1602///
1603/// `QUARTILE.EXC` applies exclusive percentile interpolation and supports quartiles `1` through
1604/// `3`.
1605///
1606/// # Remarks
1607/// - Valid quartile index values are `1`, `2`, and `3`.
1608/// - Returns `#NUM!` for invalid quartile index values.
1609/// - Returns `#NUM!` when the input is too small for exclusive quartile interpolation.
1610///
1611/// # Examples
1612///
1613/// ```yaml,sandbox
1614/// title: "First exclusive quartile from direct values"
1615/// formula: "=QUARTILE.EXC({1,2,3,4,5,6,7,8},1)"
1616/// expected: 2.25
1617/// ```
1618///
1619/// ```yaml,sandbox
1620/// title: "Third exclusive quartile from a range"
1621/// grid:
1622///   A1: 10
1623///   A2: 20
1624///   A3: 30
1625///   A4: 40
1626///   A5: 50
1627///   A6: 60
1628///   A7: 70
1629///   A8: 80
1630/// formula: "=QUARTILE.EXC(A1:A8,3)"
1631/// expected: 67.5
1632/// ```
1633///
1634/// ```yaml,docs
1635/// related:
1636///   - QUARTILE.INC
1637///   - PERCENTILE.EXC
1638///   - MEDIAN
1639/// faq:
1640///   - q: "Why can QUARTILE.EXC return #NUM! on small datasets?"
1641///     a: "Exclusive quartiles need enough data for valid interior rank interpolation."
1642/// ```
1643#[derive(Debug)]
1644pub struct QuartileExc; // quartile exclusive
1645/// [formualizer-docgen:schema:start]
1646/// Name: QUARTILE.EXC
1647/// Type: QuartileExc
1648/// Min args: 2
1649/// Max args: variadic
1650/// Variadic: true
1651/// Signature: QUARTILE.EXC(arg1...: number@range)
1652/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1653/// Caps: PURE, NUMERIC_ONLY
1654/// [formualizer-docgen:schema:end]
1655impl Function for QuartileExc {
1656    func_caps!(PURE, NUMERIC_ONLY);
1657    fn name(&self) -> &'static str {
1658        "QUARTILE.EXC"
1659    }
1660    fn min_args(&self) -> usize {
1661        2
1662    }
1663    fn variadic(&self) -> bool {
1664        true
1665    }
1666    fn arg_schema(&self) -> &'static [ArgSchema] {
1667        &ARG_RANGE_NUM_LENIENT_ONE[..]
1668    }
1669    fn eval<'a, 'b, 'c>(
1670        &self,
1671        args: &'c [ArgumentHandle<'a, 'b>],
1672        _ctx: &dyn FunctionContext<'b>,
1673    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1674        if args.len() < 2 {
1675            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1676                ExcelError::new_num(),
1677            )));
1678        }
1679        let qv = scalar_like_value(args.last().unwrap())?;
1680        let q = match coerce_num(&qv) {
1681            Ok(n) => n,
1682            Err(_) => {
1683                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1684                    ExcelError::new_num(),
1685                )));
1686            }
1687        };
1688        let q_i = q as i64;
1689        if !(1..=3).contains(&q_i) {
1690            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1691                ExcelError::new_num(),
1692            )));
1693        }
1694        let mut nums = collect_numeric_stats(&args[..args.len() - 1])?;
1695        if nums.len() < 2 {
1696            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1697                ExcelError::new_num(),
1698            )));
1699        }
1700        let p = match q_i {
1701            1 => 0.25,
1702            2 => 0.5,
1703            3 => 0.75,
1704            _ => {
1705                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1706                    ExcelError::new_num(),
1707                )));
1708            }
1709        };
1710        match percentile_exc(&mut nums, p) {
1711            Ok(v) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(v))),
1712            Err(e) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
1713        }
1714    }
1715}
1716
1717/// Multiplies all numeric arguments and returns their product.
1718///
1719/// `PRODUCT` is useful for chained growth factors, scaling ratios, and compound multipliers.
1720///
1721/// # Remarks
1722/// - Non-numeric values in referenced ranges are ignored.
1723/// - Returns `0` when no numeric values are found.
1724/// - Direct scalar arguments still attempt numeric coercion.
1725///
1726/// # Examples
1727///
1728/// ```yaml,sandbox
1729/// title: "Product of scalar values"
1730/// formula: "=PRODUCT(2,3,4)"
1731/// expected: 24
1732/// ```
1733///
1734/// ```yaml,sandbox
1735/// title: "Product from a range"
1736/// grid:
1737///   A1: 1
1738///   A2: 5
1739///   A3: 10
1740/// formula: "=PRODUCT(A1:A3)"
1741/// expected: 50
1742/// ```
1743///
1744/// ```yaml,docs
1745/// related:
1746///   - SUM
1747///   - GEOMEAN
1748///   - SUMPRODUCT
1749/// faq:
1750///   - q: "Why does PRODUCT return 0 when no numeric inputs are found?"
1751///     a: "This implementation returns 0 for an empty numeric set after filtering."
1752/// ```
1753#[derive(Debug)]
1754pub struct ProductFn;
1755/// [formualizer-docgen:schema:start]
1756/// Name: PRODUCT
1757/// Type: ProductFn
1758/// Min args: 1
1759/// Max args: variadic
1760/// Variadic: true
1761/// Signature: PRODUCT(arg1...: number@range)
1762/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1763/// Caps: PURE, REDUCTION, NUMERIC_ONLY
1764/// [formualizer-docgen:schema:end]
1765impl Function for ProductFn {
1766    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
1767    fn name(&self) -> &'static str {
1768        "PRODUCT"
1769    }
1770    fn min_args(&self) -> usize {
1771        1
1772    }
1773    fn variadic(&self) -> bool {
1774        true
1775    }
1776    fn dependency_contract(&self, arity: usize) -> Option<FunctionDependencyContract> {
1777        FunctionDependencyContract::static_reduction(arity, self.min_args())
1778    }
1779    fn arg_schema(&self) -> &'static [ArgSchema] {
1780        &ARG_RANGE_NUM_LENIENT_ONE[..]
1781    }
1782    fn eval<'a, 'b, 'c>(
1783        &self,
1784        args: &'c [ArgumentHandle<'a, 'b>],
1785        _ctx: &dyn FunctionContext<'b>,
1786    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1787        let nums = collect_numeric_stats(args)?;
1788        if nums.is_empty() {
1789            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0)));
1790        }
1791        let result = nums.iter().product::<f64>();
1792        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
1793            result,
1794        )))
1795    }
1796}
1797
1798/// Returns the geometric mean of positive numeric values.
1799///
1800/// `GEOMEAN` is commonly used for rates of change and multiplicative growth comparisons.
1801///
1802/// # Remarks
1803/// - All numeric inputs must be strictly greater than `0`.
1804/// - Returns `#NUM!` if any value is `<= 0`, or if no numeric values are provided.
1805/// - Non-numeric values in referenced ranges are ignored.
1806///
1807/// # Examples
1808///
1809/// ```yaml,sandbox
1810/// title: "Geometric mean from scalar values"
1811/// formula: "=GEOMEAN(4,16)"
1812/// expected: 8
1813/// ```
1814///
1815/// ```yaml,sandbox
1816/// title: "Geometric mean from a range"
1817/// grid:
1818///   A1: 1
1819///   A2: 3
1820///   A3: 9
1821/// formula: "=GEOMEAN(A1:A3)"
1822/// expected: 3
1823/// ```
1824///
1825/// ```yaml,docs
1826/// related:
1827///   - HARMEAN
1828///   - PRODUCT
1829///   - AVERAGE
1830/// faq:
1831///   - q: "When does GEOMEAN return #NUM!?"
1832///     a: "GEOMEAN returns #NUM! if any numeric value is <= 0 or if no numeric values exist."
1833/// ```
1834#[derive(Debug)]
1835pub struct GeomeanFn;
1836/// [formualizer-docgen:schema:start]
1837/// Name: GEOMEAN
1838/// Type: GeomeanFn
1839/// Min args: 1
1840/// Max args: variadic
1841/// Variadic: true
1842/// Signature: GEOMEAN(arg1...: number@range)
1843/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1844/// Caps: PURE, REDUCTION, NUMERIC_ONLY
1845/// [formualizer-docgen:schema:end]
1846impl Function for GeomeanFn {
1847    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
1848    fn name(&self) -> &'static str {
1849        "GEOMEAN"
1850    }
1851    fn min_args(&self) -> usize {
1852        1
1853    }
1854    fn variadic(&self) -> bool {
1855        true
1856    }
1857    fn arg_schema(&self) -> &'static [ArgSchema] {
1858        &ARG_RANGE_NUM_LENIENT_ONE[..]
1859    }
1860    fn eval<'a, 'b, 'c>(
1861        &self,
1862        args: &'c [ArgumentHandle<'a, 'b>],
1863        _ctx: &dyn FunctionContext<'b>,
1864    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1865        let nums = collect_numeric_stats(args)?;
1866        if nums.is_empty() {
1867            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1868                ExcelError::new_num(),
1869            )));
1870        }
1871        // All values must be positive
1872        if nums.iter().any(|&n| n <= 0.0) {
1873            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1874                ExcelError::new_num(),
1875            )));
1876        }
1877        // Geometric mean = (x1 * x2 * ... * xn)^(1/n)
1878        // Use log to avoid overflow: exp(mean(ln(x)))
1879        let log_sum: f64 = nums.iter().map(|x| x.ln()).sum();
1880        let result = (log_sum / nums.len() as f64).exp();
1881        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
1882            result,
1883        )))
1884    }
1885}
1886
1887/// Returns the harmonic mean of positive numeric values.
1888///
1889/// `HARMEAN` emphasizes smaller values and is useful for averaging rates and ratios.
1890///
1891/// # Remarks
1892/// - All numeric inputs must be strictly greater than `0`.
1893/// - Returns `#NUM!` if any value is `<= 0`, or if no numeric values are provided.
1894/// - Non-numeric values in referenced ranges are ignored.
1895///
1896/// # Examples
1897///
1898/// ```yaml,sandbox
1899/// title: "Harmonic mean from scalar values"
1900/// formula: "=HARMEAN(1,2,4)"
1901/// expected: 1.7142857142857142
1902/// ```
1903///
1904/// ```yaml,sandbox
1905/// title: "Harmonic mean from a range"
1906/// grid:
1907///   A1: 2
1908///   A2: 3
1909///   A3: 6
1910/// formula: "=HARMEAN(A1:A3)"
1911/// expected: 3
1912/// ```
1913///
1914/// ```yaml,docs
1915/// related:
1916///   - GEOMEAN
1917///   - AVERAGE
1918///   - PRODUCT
1919/// faq:
1920///   - q: "Why does HARMEAN fail on zeros?"
1921///     a: "Harmonic mean uses reciprocals, so inputs must be strictly positive."
1922/// ```
1923#[derive(Debug)]
1924pub struct HarmeanFn;
1925/// [formualizer-docgen:schema:start]
1926/// Name: HARMEAN
1927/// Type: HarmeanFn
1928/// Min args: 1
1929/// Max args: variadic
1930/// Variadic: true
1931/// Signature: HARMEAN(arg1...: number@range)
1932/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1933/// Caps: PURE, REDUCTION, NUMERIC_ONLY
1934/// [formualizer-docgen:schema:end]
1935impl Function for HarmeanFn {
1936    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
1937    fn name(&self) -> &'static str {
1938        "HARMEAN"
1939    }
1940    fn min_args(&self) -> usize {
1941        1
1942    }
1943    fn variadic(&self) -> bool {
1944        true
1945    }
1946    fn arg_schema(&self) -> &'static [ArgSchema] {
1947        &ARG_RANGE_NUM_LENIENT_ONE[..]
1948    }
1949    fn eval<'a, 'b, 'c>(
1950        &self,
1951        args: &'c [ArgumentHandle<'a, 'b>],
1952        _ctx: &dyn FunctionContext<'b>,
1953    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1954        let nums = collect_numeric_stats(args)?;
1955        if nums.is_empty() {
1956            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1957                ExcelError::new_num(),
1958            )));
1959        }
1960        // All values must be positive
1961        if nums.iter().any(|&n| n <= 0.0) {
1962            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1963                ExcelError::new_num(),
1964            )));
1965        }
1966        // Harmonic mean = n / sum(1/x)
1967        let sum_reciprocals: f64 = nums.iter().map(|x| 1.0 / x).sum();
1968        let result = nums.len() as f64 / sum_reciprocals;
1969        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
1970            result,
1971        )))
1972    }
1973}
1974
1975/// Returns the average of absolute deviations from the mean.
1976///
1977/// `AVEDEV` provides a robust spread measure that is less sensitive to outliers than squared-error
1978/// metrics.
1979///
1980/// # Remarks
1981/// - Returns `#NUM!` when no numeric values are available.
1982/// - Non-numeric values in referenced ranges are ignored.
1983/// - Uses the arithmetic mean as the center point.
1984///
1985/// # Examples
1986///
1987/// ```yaml,sandbox
1988/// title: "Average absolute deviation from scalar values"
1989/// formula: "=AVEDEV(2,4,6)"
1990/// expected: 1.3333333333333333
1991/// ```
1992///
1993/// ```yaml,sandbox
1994/// title: "Average absolute deviation from a range"
1995/// grid:
1996///   A1: 1
1997///   A2: 1
1998///   A3: 3
1999///   A4: 5
2000/// formula: "=AVEDEV(A1:A4)"
2001/// expected: 1.5
2002/// ```
2003///
2004/// ```yaml,docs
2005/// related:
2006///   - DEVSQ
2007///   - STDEV.S
2008///   - VAR.S
2009/// faq:
2010///   - q: "What center does AVEDEV use for deviations?"
2011///     a: "It computes absolute deviations around the arithmetic mean of included values."
2012/// ```
2013#[derive(Debug)]
2014pub struct AvedevFn;
2015/// [formualizer-docgen:schema:start]
2016/// Name: AVEDEV
2017/// Type: AvedevFn
2018/// Min args: 1
2019/// Max args: variadic
2020/// Variadic: true
2021/// Signature: AVEDEV(arg1...: number@range)
2022/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
2023/// Caps: PURE, REDUCTION, NUMERIC_ONLY
2024/// [formualizer-docgen:schema:end]
2025impl Function for AvedevFn {
2026    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
2027    fn name(&self) -> &'static str {
2028        "AVEDEV"
2029    }
2030    fn min_args(&self) -> usize {
2031        1
2032    }
2033    fn variadic(&self) -> bool {
2034        true
2035    }
2036    fn arg_schema(&self) -> &'static [ArgSchema] {
2037        &ARG_RANGE_NUM_LENIENT_ONE[..]
2038    }
2039    fn eval<'a, 'b, 'c>(
2040        &self,
2041        args: &'c [ArgumentHandle<'a, 'b>],
2042        _ctx: &dyn FunctionContext<'b>,
2043    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2044        let nums = collect_numeric_stats(args)?;
2045        if nums.is_empty() {
2046            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2047                ExcelError::new_num(),
2048            )));
2049        }
2050        let mean = nums.iter().sum::<f64>() / nums.len() as f64;
2051        let avedev = nums.iter().map(|x| (x - mean).abs()).sum::<f64>() / nums.len() as f64;
2052        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
2053            avedev,
2054        )))
2055    }
2056}
2057
2058/// Returns the sum of squared deviations from the mean.
2059///
2060/// `DEVSQ` is useful for variance-related calculations and diagnostics of spread.
2061///
2062/// # Remarks
2063/// - Returns `#NUM!` when no numeric values are available.
2064/// - Non-numeric values in referenced ranges are ignored.
2065/// - Uses the arithmetic mean of included values.
2066///
2067/// # Examples
2068///
2069/// ```yaml,sandbox
2070/// title: "Sum of squared deviations from scalar values"
2071/// formula: "=DEVSQ(2,4,6)"
2072/// expected: 8
2073/// ```
2074///
2075/// ```yaml,sandbox
2076/// title: "Sum of squared deviations from a range"
2077/// grid:
2078///   A1: 1
2079///   A2: 2
2080///   A3: 3
2081///   A4: 4
2082/// formula: "=DEVSQ(A1:A4)"
2083/// expected: 5
2084/// ```
2085#[derive(Debug)]
2086pub struct DevsqFn;
2087
2088/* ─────────────────────────── MAXIFS / MINIFS ──────────────────────────── */
2089
2090use super::utils::{ARG_ANY_ONE, criteria_match};
2091
2092/// Returns the maximum numeric value in a range that meets all criteria.
2093///
2094/// `MAXIFS` applies one or more `(criteria_range, criteria)` pairs and returns the largest
2095/// matching numeric value.
2096///
2097/// # Remarks
2098/// - Arguments must be `target_range` plus one or more criteria pairs.
2099/// - Criteria are combined with logical AND.
2100/// - Returns `0` when no cells satisfy all criteria.
2101/// - Non-numeric cells in `target_range` are ignored.
2102///
2103/// # Examples
2104///
2105/// ```yaml,sandbox
2106/// title: "Maximum value for one condition"
2107/// grid:
2108///   A1: 10
2109///   A2: 20
2110///   A3: 15
2111///   B1: "East"
2112///   B2: "West"
2113///   B3: "East"
2114/// formula: "=MAXIFS(A1:A3,B1:B3,\"East\")"
2115/// expected: 15
2116/// ```
2117///
2118/// ```yaml,sandbox
2119/// title: "Maximum value with two criteria"
2120/// grid:
2121///   A1: 100
2122///   A2: 80
2123///   A3: 90
2124///   A4: 70
2125///   B1: "A"
2126///   B2: "A"
2127///   B3: "B"
2128///   B4: "B"
2129///   C1: "Q1"
2130///   C2: "Q2"
2131///   C3: "Q1"
2132///   C4: "Q1"
2133/// formula: "=MAXIFS(A1:A4,B1:B4,\"B\",C1:C4,\"Q1\")"
2134/// expected: 90
2135/// ```
2136///
2137/// ```yaml,docs
2138/// related:
2139///   - MINIFS
2140///   - MAX
2141///   - SUMIFS
2142/// faq:
2143///   - q: "What does MAXIFS return when no rows match all criteria?"
2144///     a: "It returns 0 when no numeric target cells satisfy every criterion."
2145/// ```
2146#[derive(Debug)]
2147pub struct MaxIfsFn;
2148/// [formualizer-docgen:schema:start]
2149/// Name: MAXIFS
2150/// Type: MaxIfsFn
2151/// Min args: 3
2152/// Max args: variadic
2153/// Variadic: true
2154/// Signature: MAXIFS(arg1...: any@scalar)
2155/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
2156/// Caps: PURE, REDUCTION
2157/// [formualizer-docgen:schema:end]
2158impl Function for MaxIfsFn {
2159    func_caps!(PURE, REDUCTION);
2160    fn name(&self) -> &'static str {
2161        "MAXIFS"
2162    }
2163    fn min_args(&self) -> usize {
2164        3
2165    }
2166    fn variadic(&self) -> bool {
2167        true
2168    }
2169    fn arg_schema(&self) -> &'static [ArgSchema] {
2170        &ARG_ANY_ONE[..]
2171    }
2172    fn eval<'a, 'b, 'c>(
2173        &self,
2174        args: &'c [ArgumentHandle<'a, 'b>],
2175        _ctx: &dyn FunctionContext<'b>,
2176    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2177        eval_maxminifs(args, true)
2178    }
2179}
2180
2181/// Returns the minimum numeric value in a range that meets all criteria.
2182///
2183/// `MINIFS` evaluates one or more `(criteria_range, criteria)` pairs and returns the smallest
2184/// matching numeric value.
2185///
2186/// # Remarks
2187/// - Arguments must be `target_range` plus one or more criteria pairs.
2188/// - Criteria are combined with logical AND.
2189/// - Returns `0` when no cells satisfy all criteria.
2190/// - Non-numeric cells in `target_range` are ignored.
2191///
2192/// # Examples
2193///
2194/// ```yaml,sandbox
2195/// title: "Minimum value for one condition"
2196/// grid:
2197///   A1: 10
2198///   A2: 20
2199///   A3: 15
2200///   B1: "East"
2201///   B2: "West"
2202///   B3: "East"
2203/// formula: "=MINIFS(A1:A3,B1:B3,\"East\")"
2204/// expected: 10
2205/// ```
2206///
2207/// ```yaml,sandbox
2208/// title: "Minimum value with two criteria"
2209/// grid:
2210///   A1: 100
2211///   A2: 80
2212///   A3: 90
2213///   A4: 70
2214///   B1: "A"
2215///   B2: "A"
2216///   B3: "B"
2217///   B4: "B"
2218///   C1: "Q1"
2219///   C2: "Q2"
2220///   C3: "Q1"
2221///   C4: "Q1"
2222/// formula: "=MINIFS(A1:A4,B1:B4,\"B\",C1:C4,\"Q1\")"
2223/// expected: 70
2224/// ```
2225///
2226/// ```yaml,docs
2227/// related:
2228///   - MAXIFS
2229///   - MIN
2230///   - SUMIFS
2231/// faq:
2232///   - q: "How does MINIFS treat non-numeric target cells?"
2233///     a: "Non-numeric target cells are ignored; only numeric matches are eligible."
2234/// ```
2235#[derive(Debug)]
2236pub struct MinIfsFn;
2237/// [formualizer-docgen:schema:start]
2238/// Name: MINIFS
2239/// Type: MinIfsFn
2240/// Min args: 3
2241/// Max args: variadic
2242/// Variadic: true
2243/// Signature: MINIFS(arg1...: any@scalar)
2244/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
2245/// Caps: PURE, REDUCTION
2246/// [formualizer-docgen:schema:end]
2247impl Function for MinIfsFn {
2248    func_caps!(PURE, REDUCTION);
2249    fn name(&self) -> &'static str {
2250        "MINIFS"
2251    }
2252    fn min_args(&self) -> usize {
2253        3
2254    }
2255    fn variadic(&self) -> bool {
2256        true
2257    }
2258    fn arg_schema(&self) -> &'static [ArgSchema] {
2259        &ARG_ANY_ONE[..]
2260    }
2261    fn eval<'a, 'b, 'c>(
2262        &self,
2263        args: &'c [ArgumentHandle<'a, 'b>],
2264        _ctx: &dyn FunctionContext<'b>,
2265    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2266        eval_maxminifs(args, false)
2267    }
2268}
2269
2270/// Shared implementation for MAXIFS and MINIFS
2271fn eval_maxminifs<'a, 'b>(
2272    args: &[ArgumentHandle<'a, 'b>],
2273    is_max: bool,
2274) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2275    // Validate argument count: must be target_range + N pairs
2276    if args.len() < 3 || !(args.len() - 1).is_multiple_of(2) {
2277        return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2278            ExcelError::new_value().with_message(format!(
2279                "Function expects 1 target_range followed by N pairs (criteria_range, criteria); got {} args",
2280                args.len()
2281            )),
2282        )));
2283    }
2284
2285    // Get target range
2286    let target_view = match args[0].range_view() {
2287        Ok(v) => v,
2288        Err(_) => {
2289            // Single value case - if criteria match, return that value
2290            let target_val = args[0].value()?.into_literal();
2291            if let LiteralValue::Error(e) = target_val {
2292                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
2293            }
2294            // Check all criteria against empty/scalar
2295            let mut all_match = true;
2296            for i in (1..args.len()).step_by(2) {
2297                let crit_val = args[i].value()?.into_literal();
2298                let pred = crate::args::parse_criteria(&args[i + 1].value()?.into_literal())?;
2299                if !criteria_match(&pred, &crit_val) {
2300                    all_match = false;
2301                    break;
2302                }
2303            }
2304            if all_match {
2305                return match coerce_num(&target_val) {
2306                    Ok(n) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(n))),
2307                    Err(_) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0))),
2308                };
2309            }
2310            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0)));
2311        }
2312    };
2313
2314    let (rows, cols) = target_view.dims();
2315
2316    // Parse all criteria
2317    let mut criteria_ranges = Vec::new();
2318    let mut predicates = Vec::new();
2319    for i in (1..args.len()).step_by(2) {
2320        let crit_view = args[i].range_view().ok();
2321        let pred = crate::args::parse_criteria(&args[i + 1].value()?.into_literal())?;
2322        criteria_ranges.push(crit_view);
2323        predicates.push(pred);
2324    }
2325
2326    // Iterate through all cells and find max/min where all criteria match
2327    let mut result: Option<f64> = None;
2328
2329    for r in 0..rows {
2330        for c in 0..cols {
2331            // Check all criteria
2332            let mut all_match = true;
2333            for (crit_idx, pred) in predicates.iter().enumerate() {
2334                let crit_val = match &criteria_ranges[crit_idx] {
2335                    Some(view) => {
2336                        let (cr, cc) = view.dims();
2337                        if r < cr && c < cc {
2338                            view.get_cell(r, c)
2339                        } else {
2340                            LiteralValue::Empty
2341                        }
2342                    }
2343                    None => LiteralValue::Empty,
2344                };
2345                if !criteria_match(pred, &crit_val) {
2346                    all_match = false;
2347                    break;
2348                }
2349            }
2350
2351            if all_match {
2352                let target_val = target_view.get_cell(r, c);
2353                match target_val {
2354                    LiteralValue::Error(e) => {
2355                        return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
2356                    }
2357                    LiteralValue::Number(n) => {
2358                        result = Some(match result {
2359                            None => n,
2360                            Some(curr) => {
2361                                if is_max {
2362                                    curr.max(n)
2363                                } else {
2364                                    curr.min(n)
2365                                }
2366                            }
2367                        });
2368                    }
2369                    LiteralValue::Int(i) => {
2370                        let n = i as f64;
2371                        result = Some(match result {
2372                            None => n,
2373                            Some(curr) => {
2374                                if is_max {
2375                                    curr.max(n)
2376                                } else {
2377                                    curr.min(n)
2378                                }
2379                            }
2380                        });
2381                    }
2382                    _ => {} // Skip non-numeric
2383                }
2384            }
2385        }
2386    }
2387
2388    // Excel returns 0 if no matches found
2389    Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
2390        result.unwrap_or(0.0),
2391    )))
2392}
2393
2394/* ─────────────────────────── TRIMMEAN ──────────────────────────── */
2395
2396/// Returns the mean after trimming a percentage of values from both tails.
2397///
2398/// `TRIMMEAN` sorts numeric data, removes an equal count from low and high ends, then averages the
2399/// remaining interior values.
2400///
2401/// # Remarks
2402/// - `percent` must satisfy `0 <= percent < 1`.
2403/// - The trimmed count per side is `floor(n * percent / 2)`.
2404/// - Returns `#NUM!` for invalid percent values or when no numeric values are available.
2405///
2406/// # Examples
2407///
2408/// ```yaml,sandbox
2409/// title: "Trimmed mean from direct values"
2410/// formula: "=TRIMMEAN({1,2,3,4,5,6},0.3333333333333333)"
2411/// expected: 3.5
2412/// ```
2413///
2414/// ```yaml,sandbox
2415/// title: "Trimmed mean from a range"
2416/// grid:
2417///   A1: 10
2418///   A2: 12
2419///   A3: 13
2420///   A4: 20
2421///   A5: 21
2422///   A6: 30
2423/// formula: "=TRIMMEAN(A1:A6,0.4)"
2424/// expected: 16.5
2425/// ```
2426#[derive(Debug)]
2427pub struct TrimmeanFn;
2428/// [formualizer-docgen:schema:start]
2429/// Name: TRIMMEAN
2430/// Type: TrimmeanFn
2431/// Min args: 2
2432/// Max args: 1
2433/// Variadic: false
2434/// Signature: TRIMMEAN(arg1: number@range)
2435/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
2436/// Caps: PURE, REDUCTION, NUMERIC_ONLY
2437/// [formualizer-docgen:schema:end]
2438impl Function for TrimmeanFn {
2439    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
2440    fn name(&self) -> &'static str {
2441        "TRIMMEAN"
2442    }
2443    fn min_args(&self) -> usize {
2444        2
2445    }
2446    fn arg_schema(&self) -> &'static [ArgSchema] {
2447        &ARG_RANGE_NUM_LENIENT_ONE[..]
2448    }
2449    fn eval<'a, 'b, 'c>(
2450        &self,
2451        args: &'c [ArgumentHandle<'a, 'b>],
2452        _ctx: &dyn FunctionContext<'b>,
2453    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2454        let mut nums = collect_numeric_stats(&args[0..1])?;
2455        if nums.is_empty() {
2456            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2457                ExcelError::new_num(),
2458            )));
2459        }
2460
2461        let percent = match args[1].value()?.into_literal() {
2462            LiteralValue::Error(e) => {
2463                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
2464            }
2465            other => coerce_num(&other)?,
2466        };
2467
2468        // Percent must be between 0 and 1 (exclusive of 1)
2469        if !(0.0..1.0).contains(&percent) {
2470            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2471                ExcelError::new_num(),
2472            )));
2473        }
2474
2475        nums.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
2476
2477        let n = nums.len();
2478        // Number of values to exclude from each end
2479        let exclude = ((n as f64 * percent) / 2.0).floor() as usize;
2480
2481        if 2 * exclude >= n {
2482            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2483                ExcelError::new_num(),
2484            )));
2485        }
2486
2487        let trimmed = &nums[exclude..n - exclude];
2488        let sum: f64 = trimmed.iter().sum();
2489        let mean = sum / trimmed.len() as f64;
2490
2491        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(mean)))
2492    }
2493}
2494
2495/* ─────────────────────────── CORREL ──────────────────────────── */
2496
2497/// Helper to collect two paired arrays for regression/correlation functions
2498fn collect_paired_arrays(args: &[ArgumentHandle]) -> Result<(Vec<f64>, Vec<f64>), ExcelError> {
2499    let y_nums = collect_numeric_stats(&args[0..1])?;
2500    let x_nums = collect_numeric_stats(&args[1..2])?;
2501
2502    // Arrays must have same length
2503    if y_nums.len() != x_nums.len() {
2504        return Err(ExcelError::new_na());
2505    }
2506
2507    if y_nums.is_empty() {
2508        return Err(ExcelError::new_div());
2509    }
2510
2511    Ok((y_nums, x_nums))
2512}
2513
2514/// Returns the Pearson correlation coefficient between two numeric arrays.
2515///
2516/// `CORREL` measures linear relationship strength from `-1` (perfect inverse) to `1` (perfect
2517/// direct).
2518///
2519/// # Remarks
2520/// - Both arrays must produce the same number of numeric values.
2521/// - Returns `#N/A` when array lengths differ.
2522/// - Returns `#DIV/0!` when either series has zero variance.
2523///
2524/// # Examples
2525///
2526/// ```yaml,sandbox
2527/// title: "Perfect positive linear correlation"
2528/// formula: "=CORREL({2,4,6},{1,2,3})"
2529/// expected: 1
2530/// ```
2531///
2532/// ```yaml,sandbox
2533/// title: "Perfect negative linear correlation"
2534/// grid:
2535///   A1: 10
2536///   A2: 8
2537///   A3: 6
2538///   B1: 1
2539///   B2: 2
2540///   B3: 3
2541/// formula: "=CORREL(A1:A3,B1:B3)"
2542/// expected: -1
2543/// ```
2544#[derive(Debug)]
2545pub struct CorrelFn;
2546/// [formualizer-docgen:schema:start]
2547/// Name: CORREL
2548/// Type: CorrelFn
2549/// Min args: 2
2550/// Max args: 1
2551/// Variadic: false
2552/// Signature: CORREL(arg1: number@range)
2553/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
2554/// Caps: PURE, REDUCTION, NUMERIC_ONLY
2555/// [formualizer-docgen:schema:end]
2556impl Function for CorrelFn {
2557    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
2558    fn name(&self) -> &'static str {
2559        "CORREL"
2560    }
2561    fn min_args(&self) -> usize {
2562        2
2563    }
2564    fn arg_schema(&self) -> &'static [ArgSchema] {
2565        &ARG_RANGE_NUM_LENIENT_ONE[..]
2566    }
2567    fn eval<'a, 'b, 'c>(
2568        &self,
2569        args: &'c [ArgumentHandle<'a, 'b>],
2570        _ctx: &dyn FunctionContext<'b>,
2571    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2572        let (y, x) = match collect_paired_arrays(args) {
2573            Ok(v) => v,
2574            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
2575        };
2576
2577        let n = x.len() as f64;
2578        let mean_x = x.iter().sum::<f64>() / n;
2579        let mean_y = y.iter().sum::<f64>() / n;
2580
2581        let mut sum_xy = 0.0;
2582        let mut sum_x2 = 0.0;
2583        let mut sum_y2 = 0.0;
2584
2585        for i in 0..x.len() {
2586            let dx = x[i] - mean_x;
2587            let dy = y[i] - mean_y;
2588            sum_xy += dx * dy;
2589            sum_x2 += dx * dx;
2590            sum_y2 += dy * dy;
2591        }
2592
2593        let denom = (sum_x2 * sum_y2).sqrt();
2594        if denom == 0.0 {
2595            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2596                ExcelError::new_div(),
2597            )));
2598        }
2599
2600        let correl = sum_xy / denom;
2601        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
2602            correl,
2603        )))
2604    }
2605}
2606
2607/* ─────────────────────────── SLOPE ──────────────────────────── */
2608
2609/// Returns the slope of the linear regression line for paired data.
2610///
2611/// `SLOPE` fits `y = m*x + b` and returns `m`, the rate of change in `y` per unit of `x`.
2612///
2613/// # Remarks
2614/// - `known_y` and `known_x` must have the same numeric length.
2615/// - Returns `#N/A` for mismatched lengths.
2616/// - Returns `#DIV/0!` if all `x` values are identical.
2617///
2618/// # Examples
2619///
2620/// ```yaml,sandbox
2621/// title: "Positive slope from direct arrays"
2622/// formula: "=SLOPE({2,4,6},{1,2,3})"
2623/// expected: 2
2624/// ```
2625///
2626/// ```yaml,sandbox
2627/// title: "Negative slope from ranges"
2628/// grid:
2629///   A1: 10
2630///   A2: 8
2631///   A3: 6
2632///   B1: 1
2633///   B2: 2
2634///   B3: 3
2635/// formula: "=SLOPE(A1:A3,B1:B3)"
2636/// expected: -2
2637/// ```
2638#[derive(Debug)]
2639pub struct SlopeFn;
2640/// [formualizer-docgen:schema:start]
2641/// Name: SLOPE
2642/// Type: SlopeFn
2643/// Min args: 2
2644/// Max args: 1
2645/// Variadic: false
2646/// Signature: SLOPE(arg1: number@range)
2647/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
2648/// Caps: PURE, REDUCTION, NUMERIC_ONLY
2649/// [formualizer-docgen:schema:end]
2650impl Function for SlopeFn {
2651    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
2652    fn name(&self) -> &'static str {
2653        "SLOPE"
2654    }
2655    fn min_args(&self) -> usize {
2656        2
2657    }
2658    fn arg_schema(&self) -> &'static [ArgSchema] {
2659        &ARG_RANGE_NUM_LENIENT_ONE[..]
2660    }
2661    fn eval<'a, 'b, 'c>(
2662        &self,
2663        args: &'c [ArgumentHandle<'a, 'b>],
2664        _ctx: &dyn FunctionContext<'b>,
2665    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2666        let (y, x) = match collect_paired_arrays(args) {
2667            Ok(v) => v,
2668            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
2669        };
2670
2671        let n = x.len() as f64;
2672        let mean_x = x.iter().sum::<f64>() / n;
2673        let mean_y = y.iter().sum::<f64>() / n;
2674
2675        let mut sum_xy = 0.0;
2676        let mut sum_x2 = 0.0;
2677
2678        for i in 0..x.len() {
2679            let dx = x[i] - mean_x;
2680            let dy = y[i] - mean_y;
2681            sum_xy += dx * dy;
2682            sum_x2 += dx * dx;
2683        }
2684
2685        if sum_x2 == 0.0 {
2686            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2687                ExcelError::new_div(),
2688            )));
2689        }
2690
2691        let slope = sum_xy / sum_x2;
2692        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
2693            slope,
2694        )))
2695    }
2696}
2697
2698/* ─────────────────────────── INTERCEPT ──────────────────────────── */
2699
2700/// Returns the y-intercept of the linear regression line for paired data.
2701///
2702/// `INTERCEPT` fits `y = m*x + b` and returns `b`, the predicted `y` when `x = 0`.
2703///
2704/// # Remarks
2705/// - `known_y` and `known_x` must have the same numeric length.
2706/// - Returns `#N/A` for mismatched lengths.
2707/// - Returns `#DIV/0!` if all `x` values are identical.
2708///
2709/// # Examples
2710///
2711/// ```yaml,sandbox
2712/// title: "Positive intercept from direct arrays"
2713/// formula: "=INTERCEPT({3,5,7},{1,2,3})"
2714/// expected: 1
2715/// ```
2716///
2717/// ```yaml,sandbox
2718/// title: "Intercept from range-based linear trend"
2719/// grid:
2720///   A1: 10
2721///   A2: 8
2722///   A3: 6
2723///   B1: 1
2724///   B2: 2
2725///   B3: 3
2726/// formula: "=INTERCEPT(A1:A3,B1:B3)"
2727/// expected: 12
2728/// ```
2729#[derive(Debug)]
2730pub struct InterceptFn;
2731/// [formualizer-docgen:schema:start]
2732/// Name: INTERCEPT
2733/// Type: InterceptFn
2734/// Min args: 2
2735/// Max args: 1
2736/// Variadic: false
2737/// Signature: INTERCEPT(arg1: number@range)
2738/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
2739/// Caps: PURE, REDUCTION, NUMERIC_ONLY
2740/// [formualizer-docgen:schema:end]
2741impl Function for InterceptFn {
2742    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
2743    fn name(&self) -> &'static str {
2744        "INTERCEPT"
2745    }
2746    fn min_args(&self) -> usize {
2747        2
2748    }
2749    fn arg_schema(&self) -> &'static [ArgSchema] {
2750        &ARG_RANGE_NUM_LENIENT_ONE[..]
2751    }
2752    fn eval<'a, 'b, 'c>(
2753        &self,
2754        args: &'c [ArgumentHandle<'a, 'b>],
2755        _ctx: &dyn FunctionContext<'b>,
2756    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2757        let (y, x) = match collect_paired_arrays(args) {
2758            Ok(v) => v,
2759            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
2760        };
2761
2762        let n = x.len() as f64;
2763        let mean_x = x.iter().sum::<f64>() / n;
2764        let mean_y = y.iter().sum::<f64>() / n;
2765
2766        let mut sum_xy = 0.0;
2767        let mut sum_x2 = 0.0;
2768
2769        for i in 0..x.len() {
2770            let dx = x[i] - mean_x;
2771            let dy = y[i] - mean_y;
2772            sum_xy += dx * dy;
2773            sum_x2 += dx * dx;
2774        }
2775
2776        if sum_x2 == 0.0 {
2777            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2778                ExcelError::new_div(),
2779            )));
2780        }
2781
2782        let slope = sum_xy / sum_x2;
2783        let intercept = mean_y - slope * mean_x;
2784        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
2785            intercept,
2786        )))
2787    }
2788}
2789
2790/// [formualizer-docgen:schema:start]
2791/// Name: DEVSQ
2792/// Type: DevsqFn
2793/// Min args: 1
2794/// Max args: variadic
2795/// Variadic: true
2796/// Signature: DEVSQ(arg1...: number@range)
2797/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
2798/// Caps: PURE, REDUCTION, NUMERIC_ONLY
2799/// [formualizer-docgen:schema:end]
2800impl Function for DevsqFn {
2801    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
2802    fn name(&self) -> &'static str {
2803        "DEVSQ"
2804    }
2805    fn min_args(&self) -> usize {
2806        1
2807    }
2808    fn variadic(&self) -> bool {
2809        true
2810    }
2811    fn arg_schema(&self) -> &'static [ArgSchema] {
2812        &ARG_RANGE_NUM_LENIENT_ONE[..]
2813    }
2814    fn eval<'a, 'b, 'c>(
2815        &self,
2816        args: &'c [ArgumentHandle<'a, 'b>],
2817        _ctx: &dyn FunctionContext<'b>,
2818    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2819        let nums = collect_numeric_stats(args)?;
2820        if nums.is_empty() {
2821            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2822                ExcelError::new_num(),
2823            )));
2824        }
2825        let mean = nums.iter().sum::<f64>() / nums.len() as f64;
2826        let devsq = nums.iter().map(|x| (x - mean).powi(2)).sum::<f64>();
2827        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
2828            devsq,
2829        )))
2830    }
2831}
2832
2833/* ═══════════════════════════════════════════════════════════════════════════
2834STATISTICAL DISTRIBUTION FUNCTIONS
2835═══════════════════════════════════════════════════════════════════════════ */
2836
2837/// Helper: Standard normal CDF using error function approximation
2838fn std_norm_cdf(z: f64) -> f64 {
2839    // Use the complementary error function: Φ(z) = 0.5 * erfc(-z / sqrt(2))
2840    // Approximation using Abramowitz and Stegun formula 7.1.26
2841    let a1 = 0.254829592;
2842    let a2 = -0.284496736;
2843    let a3 = 1.421413741;
2844    let a4 = -1.453152027;
2845    let a5 = 1.061405429;
2846    let p = 0.3275911;
2847
2848    let sign = if z < 0.0 { -1.0 } else { 1.0 };
2849    let z_abs = z.abs() / std::f64::consts::SQRT_2;
2850
2851    let t = 1.0 / (1.0 + p * z_abs);
2852    let y = 1.0 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * (-z_abs * z_abs).exp();
2853
2854    0.5 * (1.0 + sign * y)
2855}
2856
2857/// Helper: Standard normal PDF
2858fn std_norm_pdf(z: f64) -> f64 {
2859    let inv_sqrt_2pi = 1.0 / (2.0 * std::f64::consts::PI).sqrt();
2860    inv_sqrt_2pi * (-0.5 * z * z).exp()
2861}
2862
2863/// Helper: Inverse standard normal CDF (probit function)
2864/// Uses Rational approximation from Abramowitz and Stegun
2865#[allow(clippy::excessive_precision)]
2866fn std_norm_inv(p: f64) -> Option<f64> {
2867    if p <= 0.0 || p >= 1.0 {
2868        return None;
2869    }
2870
2871    // Coefficients for rational approximation
2872    const A: [f64; 6] = [
2873        -3.969683028665376e+01,
2874        2.209460984245205e+02,
2875        -2.759285104469687e+02,
2876        1.383577518672690e+02,
2877        -3.066479806614716e+01,
2878        2.506628277459239e+00,
2879    ];
2880    const B: [f64; 5] = [
2881        -5.447609879822406e+01,
2882        1.615858368580409e+02,
2883        -1.556989798598866e+02,
2884        6.680131188771972e+01,
2885        -1.328068155288572e+01,
2886    ];
2887    const C: [f64; 6] = [
2888        -7.784894002430293e-03,
2889        -3.223964580411365e-01,
2890        -2.400758277161838e+00,
2891        -2.549732539343734e+00,
2892        4.374664141464968e+00,
2893        2.938163982698783e+00,
2894    ];
2895    const D: [f64; 4] = [
2896        7.784695709041462e-03,
2897        3.224671290700398e-01,
2898        2.445134137142996e+00,
2899        3.754408661907416e+00,
2900    ];
2901
2902    const P_LOW: f64 = 0.02425;
2903    const P_HIGH: f64 = 1.0 - P_LOW;
2904
2905    let q = p - 0.5;
2906
2907    if p < P_LOW {
2908        // Lower tail
2909        let r = (-2.0 * p.ln()).sqrt();
2910        let num = ((((C[0] * r + C[1]) * r + C[2]) * r + C[3]) * r + C[4]) * r + C[5];
2911        let den = (((D[0] * r + D[1]) * r + D[2]) * r + D[3]) * r + 1.0;
2912        Some(num / den)
2913    } else if p <= P_HIGH {
2914        // Central region
2915        let r = q * q;
2916        let num = ((((A[0] * r + A[1]) * r + A[2]) * r + A[3]) * r + A[4]) * r + A[5];
2917        let den = ((((B[0] * r + B[1]) * r + B[2]) * r + B[3]) * r + B[4]) * r + 1.0;
2918        Some(q * num / den)
2919    } else {
2920        // Upper tail
2921        let r = (-2.0 * (1.0 - p).ln()).sqrt();
2922        let num = ((((C[0] * r + C[1]) * r + C[2]) * r + C[3]) * r + C[4]) * r + C[5];
2923        let den = (((D[0] * r + D[1]) * r + D[2]) * r + D[3]) * r + 1.0;
2924        Some(-num / den)
2925    }
2926}
2927
2928/// Returns the standard normal probability for a z-score as either a CDF or PDF value.
2929///
2930/// Use `NORM.S.DIST` for z-based probability lookups when the distribution has mean `0` and
2931/// standard deviation `1`.
2932///
2933/// # Remarks
2934/// - Set `cumulative` to a non-zero value for the cumulative distribution `P(Z <= z)`.
2935/// - Set `cumulative` to `0` for the probability density at exactly `z`.
2936/// - Accepts any real-valued `z`; no domain clipping is applied.
2937/// - Invalid numeric coercions propagate as spreadsheet errors.
2938///
2939/// # Examples
2940///
2941/// ```yaml,sandbox
2942/// title: "Standard normal CDF at zero"
2943/// formula: "=NORM.S.DIST(0,TRUE)"
2944/// expected: 0.5
2945/// ```
2946///
2947/// ```yaml,sandbox
2948/// title: "Standard normal PDF at zero"
2949/// formula: "=NORM.S.DIST(0,FALSE)"
2950/// expected: 0.3989422804014327
2951/// ```
2952#[derive(Debug)]
2953pub struct NormSDistFn;
2954/// [formualizer-docgen:schema:start]
2955/// Name: NORM.S.DIST
2956/// Type: NormSDistFn
2957/// Min args: 2
2958/// Max args: 2
2959/// Variadic: false
2960/// Signature: NORM.S.DIST(arg1: number@scalar, arg2: number@scalar)
2961/// 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}
2962/// Caps: PURE
2963/// [formualizer-docgen:schema:end]
2964impl Function for NormSDistFn {
2965    func_caps!(PURE);
2966    fn name(&self) -> &'static str {
2967        "NORM.S.DIST"
2968    }
2969    fn min_args(&self) -> usize {
2970        2
2971    }
2972    fn arg_schema(&self) -> &'static [ArgSchema] {
2973        use std::sync::LazyLock;
2974        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
2975            vec![
2976                ArgSchema::number_lenient_scalar(),
2977                ArgSchema::number_lenient_scalar(),
2978            ]
2979        });
2980        &SCHEMA[..]
2981    }
2982    fn eval<'a, 'b, 'c>(
2983        &self,
2984        args: &'c [ArgumentHandle<'a, 'b>],
2985        _ctx: &dyn FunctionContext<'b>,
2986    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2987        let z = coerce_num(&scalar_like_value(&args[0])?)?;
2988        let cumulative = coerce_num(&scalar_like_value(&args[1])?)? != 0.0;
2989
2990        let result = if cumulative {
2991            std_norm_cdf(z)
2992        } else {
2993            std_norm_pdf(z)
2994        };
2995        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
2996            result,
2997        )))
2998    }
2999}
3000
3001/// Returns the z-score whose standard normal cumulative probability matches `probability`.
3002///
3003/// This is the inverse of `NORM.S.DIST(z, TRUE)` and is commonly used for critical-value
3004/// thresholds.
3005///
3006/// # Remarks
3007/// - `probability` must be strictly between `0` and `1`.
3008/// - Returns `#NUM!` when `probability <= 0` or `probability >= 1`.
3009/// - Output can be negative, zero, or positive depending on which side of `0.5` you query.
3010/// - Invalid numeric coercions propagate as spreadsheet errors.
3011///
3012/// # Examples
3013///
3014/// ```yaml,sandbox
3015/// title: "Median probability maps to zero"
3016/// formula: "=NORM.S.INV(0.5)"
3017/// expected: 0
3018/// ```
3019///
3020/// ```yaml,sandbox
3021/// title: "Upper-tail critical z-score"
3022/// formula: "=NORM.S.INV(0.975)"
3023/// expected: 1.959963986120195
3024/// ```
3025#[derive(Debug)]
3026pub struct NormSInvFn;
3027/// [formualizer-docgen:schema:start]
3028/// Name: NORM.S.INV
3029/// Type: NormSInvFn
3030/// Min args: 1
3031/// Max args: 1
3032/// Variadic: false
3033/// Signature: NORM.S.INV(arg1: number@scalar)
3034/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
3035/// Caps: PURE
3036/// [formualizer-docgen:schema:end]
3037impl Function for NormSInvFn {
3038    func_caps!(PURE);
3039    fn name(&self) -> &'static str {
3040        "NORM.S.INV"
3041    }
3042    fn min_args(&self) -> usize {
3043        1
3044    }
3045    fn arg_schema(&self) -> &'static [ArgSchema] {
3046        use std::sync::LazyLock;
3047        static SCHEMA: LazyLock<Vec<ArgSchema>> =
3048            LazyLock::new(|| vec![ArgSchema::number_lenient_scalar()]);
3049        &SCHEMA[..]
3050    }
3051    fn eval<'a, 'b, 'c>(
3052        &self,
3053        args: &'c [ArgumentHandle<'a, 'b>],
3054        _ctx: &dyn FunctionContext<'b>,
3055    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3056        let p = coerce_num(&scalar_like_value(&args[0])?)?;
3057
3058        match std_norm_inv(p) {
3059            Some(z) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(z))),
3060            None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3061                ExcelError::new_num(),
3062            ))),
3063        }
3064    }
3065}
3066
3067/// Returns the normal-distribution probability at `x` for a given mean and standard deviation.
3068///
3069/// Use `NORM.DIST` for either cumulative probabilities or point density under a non-standard
3070/// normal model.
3071///
3072/// # Remarks
3073/// - Set `cumulative` to non-zero for `P(X <= x)`; set it to `0` for density mode.
3074/// - `standard_dev` must be strictly greater than `0`.
3075/// - Returns `#NUM!` when `standard_dev <= 0`.
3076/// - Invalid numeric coercions propagate as spreadsheet errors.
3077///
3078/// # Examples
3079///
3080/// ```yaml,sandbox
3081/// title: "Normal CDF at the mean"
3082/// formula: "=NORM.DIST(50,50,10,TRUE)"
3083/// expected: 0.5
3084/// ```
3085///
3086/// ```yaml,sandbox
3087/// title: "Normal PDF at the mean"
3088/// formula: "=NORM.DIST(50,50,10,FALSE)"
3089/// expected: 0.03989422804014327
3090/// ```
3091#[derive(Debug)]
3092pub struct NormDistFn;
3093/// [formualizer-docgen:schema:start]
3094/// Name: NORM.DIST
3095/// Type: NormDistFn
3096/// Min args: 4
3097/// Max args: 4
3098/// Variadic: false
3099/// Signature: NORM.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar)
3100/// 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}
3101/// Caps: PURE
3102/// [formualizer-docgen:schema:end]
3103impl Function for NormDistFn {
3104    func_caps!(PURE);
3105    fn name(&self) -> &'static str {
3106        "NORM.DIST"
3107    }
3108    fn min_args(&self) -> usize {
3109        4
3110    }
3111    fn arg_schema(&self) -> &'static [ArgSchema] {
3112        use std::sync::LazyLock;
3113        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
3114            vec![
3115                ArgSchema::number_lenient_scalar(),
3116                ArgSchema::number_lenient_scalar(),
3117                ArgSchema::number_lenient_scalar(),
3118                ArgSchema::number_lenient_scalar(),
3119            ]
3120        });
3121        &SCHEMA[..]
3122    }
3123    fn eval<'a, 'b, 'c>(
3124        &self,
3125        args: &'c [ArgumentHandle<'a, 'b>],
3126        _ctx: &dyn FunctionContext<'b>,
3127    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3128        let x = coerce_num(&scalar_like_value(&args[0])?)?;
3129        let mean = coerce_num(&scalar_like_value(&args[1])?)?;
3130        let std_dev = coerce_num(&scalar_like_value(&args[2])?)?;
3131        let cumulative = coerce_num(&scalar_like_value(&args[3])?)? != 0.0;
3132
3133        if std_dev <= 0.0 {
3134            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3135                ExcelError::new_num(),
3136            )));
3137        }
3138
3139        let z = (x - mean) / std_dev;
3140
3141        let result = if cumulative {
3142            std_norm_cdf(z)
3143        } else {
3144            std_norm_pdf(z) / std_dev
3145        };
3146        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
3147            result,
3148        )))
3149    }
3150}
3151
3152/// Returns the value `x` whose normal cumulative probability equals `probability`.
3153///
3154/// This function is the inverse of `NORM.DIST(x, mean, standard_dev, TRUE)`.
3155///
3156/// # Remarks
3157/// - `probability` must be strictly between `0` and `1`.
3158/// - `standard_dev` must be strictly greater than `0`.
3159/// - Returns `#NUM!` for invalid probability bounds or non-positive standard deviation.
3160/// - Invalid numeric coercions propagate as spreadsheet errors.
3161///
3162/// # Examples
3163///
3164/// ```yaml,sandbox
3165/// title: "Median probability returns the mean"
3166/// formula: "=NORM.INV(0.5,10,2)"
3167/// expected: 10
3168/// ```
3169///
3170/// ```yaml,sandbox
3171/// title: "One-standard-deviation quantile"
3172/// formula: "=NORM.INV(0.841344746068543,0,1)"
3173/// expected: 1
3174/// ```
3175#[derive(Debug)]
3176pub struct NormInvFn;
3177/// [formualizer-docgen:schema:start]
3178/// Name: NORM.INV
3179/// Type: NormInvFn
3180/// Min args: 3
3181/// Max args: 3
3182/// Variadic: false
3183/// Signature: NORM.INV(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
3184/// 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}
3185/// Caps: PURE
3186/// [formualizer-docgen:schema:end]
3187impl Function for NormInvFn {
3188    func_caps!(PURE);
3189    fn name(&self) -> &'static str {
3190        "NORM.INV"
3191    }
3192    fn min_args(&self) -> usize {
3193        3
3194    }
3195    fn arg_schema(&self) -> &'static [ArgSchema] {
3196        use std::sync::LazyLock;
3197        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
3198            vec![
3199                ArgSchema::number_lenient_scalar(),
3200                ArgSchema::number_lenient_scalar(),
3201                ArgSchema::number_lenient_scalar(),
3202            ]
3203        });
3204        &SCHEMA[..]
3205    }
3206    fn eval<'a, 'b, 'c>(
3207        &self,
3208        args: &'c [ArgumentHandle<'a, 'b>],
3209        _ctx: &dyn FunctionContext<'b>,
3210    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3211        let p = coerce_num(&scalar_like_value(&args[0])?)?;
3212        let mean = coerce_num(&scalar_like_value(&args[1])?)?;
3213        let std_dev = coerce_num(&scalar_like_value(&args[2])?)?;
3214
3215        if std_dev <= 0.0 {
3216            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3217                ExcelError::new_num(),
3218            )));
3219        }
3220
3221        match std_norm_inv(p) {
3222            Some(z) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
3223                mean + z * std_dev,
3224            ))),
3225            None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3226                ExcelError::new_num(),
3227            ))),
3228        }
3229    }
3230}
3231
3232/// Returns the log-normal probability at `x` as either a cumulative value or density.
3233///
3234/// `LOGNORM.DIST` models positive-valued variables where `ln(X)` follows a normal distribution.
3235///
3236/// # Remarks
3237/// - Set `cumulative` to non-zero for CDF mode; set it to `0` for PDF mode.
3238/// - Requires `x > 0` and `standard_dev > 0`.
3239/// - Returns `#NUM!` when `x <= 0` or `standard_dev <= 0`.
3240/// - Invalid numeric coercions propagate as spreadsheet errors.
3241///
3242/// # Examples
3243///
3244/// ```yaml,sandbox
3245/// title: "Log-normal CDF at x = 1"
3246/// formula: "=LOGNORM.DIST(1,0,1,TRUE)"
3247/// expected: 0.5
3248/// ```
3249///
3250/// ```yaml,sandbox
3251/// title: "Log-normal PDF at x = 1"
3252/// formula: "=LOGNORM.DIST(1,0,1,FALSE)"
3253/// expected: 0.3989422804014327
3254/// ```
3255#[derive(Debug)]
3256pub struct LognormDistFn;
3257/// [formualizer-docgen:schema:start]
3258/// Name: LOGNORM.DIST
3259/// Type: LognormDistFn
3260/// Min args: 4
3261/// Max args: 4
3262/// Variadic: false
3263/// Signature: LOGNORM.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar)
3264/// 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}
3265/// Caps: PURE
3266/// [formualizer-docgen:schema:end]
3267impl Function for LognormDistFn {
3268    func_caps!(PURE);
3269    fn name(&self) -> &'static str {
3270        "LOGNORM.DIST"
3271    }
3272    fn min_args(&self) -> usize {
3273        4
3274    }
3275    fn arg_schema(&self) -> &'static [ArgSchema] {
3276        use std::sync::LazyLock;
3277        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
3278            vec![
3279                ArgSchema::number_lenient_scalar(),
3280                ArgSchema::number_lenient_scalar(),
3281                ArgSchema::number_lenient_scalar(),
3282                ArgSchema::number_lenient_scalar(),
3283            ]
3284        });
3285        &SCHEMA[..]
3286    }
3287    fn eval<'a, 'b, 'c>(
3288        &self,
3289        args: &'c [ArgumentHandle<'a, 'b>],
3290        _ctx: &dyn FunctionContext<'b>,
3291    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3292        let x = coerce_num(&scalar_like_value(&args[0])?)?;
3293        let mean = coerce_num(&scalar_like_value(&args[1])?)?;
3294        let std_dev = coerce_num(&scalar_like_value(&args[2])?)?;
3295        let cumulative = coerce_num(&scalar_like_value(&args[3])?)? != 0.0;
3296
3297        if x <= 0.0 || std_dev <= 0.0 {
3298            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3299                ExcelError::new_num(),
3300            )));
3301        }
3302
3303        let z = (x.ln() - mean) / std_dev;
3304
3305        let result = if cumulative {
3306            std_norm_cdf(z)
3307        } else {
3308            std_norm_pdf(z) / (x * std_dev)
3309        };
3310        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
3311            result,
3312        )))
3313    }
3314}
3315
3316/// Returns the positive value `x` whose log-normal cumulative probability is `probability`.
3317///
3318/// This function inverts `LOGNORM.DIST(x, mean, standard_dev, TRUE)`.
3319///
3320/// # Remarks
3321/// - `probability` must be strictly between `0` and `1`.
3322/// - `standard_dev` must be strictly greater than `0`.
3323/// - Returns `#NUM!` when inputs violate probability or scale constraints.
3324/// - Invalid numeric coercions propagate as spreadsheet errors.
3325///
3326/// # Examples
3327///
3328/// ```yaml,sandbox
3329/// title: "Median log-normal quantile"
3330/// formula: "=LOGNORM.INV(0.5,0,1)"
3331/// expected: 1
3332/// ```
3333///
3334/// ```yaml,sandbox
3335/// title: "Upper quantile for mean 0 and stdev 1"
3336/// formula: "=LOGNORM.INV(0.841344746068543,0,1)"
3337/// expected: 2.718281828459045
3338/// ```
3339#[derive(Debug)]
3340pub struct LognormInvFn;
3341/// [formualizer-docgen:schema:start]
3342/// Name: LOGNORM.INV
3343/// Type: LognormInvFn
3344/// Min args: 3
3345/// Max args: 3
3346/// Variadic: false
3347/// Signature: LOGNORM.INV(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
3348/// 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}
3349/// Caps: PURE
3350/// [formualizer-docgen:schema:end]
3351impl Function for LognormInvFn {
3352    func_caps!(PURE);
3353    fn name(&self) -> &'static str {
3354        "LOGNORM.INV"
3355    }
3356    fn min_args(&self) -> usize {
3357        3
3358    }
3359    fn arg_schema(&self) -> &'static [ArgSchema] {
3360        use std::sync::LazyLock;
3361        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
3362            vec![
3363                ArgSchema::number_lenient_scalar(),
3364                ArgSchema::number_lenient_scalar(),
3365                ArgSchema::number_lenient_scalar(),
3366            ]
3367        });
3368        &SCHEMA[..]
3369    }
3370    fn eval<'a, 'b, 'c>(
3371        &self,
3372        args: &'c [ArgumentHandle<'a, 'b>],
3373        _ctx: &dyn FunctionContext<'b>,
3374    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3375        let p = coerce_num(&scalar_like_value(&args[0])?)?;
3376        let mean = coerce_num(&scalar_like_value(&args[1])?)?;
3377        let std_dev = coerce_num(&scalar_like_value(&args[2])?)?;
3378
3379        if std_dev <= 0.0 {
3380            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3381                ExcelError::new_num(),
3382            )));
3383        }
3384
3385        match std_norm_inv(p) {
3386            Some(z) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
3387                (mean + z * std_dev).exp(),
3388            ))),
3389            None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3390                ExcelError::new_num(),
3391            ))),
3392        }
3393    }
3394}
3395
3396/// Returns the standard normal probability density at `x`.
3397///
3398/// `PHI` is equivalent to `NORM.S.DIST(x, FALSE)` and is useful in continuous-probability
3399/// calculations.
3400///
3401/// # Remarks
3402/// - Evaluates the density of a standard normal variable centered at `0`.
3403/// - The result is always non-negative and symmetric around `x = 0`.
3404/// - Works for any real input value.
3405/// - Invalid numeric coercions propagate as spreadsheet errors.
3406///
3407/// # Examples
3408///
3409/// ```yaml,sandbox
3410/// title: "Standard normal density at zero"
3411/// formula: "=PHI(0)"
3412/// expected: 0.3989422804014327
3413/// ```
3414///
3415/// ```yaml,sandbox
3416/// title: "Standard normal density at one"
3417/// formula: "=PHI(1)"
3418/// expected: 0.24197072451914337
3419/// ```
3420#[derive(Debug)]
3421pub struct PhiFn;
3422/// [formualizer-docgen:schema:start]
3423/// Name: PHI
3424/// Type: PhiFn
3425/// Min args: 1
3426/// Max args: 1
3427/// Variadic: false
3428/// Signature: PHI(arg1: number@scalar)
3429/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
3430/// Caps: PURE
3431/// [formualizer-docgen:schema:end]
3432impl Function for PhiFn {
3433    func_caps!(PURE);
3434    fn name(&self) -> &'static str {
3435        "PHI"
3436    }
3437    fn min_args(&self) -> usize {
3438        1
3439    }
3440    fn arg_schema(&self) -> &'static [ArgSchema] {
3441        use std::sync::LazyLock;
3442        static SCHEMA: LazyLock<Vec<ArgSchema>> =
3443            LazyLock::new(|| vec![ArgSchema::number_lenient_scalar()]);
3444        &SCHEMA[..]
3445    }
3446    fn eval<'a, 'b, 'c>(
3447        &self,
3448        args: &'c [ArgumentHandle<'a, 'b>],
3449        _ctx: &dyn FunctionContext<'b>,
3450    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3451        let z = coerce_num(&scalar_like_value(&args[0])?)?;
3452        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
3453            std_norm_pdf(z),
3454        )))
3455    }
3456}
3457
3458/// Returns the standard normal area between `0` and `z`.
3459///
3460/// `GAUSS` computes `NORM.S.DIST(z, TRUE) - 0.5`, preserving the sign of `z`.
3461///
3462/// # Remarks
3463/// - Positive `z` returns a positive area; negative `z` returns a negative area.
3464/// - `GAUSS(0)` returns `0`.
3465/// - Output magnitude is always less than `0.5`.
3466/// - Invalid numeric coercions propagate as spreadsheet errors.
3467///
3468/// # Examples
3469///
3470/// ```yaml,sandbox
3471/// title: "Area from mean to z = 1"
3472/// formula: "=GAUSS(1)"
3473/// expected: 0.3413447460685429
3474/// ```
3475///
3476/// ```yaml,sandbox
3477/// title: "Symmetric negative z-value"
3478/// formula: "=GAUSS(-1)"
3479/// expected: -0.3413447460685429
3480/// ```
3481#[derive(Debug)]
3482pub struct GaussFn;
3483/// [formualizer-docgen:schema:start]
3484/// Name: GAUSS
3485/// Type: GaussFn
3486/// Min args: 1
3487/// Max args: 1
3488/// Variadic: false
3489/// Signature: GAUSS(arg1: number@scalar)
3490/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
3491/// Caps: PURE
3492/// [formualizer-docgen:schema:end]
3493impl Function for GaussFn {
3494    func_caps!(PURE);
3495    fn name(&self) -> &'static str {
3496        "GAUSS"
3497    }
3498    fn min_args(&self) -> usize {
3499        1
3500    }
3501    fn arg_schema(&self) -> &'static [ArgSchema] {
3502        use std::sync::LazyLock;
3503        static SCHEMA: LazyLock<Vec<ArgSchema>> =
3504            LazyLock::new(|| vec![ArgSchema::number_lenient_scalar()]);
3505        &SCHEMA[..]
3506    }
3507    fn eval<'a, 'b, 'c>(
3508        &self,
3509        args: &'c [ArgumentHandle<'a, 'b>],
3510        _ctx: &dyn FunctionContext<'b>,
3511    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3512        let z = coerce_num(&scalar_like_value(&args[0])?)?;
3513        // GAUSS(z) = Φ(z) - 0.5
3514        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
3515            std_norm_cdf(z) - 0.5,
3516        )))
3517    }
3518}
3519
3520/// Helper: Log-gamma function
3521#[allow(clippy::excessive_precision)]
3522fn ln_gamma(x: f64) -> f64 {
3523    // Lanczos approximation
3524    const G: f64 = 7.0;
3525    const C: [f64; 9] = [
3526        0.99999999999980993,
3527        676.5203681218851,
3528        -1259.1392167224028,
3529        771.32342877765313,
3530        -176.61502916214059,
3531        12.507343278686905,
3532        -0.13857109526572012,
3533        9.9843695780195716e-6,
3534        1.5056327351493116e-7,
3535    ];
3536
3537    if x < 0.5 {
3538        // Reflection formula
3539        let pi = std::f64::consts::PI;
3540        pi.ln() - (pi * x).sin().ln() - ln_gamma(1.0 - x)
3541    } else {
3542        let x = x - 1.0;
3543        let mut ag = C[0];
3544        for (i, c) in C.iter().enumerate().skip(1) {
3545            ag += c / (x + i as f64);
3546        }
3547        let tmp = x + G + 0.5;
3548        0.5 * (2.0 * std::f64::consts::PI).ln() + (tmp).ln() * (x + 0.5) - tmp + ag.ln()
3549    }
3550}
3551
3552/// Helper: Regularized lower incomplete gamma function P(a, x)
3553fn gamma_p(a: f64, x: f64) -> f64 {
3554    if x < 0.0 || a <= 0.0 {
3555        return 0.0;
3556    }
3557    if x == 0.0 {
3558        return 0.0;
3559    }
3560
3561    // Use series expansion for x < a+1
3562    if x < a + 1.0 {
3563        gamma_series(a, x)
3564    } else {
3565        // Use continued fraction for x >= a+1
3566        1.0 - gamma_cf(a, x)
3567    }
3568}
3569
3570/// Helper: Series expansion for incomplete gamma
3571fn gamma_series(a: f64, x: f64) -> f64 {
3572    let ln_ga = ln_gamma(a);
3573    let mut sum = 1.0 / a;
3574    let mut term = sum;
3575    for n in 1..200 {
3576        term *= x / (a + n as f64);
3577        sum += term;
3578        if term.abs() < sum.abs() * 1e-15 {
3579            break;
3580        }
3581    }
3582    sum * (-x + a * x.ln() - ln_ga).exp()
3583}
3584
3585/// Helper: Continued fraction for upper incomplete gamma Q(a,x)
3586/// Using modified Lentz's algorithm (Numerical Recipes formulation)
3587fn gamma_cf(a: f64, x: f64) -> f64 {
3588    let ln_ga = ln_gamma(a);
3589    const TINY: f64 = 1e-30;
3590    const EPS: f64 = 1e-14;
3591
3592    // Set up for evaluating continued fraction by modified Lentz's method
3593    let mut b = x + 1.0 - a;
3594    let mut c = 1.0 / TINY;
3595    let mut d = 1.0 / b;
3596    let mut h = d;
3597
3598    for i in 1..=200 {
3599        let an = -(i as f64) * (i as f64 - a);
3600        b += 2.0;
3601        d = an * d + b;
3602        if d.abs() < TINY {
3603            d = TINY;
3604        }
3605        c = b + an / c;
3606        if c.abs() < TINY {
3607            c = TINY;
3608        }
3609        d = 1.0 / d;
3610        let delta = d * c;
3611        h *= delta;
3612        if (delta - 1.0).abs() <= EPS {
3613            break;
3614        }
3615    }
3616
3617    h * (-x + a * x.ln() - ln_ga).exp()
3618}
3619
3620/// Helper: Regularized incomplete beta function I_x(a,b)
3621/// Uses the continued fraction representation (NIST DLMF 8.17.22)
3622fn beta_i(x: f64, a: f64, b: f64) -> f64 {
3623    if x <= 0.0 {
3624        return 0.0;
3625    }
3626    if x >= 1.0 {
3627        return 1.0;
3628    }
3629    if a <= 0.0 || b <= 0.0 {
3630        return f64::NAN;
3631    }
3632
3633    // Use symmetry for better convergence: I_x(a,b) = 1 - I_{1-x}(b,a)
3634    if x > (a + 1.0) / (a + b + 2.0) {
3635        return 1.0 - beta_i(1.0 - x, b, a);
3636    }
3637
3638    // Compute the prefactor: x^a * (1-x)^b / (a * B(a,b))
3639    let ln_beta = ln_gamma(a) + ln_gamma(b) - ln_gamma(a + b);
3640    let ln_prefactor = a * x.ln() + b * (1.0 - x).ln() - ln_beta - a.ln();
3641    let prefactor = ln_prefactor.exp();
3642
3643    // Evaluate the continued fraction using modified Lentz algorithm
3644    // The CF is: 1 / (1 + d1/(1 + d2/(1 + ...)))
3645    // where d_{2m+1} = -(a+m)(a+b+m)x / ((a+2m)(a+2m+1))
3646    //       d_{2m}   = m(b-m)x / ((a+2m-1)(a+2m))
3647    const EPS: f64 = 1e-14;
3648    const TINY: f64 = 1e-30;
3649
3650    let qab = a + b;
3651    let qap = a + 1.0;
3652    let qam = a - 1.0;
3653    let mut c = 1.0;
3654    let mut d = 1.0 - qab * x / qap;
3655    if d.abs() < TINY {
3656        d = TINY;
3657    }
3658    d = 1.0 / d;
3659    let mut h = d;
3660
3661    for m in 1..=200 {
3662        let m_f64 = m as f64;
3663        let m2 = 2.0 * m_f64;
3664
3665        // Even step: d_{2m} = m(b-m)x / ((a+2m-1)(a+2m))
3666        let aa = m_f64 * (b - m_f64) * x / ((qam + m2) * (a + m2));
3667        d = 1.0 + aa * d;
3668        if d.abs() < TINY {
3669            d = TINY;
3670        }
3671        c = 1.0 + aa / c;
3672        if c.abs() < TINY {
3673            c = TINY;
3674        }
3675        d = 1.0 / d;
3676        h *= d * c;
3677
3678        // Odd step: d_{2m+1} = -(a+m)(a+b+m)x / ((a+2m)(a+2m+1))
3679        let aa = -((a + m_f64) * (qab + m_f64) * x) / ((a + m2) * (qap + m2));
3680        d = 1.0 + aa * d;
3681        if d.abs() < TINY {
3682            d = TINY;
3683        }
3684        c = 1.0 + aa / c;
3685        if c.abs() < TINY {
3686            c = TINY;
3687        }
3688        d = 1.0 / d;
3689        let delta = d * c;
3690        h *= delta;
3691
3692        if (delta - 1.0).abs() <= EPS {
3693            break;
3694        }
3695    }
3696
3697    prefactor * h
3698}
3699
3700/// Helper: T distribution CDF
3701fn t_cdf(t: f64, df: f64) -> f64 {
3702    let x = df / (df + t * t);
3703    0.5 * (1.0 + t.signum() * (1.0 - beta_i(x, df / 2.0, 0.5)))
3704}
3705
3706/// Helper: T distribution inverse CDF using Newton-Raphson
3707fn t_inv(p: f64, df: f64) -> Option<f64> {
3708    if p <= 0.0 || p >= 1.0 {
3709        return None;
3710    }
3711
3712    // Initial guess using normal approximation
3713    let mut t = std_norm_inv(p)?;
3714
3715    // Newton-Raphson iteration
3716    for _ in 0..50 {
3717        let cdf = t_cdf(t, df);
3718        let pdf = t_pdf(t, df);
3719        if pdf.abs() < 1e-30 {
3720            break;
3721        }
3722        let delta = (cdf - p) / pdf;
3723        t -= delta;
3724        if delta.abs() < 1e-12 {
3725            break;
3726        }
3727    }
3728
3729    Some(t)
3730}
3731
3732/// Helper: T distribution PDF
3733fn t_pdf(t: f64, df: f64) -> f64 {
3734    let coef =
3735        (ln_gamma((df + 1.0) / 2.0) - ln_gamma(df / 2.0) - 0.5 * (df * std::f64::consts::PI).ln())
3736            .exp();
3737    coef * (1.0 + t * t / df).powf(-(df + 1.0) / 2.0)
3738}
3739
3740/// Helper: Chi-square CDF
3741fn chisq_cdf(x: f64, df: f64) -> f64 {
3742    if x <= 0.0 {
3743        return 0.0;
3744    }
3745    gamma_p(df / 2.0, x / 2.0)
3746}
3747
3748/// Helper: Chi-square inverse CDF using Newton-Raphson
3749fn chisq_inv(p: f64, df: f64) -> Option<f64> {
3750    if p <= 0.0 || p >= 1.0 {
3751        return None;
3752    }
3753
3754    // Initial guess
3755    let mut x = df.max(1.0);
3756    if p < 0.5 {
3757        x = x.min(1.0);
3758    }
3759
3760    // Newton-Raphson iteration
3761    for _ in 0..100 {
3762        let cdf = chisq_cdf(x, df);
3763        let pdf = chisq_pdf(x, df);
3764        if pdf.abs() < 1e-30 {
3765            break;
3766        }
3767        let delta = (cdf - p) / pdf;
3768        let new_x = (x - delta).max(1e-15);
3769        if (new_x - x).abs() < 1e-12 * x {
3770            x = new_x;
3771            break;
3772        }
3773        x = new_x;
3774    }
3775
3776    Some(x)
3777}
3778
3779/// Helper: Chi-square PDF
3780fn chisq_pdf(x: f64, df: f64) -> f64 {
3781    if x <= 0.0 {
3782        return 0.0;
3783    }
3784    let k = df / 2.0;
3785    ((k - 1.0) * x.ln() - x / 2.0 - k * 2.0_f64.ln() - ln_gamma(k)).exp()
3786}
3787
3788/// Helper: F distribution CDF
3789fn f_cdf(f: f64, d1: f64, d2: f64) -> f64 {
3790    if f <= 0.0 {
3791        return 0.0;
3792    }
3793    let x = d1 * f / (d1 * f + d2);
3794    beta_i(x, d1 / 2.0, d2 / 2.0)
3795}
3796
3797/// Helper: F distribution inverse CDF using Newton-Raphson
3798fn f_inv(p: f64, d1: f64, d2: f64) -> Option<f64> {
3799    if p <= 0.0 || p >= 1.0 {
3800        return None;
3801    }
3802
3803    // Initial guess
3804    let mut f = 1.0;
3805
3806    // Newton-Raphson iteration
3807    for _ in 0..100 {
3808        let cdf = f_cdf(f, d1, d2);
3809        let pdf = f_pdf(f, d1, d2);
3810        if pdf.abs() < 1e-30 {
3811            break;
3812        }
3813        let delta = (cdf - p) / pdf;
3814        let new_f = (f - delta).max(1e-15);
3815        if (new_f - f).abs() < 1e-12 * f {
3816            f = new_f;
3817            break;
3818        }
3819        f = new_f;
3820    }
3821
3822    Some(f)
3823}
3824
3825/// Helper: F distribution PDF
3826fn f_pdf(f: f64, d1: f64, d2: f64) -> f64 {
3827    if f <= 0.0 {
3828        return 0.0;
3829    }
3830    let ln_beta = ln_gamma(d1 / 2.0) + ln_gamma(d2 / 2.0) - ln_gamma((d1 + d2) / 2.0);
3831    let coef = (d1 / 2.0) * (d1 / d2).ln() + (d1 / 2.0 - 1.0) * f.ln()
3832        - ((d1 + d2) / 2.0) * (1.0 + d1 * f / d2).ln()
3833        - ln_beta;
3834    coef.exp()
3835}
3836
3837/// Returns the Student's t probability for `x` and a given degrees-of-freedom value.
3838///
3839/// Use `T.DIST` in either cumulative mode (left-tail probability) or density mode.
3840///
3841/// # Remarks
3842/// - Set `cumulative` to non-zero for CDF mode, or `0` for PDF mode.
3843/// - `deg_freedom` must be at least `1`.
3844/// - Returns `#NUM!` when `deg_freedom < 1`.
3845/// - Invalid numeric coercions propagate as spreadsheet errors.
3846///
3847/// # Examples
3848///
3849/// ```yaml,sandbox
3850/// title: "t CDF at zero"
3851/// formula: "=T.DIST(0,10,TRUE)"
3852/// expected: 0.5
3853/// ```
3854///
3855/// ```yaml,sandbox
3856/// title: "t PDF at zero"
3857/// formula: "=T.DIST(0,10,FALSE)"
3858/// expected: 0.389108383966031
3859/// ```
3860#[derive(Debug)]
3861pub struct TDistFn;
3862/// [formualizer-docgen:schema:start]
3863/// Name: T.DIST
3864/// Type: TDistFn
3865/// Min args: 3
3866/// Max args: 3
3867/// Variadic: false
3868/// Signature: T.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
3869/// 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}
3870/// Caps: PURE
3871/// [formualizer-docgen:schema:end]
3872impl Function for TDistFn {
3873    func_caps!(PURE);
3874    fn name(&self) -> &'static str {
3875        "T.DIST"
3876    }
3877    fn min_args(&self) -> usize {
3878        3
3879    }
3880    fn arg_schema(&self) -> &'static [ArgSchema] {
3881        use std::sync::LazyLock;
3882        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
3883            vec![
3884                ArgSchema::number_lenient_scalar(),
3885                ArgSchema::number_lenient_scalar(),
3886                ArgSchema::number_lenient_scalar(),
3887            ]
3888        });
3889        &SCHEMA[..]
3890    }
3891    fn eval<'a, 'b, 'c>(
3892        &self,
3893        args: &'c [ArgumentHandle<'a, 'b>],
3894        _ctx: &dyn FunctionContext<'b>,
3895    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3896        let x = coerce_num(&scalar_like_value(&args[0])?)?;
3897        let df = coerce_num(&scalar_like_value(&args[1])?)?;
3898        let cumulative = coerce_num(&scalar_like_value(&args[2])?)? != 0.0;
3899
3900        if df < 1.0 {
3901            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3902                ExcelError::new_num(),
3903            )));
3904        }
3905
3906        let result = if cumulative {
3907            t_cdf(x, df)
3908        } else {
3909            t_pdf(x, df)
3910        };
3911        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
3912            result,
3913        )))
3914    }
3915}
3916
3917/// Returns the t-value whose left-tail probability equals `probability`.
3918///
3919/// `T.INV` is the inverse of `T.DIST(x, deg_freedom, TRUE)`.
3920///
3921/// # Remarks
3922/// - `probability` must be strictly between `0` and `1`.
3923/// - `deg_freedom` must be at least `1`.
3924/// - Returns `#NUM!` for out-of-range probability or invalid degrees of freedom.
3925/// - Invalid numeric coercions propagate as spreadsheet errors.
3926///
3927/// # Examples
3928///
3929/// ```yaml,sandbox
3930/// title: "Median t quantile"
3931/// formula: "=T.INV(0.5,10)"
3932/// expected: 0
3933/// ```
3934///
3935/// ```yaml,sandbox
3936/// title: "Upper-tail critical value"
3937/// formula: "=T.INV(0.975,10)"
3938/// expected: 2.228138851986273
3939/// ```
3940#[derive(Debug)]
3941pub struct TInvFn;
3942/// [formualizer-docgen:schema:start]
3943/// Name: T.INV
3944/// Type: TInvFn
3945/// Min args: 2
3946/// Max args: 2
3947/// Variadic: false
3948/// Signature: T.INV(arg1: number@scalar, arg2: number@scalar)
3949/// 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}
3950/// Caps: PURE
3951/// [formualizer-docgen:schema:end]
3952impl Function for TInvFn {
3953    func_caps!(PURE);
3954    fn name(&self) -> &'static str {
3955        "T.INV"
3956    }
3957    fn min_args(&self) -> usize {
3958        2
3959    }
3960    fn arg_schema(&self) -> &'static [ArgSchema] {
3961        use std::sync::LazyLock;
3962        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
3963            vec![
3964                ArgSchema::number_lenient_scalar(),
3965                ArgSchema::number_lenient_scalar(),
3966            ]
3967        });
3968        &SCHEMA[..]
3969    }
3970    fn eval<'a, 'b, 'c>(
3971        &self,
3972        args: &'c [ArgumentHandle<'a, 'b>],
3973        _ctx: &dyn FunctionContext<'b>,
3974    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3975        let p = coerce_num(&scalar_like_value(&args[0])?)?;
3976        let df = coerce_num(&scalar_like_value(&args[1])?)?;
3977
3978        if df < 1.0 {
3979            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3980                ExcelError::new_num(),
3981            )));
3982        }
3983
3984        match t_inv(p, df) {
3985            Some(result) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
3986                result,
3987            ))),
3988            None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3989                ExcelError::new_num(),
3990            ))),
3991        }
3992    }
3993}
3994
3995/// Returns the chi-square probability for `x` with the specified degrees of freedom.
3996///
3997/// Use `CHISQ.DIST` in cumulative mode for left-tail probability or density mode for the PDF.
3998///
3999/// # Remarks
4000/// - Set `cumulative` to non-zero for CDF mode, or `0` for PDF mode.
4001/// - Requires `x >= 0` and `deg_freedom >= 1`.
4002/// - Returns `#NUM!` for negative `x` or invalid degrees of freedom.
4003/// - Invalid numeric coercions propagate as spreadsheet errors.
4004///
4005/// # Examples
4006///
4007/// ```yaml,sandbox
4008/// title: "Chi-square CDF at zero"
4009/// formula: "=CHISQ.DIST(0,4,TRUE)"
4010/// expected: 0
4011/// ```
4012///
4013/// ```yaml,sandbox
4014/// title: "Chi-square PDF example"
4015/// formula: "=CHISQ.DIST(2,2,FALSE)"
4016/// expected: 0.18393972058572117
4017/// ```
4018#[derive(Debug)]
4019pub struct ChisqDistFn;
4020/// [formualizer-docgen:schema:start]
4021/// Name: CHISQ.DIST
4022/// Type: ChisqDistFn
4023/// Min args: 3
4024/// Max args: 3
4025/// Variadic: false
4026/// Signature: CHISQ.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
4027/// 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}
4028/// Caps: PURE
4029/// [formualizer-docgen:schema:end]
4030impl Function for ChisqDistFn {
4031    func_caps!(PURE);
4032    fn name(&self) -> &'static str {
4033        "CHISQ.DIST"
4034    }
4035    fn min_args(&self) -> usize {
4036        3
4037    }
4038    fn arg_schema(&self) -> &'static [ArgSchema] {
4039        use std::sync::LazyLock;
4040        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
4041            vec![
4042                ArgSchema::number_lenient_scalar(),
4043                ArgSchema::number_lenient_scalar(),
4044                ArgSchema::number_lenient_scalar(),
4045            ]
4046        });
4047        &SCHEMA[..]
4048    }
4049    fn eval<'a, 'b, 'c>(
4050        &self,
4051        args: &'c [ArgumentHandle<'a, 'b>],
4052        _ctx: &dyn FunctionContext<'b>,
4053    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4054        let x = coerce_num(&scalar_like_value(&args[0])?)?;
4055        let df = coerce_num(&scalar_like_value(&args[1])?)?;
4056        let cumulative = coerce_num(&scalar_like_value(&args[2])?)? != 0.0;
4057
4058        if df < 1.0 || x < 0.0 {
4059            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4060                ExcelError::new_num(),
4061            )));
4062        }
4063
4064        let result = if cumulative {
4065            chisq_cdf(x, df)
4066        } else {
4067            chisq_pdf(x, df)
4068        };
4069        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
4070            result,
4071        )))
4072    }
4073}
4074
4075/// Returns the chi-square value whose left-tail probability is `probability`.
4076///
4077/// `CHISQ.INV` inverts `CHISQ.DIST(x, deg_freedom, TRUE)`.
4078///
4079/// # Remarks
4080/// - `probability` must be strictly between `0` and `1`.
4081/// - `deg_freedom` must be at least `1`.
4082/// - Returns `#NUM!` when arguments are outside valid ranges.
4083/// - Invalid numeric coercions propagate as spreadsheet errors.
4084///
4085/// # Examples
4086///
4087/// ```yaml,sandbox
4088/// title: "Median chi-square quantile for df=2"
4089/// formula: "=CHISQ.INV(0.5,2)"
4090/// expected: 1.3862943611198906
4091/// ```
4092///
4093/// ```yaml,sandbox
4094/// title: "Upper quantile for df=10"
4095/// formula: "=CHISQ.INV(0.95,10)"
4096/// expected: 18.307038053275146
4097/// ```
4098#[derive(Debug)]
4099pub struct ChisqInvFn;
4100/// [formualizer-docgen:schema:start]
4101/// Name: CHISQ.INV
4102/// Type: ChisqInvFn
4103/// Min args: 2
4104/// Max args: 2
4105/// Variadic: false
4106/// Signature: CHISQ.INV(arg1: number@scalar, arg2: number@scalar)
4107/// 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}
4108/// Caps: PURE
4109/// [formualizer-docgen:schema:end]
4110impl Function for ChisqInvFn {
4111    func_caps!(PURE);
4112    fn name(&self) -> &'static str {
4113        "CHISQ.INV"
4114    }
4115    fn min_args(&self) -> usize {
4116        2
4117    }
4118    fn arg_schema(&self) -> &'static [ArgSchema] {
4119        use std::sync::LazyLock;
4120        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
4121            vec![
4122                ArgSchema::number_lenient_scalar(),
4123                ArgSchema::number_lenient_scalar(),
4124            ]
4125        });
4126        &SCHEMA[..]
4127    }
4128    fn eval<'a, 'b, 'c>(
4129        &self,
4130        args: &'c [ArgumentHandle<'a, 'b>],
4131        _ctx: &dyn FunctionContext<'b>,
4132    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4133        let p = coerce_num(&scalar_like_value(&args[0])?)?;
4134        let df = coerce_num(&scalar_like_value(&args[1])?)?;
4135
4136        if df < 1.0 {
4137            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4138                ExcelError::new_num(),
4139            )));
4140        }
4141
4142        match chisq_inv(p, df) {
4143            Some(result) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
4144                result,
4145            ))),
4146            None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4147                ExcelError::new_num(),
4148            ))),
4149        }
4150    }
4151}
4152
4153/// Returns the F-distribution probability for `x` with numerator and denominator degrees of freedom.
4154///
4155/// Use `F.DIST` for left-tail cumulative probabilities or density values in variance-ratio tests.
4156///
4157/// # Remarks
4158/// - Set `cumulative` to non-zero for CDF mode, or `0` for PDF mode.
4159/// - Requires `x >= 0`, `deg_freedom1 >= 1`, and `deg_freedom2 >= 1`.
4160/// - Returns `#NUM!` when any domain constraint is violated.
4161/// - Invalid numeric coercions propagate as spreadsheet errors.
4162///
4163/// # Examples
4164///
4165/// ```yaml,sandbox
4166/// title: "F CDF with symmetric 2 and 2 degrees of freedom"
4167/// formula: "=F.DIST(1,2,2,TRUE)"
4168/// expected: 0.5
4169/// ```
4170///
4171/// ```yaml,sandbox
4172/// title: "F PDF with symmetric 2 and 2 degrees of freedom"
4173/// formula: "=F.DIST(1,2,2,FALSE)"
4174/// expected: 0.25
4175/// ```
4176#[derive(Debug)]
4177pub struct FDistFn;
4178/// [formualizer-docgen:schema:start]
4179/// Name: F.DIST
4180/// Type: FDistFn
4181/// Min args: 4
4182/// Max args: 4
4183/// Variadic: false
4184/// Signature: F.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar)
4185/// 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}
4186/// Caps: PURE
4187/// [formualizer-docgen:schema:end]
4188impl Function for FDistFn {
4189    func_caps!(PURE);
4190    fn name(&self) -> &'static str {
4191        "F.DIST"
4192    }
4193    fn min_args(&self) -> usize {
4194        4
4195    }
4196    fn arg_schema(&self) -> &'static [ArgSchema] {
4197        use std::sync::LazyLock;
4198        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
4199            vec![
4200                ArgSchema::number_lenient_scalar(),
4201                ArgSchema::number_lenient_scalar(),
4202                ArgSchema::number_lenient_scalar(),
4203                ArgSchema::number_lenient_scalar(),
4204            ]
4205        });
4206        &SCHEMA[..]
4207    }
4208    fn eval<'a, 'b, 'c>(
4209        &self,
4210        args: &'c [ArgumentHandle<'a, 'b>],
4211        _ctx: &dyn FunctionContext<'b>,
4212    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4213        let x = coerce_num(&scalar_like_value(&args[0])?)?;
4214        let d1 = coerce_num(&scalar_like_value(&args[1])?)?;
4215        let d2 = coerce_num(&scalar_like_value(&args[2])?)?;
4216        let cumulative = coerce_num(&scalar_like_value(&args[3])?)? != 0.0;
4217
4218        if d1 < 1.0 || d2 < 1.0 || x < 0.0 {
4219            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4220                ExcelError::new_num(),
4221            )));
4222        }
4223
4224        let result = if cumulative {
4225            f_cdf(x, d1, d2)
4226        } else {
4227            f_pdf(x, d1, d2)
4228        };
4229        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
4230            result,
4231        )))
4232    }
4233}
4234
4235/// Returns the F value whose left-tail probability equals `probability`.
4236///
4237/// `F.INV` inverts `F.DIST(x, deg_freedom1, deg_freedom2, TRUE)`.
4238///
4239/// # Remarks
4240/// - `probability` must be strictly between `0` and `1`.
4241/// - `deg_freedom1` and `deg_freedom2` must each be at least `1`.
4242/// - Returns `#NUM!` for invalid probability or degree-of-freedom arguments.
4243/// - Invalid numeric coercions propagate as spreadsheet errors.
4244///
4245/// # Examples
4246///
4247/// ```yaml,sandbox
4248/// title: "Median F quantile with symmetric 2 and 2 degrees of freedom"
4249/// formula: "=F.INV(0.5,2,2)"
4250/// expected: 1
4251/// ```
4252///
4253/// ```yaml,sandbox
4254/// title: "Upper-tail F critical value"
4255/// formula: "=F.INV(0.95,5,10)"
4256/// expected: 3.3258345304130112
4257/// ```
4258#[derive(Debug)]
4259pub struct FInvFn;
4260/// [formualizer-docgen:schema:start]
4261/// Name: F.INV
4262/// Type: FInvFn
4263/// Min args: 3
4264/// Max args: 3
4265/// Variadic: false
4266/// Signature: F.INV(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
4267/// 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}
4268/// Caps: PURE
4269/// [formualizer-docgen:schema:end]
4270impl Function for FInvFn {
4271    func_caps!(PURE);
4272    fn name(&self) -> &'static str {
4273        "F.INV"
4274    }
4275    fn min_args(&self) -> usize {
4276        3
4277    }
4278    fn arg_schema(&self) -> &'static [ArgSchema] {
4279        use std::sync::LazyLock;
4280        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
4281            vec![
4282                ArgSchema::number_lenient_scalar(),
4283                ArgSchema::number_lenient_scalar(),
4284                ArgSchema::number_lenient_scalar(),
4285            ]
4286        });
4287        &SCHEMA[..]
4288    }
4289    fn eval<'a, 'b, 'c>(
4290        &self,
4291        args: &'c [ArgumentHandle<'a, 'b>],
4292        _ctx: &dyn FunctionContext<'b>,
4293    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4294        let p = coerce_num(&scalar_like_value(&args[0])?)?;
4295        let d1 = coerce_num(&scalar_like_value(&args[1])?)?;
4296        let d2 = coerce_num(&scalar_like_value(&args[2])?)?;
4297
4298        if d1 < 1.0 || d2 < 1.0 {
4299            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4300                ExcelError::new_num(),
4301            )));
4302        }
4303
4304        match f_inv(p, d1, d2) {
4305            Some(result) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
4306                result,
4307            ))),
4308            None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4309                ExcelError::new_num(),
4310            ))),
4311        }
4312    }
4313}
4314
4315/// Returns the z-score of `x` relative to a mean and standard deviation.
4316///
4317/// `STANDARDIZE` computes `(x - mean) / standard_dev`.
4318///
4319/// # Remarks
4320/// - `standard_dev` must be strictly greater than `0`.
4321/// - Returns `#NUM!` when `standard_dev <= 0`.
4322/// - Positive output means `x` is above the mean; negative output means below.
4323/// - Invalid numeric coercions propagate as spreadsheet errors.
4324///
4325/// # Examples
4326///
4327/// ```yaml,sandbox
4328/// title: "One standard deviation above the mean"
4329/// formula: "=STANDARDIZE(42,40,2)"
4330/// expected: 1
4331/// ```
4332///
4333/// ```yaml,sandbox
4334/// title: "Exactly at the mean"
4335/// formula: "=STANDARDIZE(100,100,10)"
4336/// expected: 0
4337/// ```
4338#[derive(Debug)]
4339pub struct StandardizeFn;
4340/// [formualizer-docgen:schema:start]
4341/// Name: STANDARDIZE
4342/// Type: StandardizeFn
4343/// Min args: 3
4344/// Max args: 3
4345/// Variadic: false
4346/// Signature: STANDARDIZE(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
4347/// 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}
4348/// Caps: PURE
4349/// [formualizer-docgen:schema:end]
4350impl Function for StandardizeFn {
4351    func_caps!(PURE);
4352    fn name(&self) -> &'static str {
4353        "STANDARDIZE"
4354    }
4355    fn min_args(&self) -> usize {
4356        3
4357    }
4358    fn arg_schema(&self) -> &'static [ArgSchema] {
4359        use std::sync::LazyLock;
4360        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
4361            vec![
4362                ArgSchema::number_lenient_scalar(),
4363                ArgSchema::number_lenient_scalar(),
4364                ArgSchema::number_lenient_scalar(),
4365            ]
4366        });
4367        &SCHEMA[..]
4368    }
4369    fn eval<'a, 'b, 'c>(
4370        &self,
4371        args: &'c [ArgumentHandle<'a, 'b>],
4372        _ctx: &dyn FunctionContext<'b>,
4373    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4374        let x = coerce_num(&scalar_like_value(&args[0])?)?;
4375        let mean = coerce_num(&scalar_like_value(&args[1])?)?;
4376        let std_dev = coerce_num(&scalar_like_value(&args[2])?)?;
4377
4378        if std_dev <= 0.0 {
4379            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4380                ExcelError::new_num(),
4381            )));
4382        }
4383
4384        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
4385            (x - mean) / std_dev,
4386        )))
4387    }
4388}
4389
4390/// Helper: Factorial function
4391fn factorial(n: i64) -> f64 {
4392    if n < 0 {
4393        return f64::NAN;
4394    }
4395    if n <= 1 {
4396        return 1.0;
4397    }
4398    // For large n, use gamma function: n! = Gamma(n+1)
4399    if n > 20 {
4400        return ln_gamma((n + 1) as f64).exp();
4401    }
4402    let mut result = 1.0;
4403    for i in 2..=n {
4404        result *= i as f64;
4405    }
4406    result
4407}
4408
4409/// Helper: Log of binomial coefficient (n choose k)
4410fn ln_binom(n: i64, k: i64) -> f64 {
4411    if k < 0 || k > n {
4412        return f64::NEG_INFINITY;
4413    }
4414    if k == 0 || k == n {
4415        return 0.0;
4416    }
4417    ln_gamma((n + 1) as f64) - ln_gamma((k + 1) as f64) - ln_gamma((n - k + 1) as f64)
4418}
4419
4420/// Returns the binomial probability for a count of successes across independent trials.
4421///
4422/// Use `BINOM.DIST` to evaluate either exact-success probability (PMF) or cumulative probability
4423/// up to a success count (CDF).
4424///
4425/// # Remarks
4426/// - `number_s` and `trials` are truncated to integers.
4427/// - Requires `0 <= number_s <= trials`, `trials >= 0`, and `0 <= probability_s <= 1`.
4428/// - Set `cumulative` to non-zero for CDF mode, or `0` for PMF mode.
4429/// - Returns `#NUM!` for invalid count or probability ranges.
4430///
4431/// # Examples
4432///
4433/// ```yaml,sandbox
4434/// title: "Binomial PMF for exactly 3 successes"
4435/// formula: "=BINOM.DIST(3,10,0.5,FALSE)"
4436/// expected: 0.1171875
4437/// ```
4438///
4439/// ```yaml,sandbox
4440/// title: "Binomial CDF for at most 3 successes"
4441/// formula: "=BINOM.DIST(3,10,0.5,TRUE)"
4442/// expected: 0.171875
4443/// ```
4444#[derive(Debug)]
4445pub struct BinomDistFn;
4446/// [formualizer-docgen:schema:start]
4447/// Name: BINOM.DIST
4448/// Type: BinomDistFn
4449/// Min args: 4
4450/// Max args: 4
4451/// Variadic: false
4452/// Signature: BINOM.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar)
4453/// 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}
4454/// Caps: PURE
4455/// [formualizer-docgen:schema:end]
4456impl Function for BinomDistFn {
4457    func_caps!(PURE);
4458    fn name(&self) -> &'static str {
4459        "BINOM.DIST"
4460    }
4461    fn min_args(&self) -> usize {
4462        4
4463    }
4464    fn arg_schema(&self) -> &'static [ArgSchema] {
4465        use std::sync::LazyLock;
4466        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
4467            vec![
4468                ArgSchema::number_lenient_scalar(),
4469                ArgSchema::number_lenient_scalar(),
4470                ArgSchema::number_lenient_scalar(),
4471                ArgSchema::number_lenient_scalar(),
4472            ]
4473        });
4474        &SCHEMA[..]
4475    }
4476    fn eval<'a, 'b, 'c>(
4477        &self,
4478        args: &'c [ArgumentHandle<'a, 'b>],
4479        _ctx: &dyn FunctionContext<'b>,
4480    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4481        let k = coerce_num(&scalar_like_value(&args[0])?)?.trunc() as i64;
4482        let n = coerce_num(&scalar_like_value(&args[1])?)?.trunc() as i64;
4483        let p = coerce_num(&scalar_like_value(&args[2])?)?;
4484        let cumulative = coerce_num(&scalar_like_value(&args[3])?)? != 0.0;
4485
4486        if n < 0 || k < 0 || k > n || !(0.0..=1.0).contains(&p) {
4487            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4488                ExcelError::new_num(),
4489            )));
4490        }
4491
4492        let result = if cumulative {
4493            // CDF: sum from i=0 to k of P(X=i)
4494            let mut sum = 0.0;
4495            for i in 0..=k {
4496                let ln_prob =
4497                    ln_binom(n, i) + (i as f64) * p.ln() + ((n - i) as f64) * (1.0 - p).ln();
4498                sum += ln_prob.exp();
4499            }
4500            sum
4501        } else {
4502            // PMF: P(X=k)
4503            let ln_prob = ln_binom(n, k) + (k as f64) * p.ln() + ((n - k) as f64) * (1.0 - p).ln();
4504            ln_prob.exp()
4505        };
4506
4507        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
4508            result,
4509        )))
4510    }
4511}
4512
4513/// Returns the Poisson probability for event count `x` at average rate `mean`.
4514///
4515/// `POISSON.DIST` supports exact-count mode (PMF) and cumulative mode (CDF).
4516///
4517/// # Remarks
4518/// - `x` is truncated to an integer and must be at least `0`.
4519/// - `mean` must be non-negative.
4520/// - Set `cumulative` to non-zero for CDF mode, or `0` for PMF mode.
4521/// - Returns `#NUM!` for negative counts or negative mean values.
4522///
4523/// # Examples
4524///
4525/// ```yaml,sandbox
4526/// title: "Poisson PMF for zero events"
4527/// formula: "=POISSON.DIST(0,2,FALSE)"
4528/// expected: 0.1353352832366127
4529/// ```
4530///
4531/// ```yaml,sandbox
4532/// title: "Poisson CDF up to two events"
4533/// formula: "=POISSON.DIST(2,2,TRUE)"
4534/// expected: 0.6766764161830634
4535/// ```
4536#[derive(Debug)]
4537pub struct PoissonDistFn;
4538/// [formualizer-docgen:schema:start]
4539/// Name: POISSON.DIST
4540/// Type: PoissonDistFn
4541/// Min args: 3
4542/// Max args: 3
4543/// Variadic: false
4544/// Signature: POISSON.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
4545/// 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}
4546/// Caps: PURE
4547/// [formualizer-docgen:schema:end]
4548impl Function for PoissonDistFn {
4549    func_caps!(PURE);
4550    fn name(&self) -> &'static str {
4551        "POISSON.DIST"
4552    }
4553    fn min_args(&self) -> usize {
4554        3
4555    }
4556    fn arg_schema(&self) -> &'static [ArgSchema] {
4557        use std::sync::LazyLock;
4558        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
4559            vec![
4560                ArgSchema::number_lenient_scalar(),
4561                ArgSchema::number_lenient_scalar(),
4562                ArgSchema::number_lenient_scalar(),
4563            ]
4564        });
4565        &SCHEMA[..]
4566    }
4567    fn eval<'a, 'b, 'c>(
4568        &self,
4569        args: &'c [ArgumentHandle<'a, 'b>],
4570        _ctx: &dyn FunctionContext<'b>,
4571    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4572        let k = coerce_num(&scalar_like_value(&args[0])?)?.trunc() as i64;
4573        let lambda = coerce_num(&scalar_like_value(&args[1])?)?;
4574        let cumulative = coerce_num(&scalar_like_value(&args[2])?)? != 0.0;
4575
4576        if k < 0 || lambda < 0.0 {
4577            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4578                ExcelError::new_num(),
4579            )));
4580        }
4581
4582        let result = if cumulative {
4583            // CDF: sum from i=0 to k of P(X=i) = 1 - Q(k+1, lambda)
4584            // Using the regularized incomplete gamma function
4585            1.0 - gamma_p((k + 1) as f64, lambda)
4586        } else {
4587            // PMF: P(X=k) = lambda^k * e^(-lambda) / k!
4588            // Use log to avoid overflow
4589            let ln_prob = (k as f64) * lambda.ln() - lambda - ln_gamma((k + 1) as f64);
4590            ln_prob.exp()
4591        };
4592
4593        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
4594            result,
4595        )))
4596    }
4597}
4598
4599/// Returns the exponential-distribution probability at `x` for rate `lambda`.
4600///
4601/// Use `EXPON.DIST` for waiting-time models where events occur with a constant hazard rate.
4602///
4603/// # Remarks
4604/// - Requires `x >= 0` and `lambda > 0`.
4605/// - Set `cumulative` to non-zero for CDF mode, or `0` for PDF mode.
4606/// - Returns `#NUM!` when inputs violate domain requirements.
4607/// - Invalid numeric coercions propagate as spreadsheet errors.
4608///
4609/// # Examples
4610///
4611/// ```yaml,sandbox
4612/// title: "Exponential CDF"
4613/// formula: "=EXPON.DIST(1,1,TRUE)"
4614/// expected: 0.6321205588285577
4615/// ```
4616///
4617/// ```yaml,sandbox
4618/// title: "Exponential PDF"
4619/// formula: "=EXPON.DIST(1,1,FALSE)"
4620/// expected: 0.36787944117144233
4621/// ```
4622#[derive(Debug)]
4623pub struct ExponDistFn;
4624/// [formualizer-docgen:schema:start]
4625/// Name: EXPON.DIST
4626/// Type: ExponDistFn
4627/// Min args: 3
4628/// Max args: 3
4629/// Variadic: false
4630/// Signature: EXPON.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
4631/// 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}
4632/// Caps: PURE
4633/// [formualizer-docgen:schema:end]
4634impl Function for ExponDistFn {
4635    func_caps!(PURE);
4636    fn name(&self) -> &'static str {
4637        "EXPON.DIST"
4638    }
4639    fn min_args(&self) -> usize {
4640        3
4641    }
4642    fn arg_schema(&self) -> &'static [ArgSchema] {
4643        use std::sync::LazyLock;
4644        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
4645            vec![
4646                ArgSchema::number_lenient_scalar(),
4647                ArgSchema::number_lenient_scalar(),
4648                ArgSchema::number_lenient_scalar(),
4649            ]
4650        });
4651        &SCHEMA[..]
4652    }
4653    fn eval<'a, 'b, 'c>(
4654        &self,
4655        args: &'c [ArgumentHandle<'a, 'b>],
4656        _ctx: &dyn FunctionContext<'b>,
4657    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4658        let x = coerce_num(&scalar_like_value(&args[0])?)?;
4659        let lambda = coerce_num(&scalar_like_value(&args[1])?)?;
4660        let cumulative = coerce_num(&scalar_like_value(&args[2])?)? != 0.0;
4661
4662        if x < 0.0 || lambda <= 0.0 {
4663            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4664                ExcelError::new_num(),
4665            )));
4666        }
4667
4668        let result = if cumulative {
4669            // CDF: 1 - e^(-lambda*x)
4670            1.0 - (-lambda * x).exp()
4671        } else {
4672            // PDF: lambda * e^(-lambda*x)
4673            lambda * (-lambda * x).exp()
4674        };
4675
4676        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
4677            result,
4678        )))
4679    }
4680}
4681
4682/// Returns the gamma-distribution probability at `x` for shape `alpha` and scale `beta`.
4683///
4684/// `GAMMA.DIST` supports cumulative and density modes for right-skewed waiting-time models.
4685///
4686/// # Remarks
4687/// - Requires `x >= 0`, `alpha > 0`, and `beta > 0`.
4688/// - Set `cumulative` to non-zero for CDF mode, or `0` for PDF mode.
4689/// - Returns `#NUM!` when any parameter is outside its valid range.
4690/// - Invalid numeric coercions propagate as spreadsheet errors.
4691///
4692/// # Examples
4693///
4694/// ```yaml,sandbox
4695/// title: "Gamma CDF with alpha=1 and beta=2"
4696/// formula: "=GAMMA.DIST(2,1,2,TRUE)"
4697/// expected: 0.6321205588285577
4698/// ```
4699///
4700/// ```yaml,sandbox
4701/// title: "Gamma PDF with alpha=1 and beta=2"
4702/// formula: "=GAMMA.DIST(2,1,2,FALSE)"
4703/// expected: 0.18393972058572117
4704/// ```
4705#[derive(Debug)]
4706pub struct GammaDistFn;
4707/// [formualizer-docgen:schema:start]
4708/// Name: GAMMA.DIST
4709/// Type: GammaDistFn
4710/// Min args: 4
4711/// Max args: 4
4712/// Variadic: false
4713/// Signature: GAMMA.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar)
4714/// 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}
4715/// Caps: PURE
4716/// [formualizer-docgen:schema:end]
4717impl Function for GammaDistFn {
4718    func_caps!(PURE);
4719    fn name(&self) -> &'static str {
4720        "GAMMA.DIST"
4721    }
4722    fn min_args(&self) -> usize {
4723        4
4724    }
4725    fn arg_schema(&self) -> &'static [ArgSchema] {
4726        use std::sync::LazyLock;
4727        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
4728            vec![
4729                ArgSchema::number_lenient_scalar(),
4730                ArgSchema::number_lenient_scalar(),
4731                ArgSchema::number_lenient_scalar(),
4732                ArgSchema::number_lenient_scalar(),
4733            ]
4734        });
4735        &SCHEMA[..]
4736    }
4737    fn eval<'a, 'b, 'c>(
4738        &self,
4739        args: &'c [ArgumentHandle<'a, 'b>],
4740        _ctx: &dyn FunctionContext<'b>,
4741    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4742        let x = coerce_num(&scalar_like_value(&args[0])?)?;
4743        let alpha = coerce_num(&scalar_like_value(&args[1])?)?; // shape
4744        let beta = coerce_num(&scalar_like_value(&args[2])?)?; // scale
4745        let cumulative = coerce_num(&scalar_like_value(&args[3])?)? != 0.0;
4746
4747        if x < 0.0 || alpha <= 0.0 || beta <= 0.0 {
4748            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4749                ExcelError::new_num(),
4750            )));
4751        }
4752
4753        let result = if cumulative {
4754            // CDF: P(alpha, x/beta) where P is the regularized lower incomplete gamma
4755            gamma_p(alpha, x / beta)
4756        } else {
4757            // PDF: x^(alpha-1) * e^(-x/beta) / (beta^alpha * Gamma(alpha))
4758            let ln_pdf = (alpha - 1.0) * x.ln() - x / beta - alpha * beta.ln() - ln_gamma(alpha);
4759            ln_pdf.exp()
4760        };
4761
4762        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
4763            result,
4764        )))
4765    }
4766}
4767
4768/// Returns the Weibull-distribution probability at `x` for shape `alpha` and scale `beta`.
4769///
4770/// `WEIBULL.DIST` is commonly used for reliability and time-to-failure analysis.
4771///
4772/// # Remarks
4773/// - Requires `x >= 0`, `alpha > 0`, and `beta > 0`.
4774/// - Set `cumulative` to non-zero for CDF mode, or `0` for PDF mode.
4775/// - Returns `#NUM!` when parameters fall outside valid ranges.
4776/// - In PDF mode at `x = 0`, behavior follows the Weibull shape-specific limit.
4777///
4778/// # Examples
4779///
4780/// ```yaml,sandbox
4781/// title: "Weibull CDF with alpha=1 and beta=2"
4782/// formula: "=WEIBULL.DIST(2,1,2,TRUE)"
4783/// expected: 0.6321205588285577
4784/// ```
4785///
4786/// ```yaml,sandbox
4787/// title: "Weibull PDF with alpha=1 and beta=2"
4788/// formula: "=WEIBULL.DIST(2,1,2,FALSE)"
4789/// expected: 0.18393972058572117
4790/// ```
4791#[derive(Debug)]
4792pub struct WeibullDistFn;
4793/// [formualizer-docgen:schema:start]
4794/// Name: WEIBULL.DIST
4795/// Type: WeibullDistFn
4796/// Min args: 4
4797/// Max args: 4
4798/// Variadic: false
4799/// Signature: WEIBULL.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar)
4800/// 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}
4801/// Caps: PURE
4802/// [formualizer-docgen:schema:end]
4803impl Function for WeibullDistFn {
4804    func_caps!(PURE);
4805    fn name(&self) -> &'static str {
4806        "WEIBULL.DIST"
4807    }
4808    fn min_args(&self) -> usize {
4809        4
4810    }
4811    fn arg_schema(&self) -> &'static [ArgSchema] {
4812        use std::sync::LazyLock;
4813        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
4814            vec![
4815                ArgSchema::number_lenient_scalar(),
4816                ArgSchema::number_lenient_scalar(),
4817                ArgSchema::number_lenient_scalar(),
4818                ArgSchema::number_lenient_scalar(),
4819            ]
4820        });
4821        &SCHEMA[..]
4822    }
4823    fn eval<'a, 'b, 'c>(
4824        &self,
4825        args: &'c [ArgumentHandle<'a, 'b>],
4826        _ctx: &dyn FunctionContext<'b>,
4827    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4828        let x = coerce_num(&scalar_like_value(&args[0])?)?;
4829        let alpha = coerce_num(&scalar_like_value(&args[1])?)?; // shape
4830        let beta = coerce_num(&scalar_like_value(&args[2])?)?; // scale
4831        let cumulative = coerce_num(&scalar_like_value(&args[3])?)? != 0.0;
4832
4833        if x < 0.0 || alpha <= 0.0 || beta <= 0.0 {
4834            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4835                ExcelError::new_num(),
4836            )));
4837        }
4838
4839        let result = if cumulative {
4840            // CDF: 1 - e^(-(x/beta)^alpha)
4841            1.0 - (-(x / beta).powf(alpha)).exp()
4842        } else {
4843            // PDF: (alpha/beta) * (x/beta)^(alpha-1) * e^(-(x/beta)^alpha)
4844            if x == 0.0 {
4845                if alpha < 1.0 {
4846                    f64::INFINITY
4847                } else if alpha == 1.0 {
4848                    alpha / beta
4849                } else {
4850                    0.0
4851                }
4852            } else {
4853                (alpha / beta) * (x / beta).powf(alpha - 1.0) * (-(x / beta).powf(alpha)).exp()
4854            }
4855        };
4856
4857        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
4858            result,
4859        )))
4860    }
4861}
4862
4863/// Returns the beta-distribution probability for `x`, with optional lower/upper bounds.
4864///
4865/// `BETA.DIST` can evaluate either the cumulative probability or density on `[A, B]` (default
4866/// `[0, 1]`).
4867///
4868/// # Remarks
4869/// - Requires `alpha > 0`, `beta > 0`, and `A < B`.
4870/// - `x` must lie within the inclusive interval `[A, B]`.
4871/// - Set `cumulative` to non-zero for CDF mode, or `0` for PDF mode.
4872/// - Returns `#NUM!` for invalid bounds, parameters, or out-of-range `x`.
4873///
4874/// # Examples
4875///
4876/// ```yaml,sandbox
4877/// title: "Uniform beta CDF on [0,1]"
4878/// formula: "=BETA.DIST(0.3,1,1,TRUE)"
4879/// expected: 0.3
4880/// ```
4881///
4882/// ```yaml,sandbox
4883/// title: "Uniform beta PDF on [0,1]"
4884/// formula: "=BETA.DIST(0.3,1,1,FALSE)"
4885/// expected: 1
4886/// ```
4887#[derive(Debug)]
4888pub struct BetaDistFn;
4889/// [formualizer-docgen:schema:start]
4890/// Name: BETA.DIST
4891/// Type: BetaDistFn
4892/// Min args: 4
4893/// Max args: variadic
4894/// Variadic: true
4895/// Signature: BETA.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar, arg5: number@scalar, arg6...: number@scalar)
4896/// 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}
4897/// Caps: PURE
4898/// [formualizer-docgen:schema:end]
4899impl Function for BetaDistFn {
4900    func_caps!(PURE);
4901    fn name(&self) -> &'static str {
4902        "BETA.DIST"
4903    }
4904    fn min_args(&self) -> usize {
4905        4
4906    }
4907    fn variadic(&self) -> bool {
4908        true
4909    }
4910    fn arg_schema(&self) -> &'static [ArgSchema] {
4911        use std::sync::LazyLock;
4912        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
4913            vec![
4914                ArgSchema::number_lenient_scalar(),
4915                ArgSchema::number_lenient_scalar(),
4916                ArgSchema::number_lenient_scalar(),
4917                ArgSchema::number_lenient_scalar(),
4918                ArgSchema::number_lenient_scalar(),
4919                ArgSchema::number_lenient_scalar(),
4920            ]
4921        });
4922        &SCHEMA[..]
4923    }
4924    fn eval<'a, 'b, 'c>(
4925        &self,
4926        args: &'c [ArgumentHandle<'a, 'b>],
4927        _ctx: &dyn FunctionContext<'b>,
4928    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4929        let x = coerce_num(&scalar_like_value(&args[0])?)?;
4930        let alpha = coerce_num(&scalar_like_value(&args[1])?)?;
4931        let beta_param = coerce_num(&scalar_like_value(&args[2])?)?;
4932        let cumulative = coerce_num(&scalar_like_value(&args[3])?)? != 0.0;
4933
4934        // Optional bounds A and B (default 0 and 1)
4935        let a = if args.len() > 4 {
4936            coerce_num(&scalar_like_value(&args[4])?)?
4937        } else {
4938            0.0
4939        };
4940        let b = if args.len() > 5 {
4941            coerce_num(&scalar_like_value(&args[5])?)?
4942        } else {
4943            1.0
4944        };
4945
4946        if alpha <= 0.0 || beta_param <= 0.0 || a >= b {
4947            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4948                ExcelError::new_num(),
4949            )));
4950        }
4951
4952        // x must be in [a, b]
4953        if x < a || x > b {
4954            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4955                ExcelError::new_num(),
4956            )));
4957        }
4958
4959        // Transform x to standard [0,1] interval
4960        let x_std = (x - a) / (b - a);
4961
4962        let result = if cumulative {
4963            // CDF: I_x(alpha, beta) - regularized incomplete beta function
4964            beta_i(x_std, alpha, beta_param)
4965        } else {
4966            // PDF: (x-A)^(alpha-1) * (B-x)^(beta-1) / ((B-A)^(alpha+beta-1) * B(alpha, beta))
4967            let ln_beta = ln_gamma(alpha) + ln_gamma(beta_param) - ln_gamma(alpha + beta_param);
4968            let scale = b - a;
4969            if (x_std == 0.0 && alpha < 1.0) || (x_std == 1.0 && beta_param < 1.0) {
4970                f64::INFINITY
4971            } else if x_std == 0.0 {
4972                if alpha == 1.0 {
4973                    (1.0 - x_std).powf(beta_param - 1.0) / (scale * ln_beta.exp())
4974                } else {
4975                    0.0
4976                }
4977            } else if x_std == 1.0 {
4978                if beta_param == 1.0 {
4979                    x_std.powf(alpha - 1.0) / (scale * ln_beta.exp())
4980                } else {
4981                    0.0
4982                }
4983            } else {
4984                let ln_pdf =
4985                    (alpha - 1.0) * x_std.ln() + (beta_param - 1.0) * (1.0 - x_std).ln() - ln_beta;
4986                ln_pdf.exp() / scale
4987            }
4988        };
4989
4990        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
4991            result,
4992        )))
4993    }
4994}
4995
4996/// Returns negative-binomial probabilities for failures observed before a target success count.
4997///
4998/// `NEGBINOM.DIST` supports exact-failure mode (PMF) and cumulative mode (CDF).
4999///
5000/// # Remarks
5001/// - `number_f` is truncated and must be `>= 0`.
5002/// - `number_s` is truncated and must be `>= 1`.
5003/// - `probability_s` must satisfy `0 < p < 1`.
5004/// - Returns `#NUM!` when counts or probability are outside valid ranges.
5005///
5006/// # Examples
5007///
5008/// ```yaml,sandbox
5009/// title: "Negative binomial PMF"
5010/// formula: "=NEGBINOM.DIST(2,1,0.5,FALSE)"
5011/// expected: 0.125
5012/// ```
5013///
5014/// ```yaml,sandbox
5015/// title: "Negative binomial CDF"
5016/// formula: "=NEGBINOM.DIST(2,1,0.5,TRUE)"
5017/// expected: 0.875
5018/// ```
5019#[derive(Debug)]
5020pub struct NegbinomDistFn;
5021/// [formualizer-docgen:schema:start]
5022/// Name: NEGBINOM.DIST
5023/// Type: NegbinomDistFn
5024/// Min args: 4
5025/// Max args: 4
5026/// Variadic: false
5027/// Signature: NEGBINOM.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar)
5028/// 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}
5029/// Caps: PURE
5030/// [formualizer-docgen:schema:end]
5031impl Function for NegbinomDistFn {
5032    func_caps!(PURE);
5033    fn name(&self) -> &'static str {
5034        "NEGBINOM.DIST"
5035    }
5036    fn min_args(&self) -> usize {
5037        4
5038    }
5039    fn arg_schema(&self) -> &'static [ArgSchema] {
5040        use std::sync::LazyLock;
5041        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
5042            vec![
5043                ArgSchema::number_lenient_scalar(),
5044                ArgSchema::number_lenient_scalar(),
5045                ArgSchema::number_lenient_scalar(),
5046                ArgSchema::number_lenient_scalar(),
5047            ]
5048        });
5049        &SCHEMA[..]
5050    }
5051    fn eval<'a, 'b, 'c>(
5052        &self,
5053        args: &'c [ArgumentHandle<'a, 'b>],
5054        _ctx: &dyn FunctionContext<'b>,
5055    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5056        let number_f = coerce_num(&scalar_like_value(&args[0])?)?.trunc() as i64; // number of failures
5057        let number_s = coerce_num(&scalar_like_value(&args[1])?)?.trunc() as i64; // number of successes
5058        let prob_s = coerce_num(&scalar_like_value(&args[2])?)?; // probability of success
5059        let cumulative = coerce_num(&scalar_like_value(&args[3])?)? != 0.0;
5060
5061        if number_f < 0 || number_s < 1 || prob_s <= 0.0 || prob_s >= 1.0 {
5062            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5063                ExcelError::new_num(),
5064            )));
5065        }
5066
5067        let result = if cumulative {
5068            // CDF: sum from i=0 to number_f of P(X=i)
5069            // This is equivalent to I_{prob_s}(number_s, number_f + 1) using regularized beta
5070            beta_i(prob_s, number_s as f64, (number_f + 1) as f64)
5071        } else {
5072            // PMF: C(number_f + number_s - 1, number_s - 1) * prob_s^number_s * (1-prob_s)^number_f
5073            // = C(k + r - 1, r - 1) * p^r * (1-p)^k where k = number_f, r = number_s
5074            let ln_prob = ln_binom(number_f + number_s - 1, number_s - 1)
5075                + (number_s as f64) * prob_s.ln()
5076                + (number_f as f64) * (1.0 - prob_s).ln();
5077            ln_prob.exp()
5078        };
5079
5080        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
5081            result,
5082        )))
5083    }
5084}
5085
5086/// Returns hypergeometric probabilities for successes drawn without replacement.
5087///
5088/// Use `HYPGEOM.DIST` for finite-population sampling where each draw changes remaining odds.
5089///
5090/// # Remarks
5091/// - Count inputs are truncated to integers.
5092/// - Requires valid population/sample bounds and feasible success counts.
5093/// - Set `cumulative` to non-zero for CDF mode, or `0` for PMF mode.
5094/// - Returns `#NUM!` for invalid population setup; out-of-support PMF values return `0`.
5095///
5096/// # Examples
5097///
5098/// ```yaml,sandbox
5099/// title: "Hypergeometric PMF"
5100/// formula: "=HYPGEOM.DIST(1,3,4,10,FALSE)"
5101/// expected: 0.5
5102/// ```
5103///
5104/// ```yaml,sandbox
5105/// title: "Hypergeometric CDF"
5106/// formula: "=HYPGEOM.DIST(1,3,4,10,TRUE)"
5107/// expected: 0.6666666666666666
5108/// ```
5109#[derive(Debug)]
5110pub struct HypgeomDistFn;
5111/// [formualizer-docgen:schema:start]
5112/// Name: HYPGEOM.DIST
5113/// Type: HypgeomDistFn
5114/// Min args: 5
5115/// Max args: 5
5116/// Variadic: false
5117/// Signature: HYPGEOM.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar, arg5: number@scalar)
5118/// 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}
5119/// Caps: PURE
5120/// [formualizer-docgen:schema:end]
5121impl Function for HypgeomDistFn {
5122    func_caps!(PURE);
5123    fn name(&self) -> &'static str {
5124        "HYPGEOM.DIST"
5125    }
5126    fn min_args(&self) -> usize {
5127        5
5128    }
5129    fn arg_schema(&self) -> &'static [ArgSchema] {
5130        use std::sync::LazyLock;
5131        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
5132            vec![
5133                ArgSchema::number_lenient_scalar(),
5134                ArgSchema::number_lenient_scalar(),
5135                ArgSchema::number_lenient_scalar(),
5136                ArgSchema::number_lenient_scalar(),
5137                ArgSchema::number_lenient_scalar(),
5138            ]
5139        });
5140        &SCHEMA[..]
5141    }
5142    fn eval<'a, 'b, 'c>(
5143        &self,
5144        args: &'c [ArgumentHandle<'a, 'b>],
5145        _ctx: &dyn FunctionContext<'b>,
5146    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5147        let sample_s = coerce_num(&scalar_like_value(&args[0])?)?.trunc() as i64; // successes in sample
5148        let number_sample = coerce_num(&scalar_like_value(&args[1])?)?.trunc() as i64; // sample size
5149        let population_s = coerce_num(&scalar_like_value(&args[2])?)?.trunc() as i64; // successes in population
5150        let number_pop = coerce_num(&scalar_like_value(&args[3])?)?.trunc() as i64; // population size
5151        let cumulative = coerce_num(&scalar_like_value(&args[4])?)? != 0.0;
5152
5153        // Validation
5154        if number_pop <= 0
5155            || population_s < 0
5156            || population_s > number_pop
5157            || number_sample < 0
5158            || number_sample > number_pop
5159            || sample_s < 0
5160        {
5161            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5162                ExcelError::new_num(),
5163            )));
5164        }
5165
5166        // sample_s must be at least max(0, number_sample - (number_pop - population_s))
5167        // and at most min(number_sample, population_s)
5168        let min_successes = 0.max(number_sample - (number_pop - population_s));
5169        let max_successes = number_sample.min(population_s);
5170
5171        if sample_s < min_successes || sample_s > max_successes {
5172            // Return 0 for PMF, or appropriate CDF value
5173            if cumulative {
5174                if sample_s < min_successes {
5175                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0)));
5176                } else {
5177                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(1.0)));
5178                }
5179            } else {
5180                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0)));
5181            }
5182        }
5183
5184        let result = if cumulative {
5185            // CDF: sum from i=min_successes to sample_s of P(X=i)
5186            let mut sum = 0.0;
5187            for i in min_successes..=sample_s {
5188                sum += hypgeom_pmf(i, number_sample, population_s, number_pop);
5189            }
5190            sum
5191        } else {
5192            // PMF: C(population_s, sample_s) * C(number_pop - population_s, number_sample - sample_s) / C(number_pop, number_sample)
5193            hypgeom_pmf(sample_s, number_sample, population_s, number_pop)
5194        };
5195
5196        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
5197            result,
5198        )))
5199    }
5200}
5201
5202/// Helper: Hypergeometric PMF
5203fn hypgeom_pmf(k: i64, n: i64, k_pop: i64, n_pop: i64) -> f64 {
5204    // P(X=k) = C(K, k) * C(N-K, n-k) / C(N, n)
5205    // Using logs to avoid overflow
5206    let ln_prob = ln_binom(k_pop, k) + ln_binom(n_pop - k_pop, n - k) - ln_binom(n_pop, n);
5207    ln_prob.exp()
5208}
5209
5210/* ═══════════════════════════════════════════════════════════════════════════
5211COVARIANCE AND CORRELATION FUNCTIONS
5212═══════════════════════════════════════════════════════════════════════════ */
5213
5214/// Returns population covariance for two paired numeric data sets.
5215///
5216/// `COVARIANCE.P` measures joint variability using `n` in the denominator.
5217///
5218/// # Remarks
5219/// - Arrays must resolve to the same number of numeric points.
5220/// - Uses population scaling (`/ n`) rather than sample scaling.
5221/// - Positive output indicates same-direction movement; negative output indicates opposite movement.
5222/// - Pairing and shape mismatches return spreadsheet errors from paired-array validation.
5223///
5224/// # Examples
5225///
5226/// ```yaml,sandbox
5227/// title: "Positive population covariance"
5228/// formula: "=COVARIANCE.P({1,3,5},{2,4,6})"
5229/// expected: 2.6666666666666665
5230/// ```
5231///
5232/// ```yaml,sandbox
5233/// title: "Negative population covariance"
5234/// formula: "=COVARIANCE.P({1,2,3},{3,2,1})"
5235/// expected: -0.6666666666666666
5236/// ```
5237#[derive(Debug)]
5238pub struct CovariancePFn;
5239/// [formualizer-docgen:schema:start]
5240/// Name: COVARIANCE.P
5241/// Type: CovariancePFn
5242/// Min args: 2
5243/// Max args: 1
5244/// Variadic: false
5245/// Signature: COVARIANCE.P(arg1: number@range)
5246/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
5247/// Caps: PURE, REDUCTION, NUMERIC_ONLY
5248/// [formualizer-docgen:schema:end]
5249impl Function for CovariancePFn {
5250    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
5251    fn name(&self) -> &'static str {
5252        "COVARIANCE.P"
5253    }
5254    fn aliases(&self) -> &'static [&'static str] {
5255        &["COVAR"]
5256    }
5257    fn min_args(&self) -> usize {
5258        2
5259    }
5260    fn arg_schema(&self) -> &'static [ArgSchema] {
5261        &ARG_RANGE_NUM_LENIENT_ONE[..]
5262    }
5263    fn eval<'a, 'b, 'c>(
5264        &self,
5265        args: &'c [ArgumentHandle<'a, 'b>],
5266        _ctx: &dyn FunctionContext<'b>,
5267    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5268        let (y, x) = match collect_paired_arrays(args) {
5269            Ok(v) => v,
5270            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
5271        };
5272
5273        let n = x.len() as f64;
5274        let mean_x = x.iter().sum::<f64>() / n;
5275        let mean_y = y.iter().sum::<f64>() / n;
5276
5277        let mut sum_xy = 0.0;
5278        for i in 0..x.len() {
5279            let dx = x[i] - mean_x;
5280            let dy = y[i] - mean_y;
5281            sum_xy += dx * dy;
5282        }
5283
5284        // Population covariance divides by n
5285        let covar = sum_xy / n;
5286        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
5287            covar,
5288        )))
5289    }
5290}
5291
5292/// Returns sample covariance for two paired numeric data sets.
5293///
5294/// `COVARIANCE.S` measures joint variability using `n - 1` in the denominator.
5295///
5296/// # Remarks
5297/// - Arrays must contain paired numeric values with matching lengths.
5298/// - Requires at least two paired points.
5299/// - Returns `#DIV/0!` when fewer than two numeric pairs are available.
5300/// - Pairing and shape mismatches return spreadsheet errors from paired-array validation.
5301///
5302/// # Examples
5303///
5304/// ```yaml,sandbox
5305/// title: "Positive sample covariance"
5306/// formula: "=COVARIANCE.S({1,3,5},{2,4,6})"
5307/// expected: 4
5308/// ```
5309///
5310/// ```yaml,sandbox
5311/// title: "Negative sample covariance"
5312/// formula: "=COVARIANCE.S({1,2,3},{3,2,1})"
5313/// expected: -1
5314/// ```
5315#[derive(Debug)]
5316pub struct CovarianceSFn;
5317/// [formualizer-docgen:schema:start]
5318/// Name: COVARIANCE.S
5319/// Type: CovarianceSFn
5320/// Min args: 2
5321/// Max args: 1
5322/// Variadic: false
5323/// Signature: COVARIANCE.S(arg1: number@range)
5324/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
5325/// Caps: PURE, REDUCTION, NUMERIC_ONLY
5326/// [formualizer-docgen:schema:end]
5327impl Function for CovarianceSFn {
5328    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
5329    fn name(&self) -> &'static str {
5330        "COVARIANCE.S"
5331    }
5332    fn min_args(&self) -> usize {
5333        2
5334    }
5335    fn arg_schema(&self) -> &'static [ArgSchema] {
5336        &ARG_RANGE_NUM_LENIENT_ONE[..]
5337    }
5338    fn eval<'a, 'b, 'c>(
5339        &self,
5340        args: &'c [ArgumentHandle<'a, 'b>],
5341        _ctx: &dyn FunctionContext<'b>,
5342    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5343        let (y, x) = match collect_paired_arrays(args) {
5344            Ok(v) => v,
5345            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
5346        };
5347
5348        let n = x.len();
5349        if n < 2 {
5350            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5351                ExcelError::new_div(),
5352            )));
5353        }
5354
5355        let mean_x = x.iter().sum::<f64>() / n as f64;
5356        let mean_y = y.iter().sum::<f64>() / n as f64;
5357
5358        let mut sum_xy = 0.0;
5359        for i in 0..n {
5360            let dx = x[i] - mean_x;
5361            let dy = y[i] - mean_y;
5362            sum_xy += dx * dy;
5363        }
5364
5365        // Sample covariance divides by (n - 1)
5366        let covar = sum_xy / (n - 1) as f64;
5367        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
5368            covar,
5369        )))
5370    }
5371}
5372
5373/// Returns the Pearson correlation coefficient between two paired numeric arrays.
5374///
5375/// `PEARSON` reports linear association on a normalized scale from `-1` to `1`.
5376///
5377/// # Remarks
5378/// - Arrays must contain the same number of numeric observations.
5379/// - Returns `#DIV/0!` when either array has zero variance.
5380/// - Positive values indicate positive linear association; negative values indicate inverse association.
5381/// - Pairing and shape mismatches return spreadsheet errors from paired-array validation.
5382///
5383/// # Examples
5384///
5385/// ```yaml,sandbox
5386/// title: "Perfect positive linear correlation"
5387/// formula: "=PEARSON({1,2,3},{2,4,6})"
5388/// expected: 1
5389/// ```
5390///
5391/// ```yaml,sandbox
5392/// title: "Perfect negative linear correlation"
5393/// formula: "=PEARSON({1,2,3},{3,2,1})"
5394/// expected: -1
5395/// ```
5396#[derive(Debug)]
5397pub struct PearsonFn;
5398/// [formualizer-docgen:schema:start]
5399/// Name: PEARSON
5400/// Type: PearsonFn
5401/// Min args: 2
5402/// Max args: 1
5403/// Variadic: false
5404/// Signature: PEARSON(arg1: number@range)
5405/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
5406/// Caps: PURE, REDUCTION, NUMERIC_ONLY
5407/// [formualizer-docgen:schema:end]
5408impl Function for PearsonFn {
5409    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
5410    fn name(&self) -> &'static str {
5411        "PEARSON"
5412    }
5413    fn min_args(&self) -> usize {
5414        2
5415    }
5416    fn arg_schema(&self) -> &'static [ArgSchema] {
5417        &ARG_RANGE_NUM_LENIENT_ONE[..]
5418    }
5419    fn eval<'a, 'b, 'c>(
5420        &self,
5421        args: &'c [ArgumentHandle<'a, 'b>],
5422        _ctx: &dyn FunctionContext<'b>,
5423    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5424        let (y, x) = match collect_paired_arrays(args) {
5425            Ok(v) => v,
5426            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
5427        };
5428
5429        let n = x.len() as f64;
5430        let mean_x = x.iter().sum::<f64>() / n;
5431        let mean_y = y.iter().sum::<f64>() / n;
5432
5433        let mut sum_xy = 0.0;
5434        let mut sum_x2 = 0.0;
5435        let mut sum_y2 = 0.0;
5436
5437        for i in 0..x.len() {
5438            let dx = x[i] - mean_x;
5439            let dy = y[i] - mean_y;
5440            sum_xy += dx * dy;
5441            sum_x2 += dx * dx;
5442            sum_y2 += dy * dy;
5443        }
5444
5445        let denom = (sum_x2 * sum_y2).sqrt();
5446        if denom == 0.0 {
5447            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5448                ExcelError::new_div(),
5449            )));
5450        }
5451
5452        let correl = sum_xy / denom;
5453        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
5454            correl,
5455        )))
5456    }
5457}
5458
5459/// Returns the coefficient of determination (`R^2`) for paired x/y data.
5460///
5461/// `RSQ` is the square of Pearson correlation and indicates explained linear variance.
5462///
5463/// # Remarks
5464/// - Arrays must contain the same number of numeric observations.
5465/// - Result is in `[0, 1]` for valid numeric inputs.
5466/// - Returns `#DIV/0!` when either input array has zero variance.
5467/// - Pairing and shape mismatches return spreadsheet errors from paired-array validation.
5468///
5469/// # Examples
5470///
5471/// ```yaml,sandbox
5472/// title: "Perfect linear fit"
5473/// formula: "=RSQ({1,2,3},{2,4,6})"
5474/// expected: 1
5475/// ```
5476///
5477/// ```yaml,sandbox
5478/// title: "Strong but imperfect linear relationship"
5479/// formula: "=RSQ({1,2,3},{1,2,4})"
5480/// expected: 0.9642857142857143
5481/// ```
5482#[derive(Debug)]
5483pub struct RsqFn;
5484/// [formualizer-docgen:schema:start]
5485/// Name: RSQ
5486/// Type: RsqFn
5487/// Min args: 2
5488/// Max args: 1
5489/// Variadic: false
5490/// Signature: RSQ(arg1: number@range)
5491/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
5492/// Caps: PURE, REDUCTION, NUMERIC_ONLY
5493/// [formualizer-docgen:schema:end]
5494impl Function for RsqFn {
5495    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
5496    fn name(&self) -> &'static str {
5497        "RSQ"
5498    }
5499    fn min_args(&self) -> usize {
5500        2
5501    }
5502    fn arg_schema(&self) -> &'static [ArgSchema] {
5503        &ARG_RANGE_NUM_LENIENT_ONE[..]
5504    }
5505    fn eval<'a, 'b, 'c>(
5506        &self,
5507        args: &'c [ArgumentHandle<'a, 'b>],
5508        _ctx: &dyn FunctionContext<'b>,
5509    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5510        let (y, x) = match collect_paired_arrays(args) {
5511            Ok(v) => v,
5512            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
5513        };
5514
5515        let n = x.len() as f64;
5516        let mean_x = x.iter().sum::<f64>() / n;
5517        let mean_y = y.iter().sum::<f64>() / n;
5518
5519        let mut sum_xy = 0.0;
5520        let mut sum_x2 = 0.0;
5521        let mut sum_y2 = 0.0;
5522
5523        for i in 0..x.len() {
5524            let dx = x[i] - mean_x;
5525            let dy = y[i] - mean_y;
5526            sum_xy += dx * dy;
5527            sum_x2 += dx * dx;
5528            sum_y2 += dy * dy;
5529        }
5530
5531        let denom = sum_x2 * sum_y2;
5532        if denom == 0.0 {
5533            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5534                ExcelError::new_div(),
5535            )));
5536        }
5537
5538        // R-squared = r^2 = (sum_xy)^2 / (sum_x2 * sum_y2)
5539        let rsq = (sum_xy * sum_xy) / denom;
5540        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(rsq)))
5541    }
5542}
5543
5544/// Returns the standard error of y-estimates from a simple linear regression.
5545///
5546/// `STEYX` measures the typical residual size around the fitted regression line.
5547///
5548/// # Remarks
5549/// - Requires paired x/y inputs with matching numeric lengths.
5550/// - Requires at least three paired points.
5551/// - Returns `#DIV/0!` when `n < 3` or x-values have zero variance.
5552/// - Pairing and shape mismatches return spreadsheet errors from paired-array validation.
5553///
5554/// # Examples
5555///
5556/// ```yaml,sandbox
5557/// title: "Perfect linear fit has zero standard error"
5558/// formula: "=STEYX({2,4,6},{1,2,3})"
5559/// expected: 0
5560/// ```
5561///
5562/// ```yaml,sandbox
5563/// title: "Non-zero regression standard error"
5564/// formula: "=STEYX({2,5,7},{1,2,3})"
5565/// expected: 0.408248290463863
5566/// ```
5567#[derive(Debug)]
5568pub struct SteyxFn;
5569/// [formualizer-docgen:schema:start]
5570/// Name: STEYX
5571/// Type: SteyxFn
5572/// Min args: 2
5573/// Max args: 1
5574/// Variadic: false
5575/// Signature: STEYX(arg1: number@range)
5576/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
5577/// Caps: PURE, REDUCTION, NUMERIC_ONLY
5578/// [formualizer-docgen:schema:end]
5579impl Function for SteyxFn {
5580    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
5581    fn name(&self) -> &'static str {
5582        "STEYX"
5583    }
5584    fn min_args(&self) -> usize {
5585        2
5586    }
5587    fn arg_schema(&self) -> &'static [ArgSchema] {
5588        &ARG_RANGE_NUM_LENIENT_ONE[..]
5589    }
5590    fn eval<'a, 'b, 'c>(
5591        &self,
5592        args: &'c [ArgumentHandle<'a, 'b>],
5593        _ctx: &dyn FunctionContext<'b>,
5594    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5595        let (y, x) = match collect_paired_arrays(args) {
5596            Ok(v) => v,
5597            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
5598        };
5599
5600        let n = x.len();
5601        if n < 3 {
5602            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5603                ExcelError::new_div(),
5604            )));
5605        }
5606
5607        let n_f = n as f64;
5608        let mean_x = x.iter().sum::<f64>() / n_f;
5609        let mean_y = y.iter().sum::<f64>() / n_f;
5610
5611        let mut sum_xy = 0.0;
5612        let mut sum_x2 = 0.0;
5613        let mut sum_y2 = 0.0;
5614
5615        for i in 0..n {
5616            let dx = x[i] - mean_x;
5617            let dy = y[i] - mean_y;
5618            sum_xy += dx * dy;
5619            sum_x2 += dx * dx;
5620            sum_y2 += dy * dy;
5621        }
5622
5623        if sum_x2 == 0.0 {
5624            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5625                ExcelError::new_div(),
5626            )));
5627        }
5628
5629        // STEYX = sqrt((sum_y2 - (sum_xy)^2 / sum_x2) / (n - 2))
5630        let sse = sum_y2 - (sum_xy * sum_xy) / sum_x2;
5631        if sse < 0.0 {
5632            // This can happen due to floating point errors; return 0 in such case
5633            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0)));
5634        }
5635        let steyx = (sse / (n_f - 2.0)).sqrt();
5636        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
5637            steyx,
5638        )))
5639    }
5640}
5641
5642/* ─────────────────────────── SKEW ──────────────────────────── */
5643
5644/// Returns the sample skewness of a numeric distribution.
5645///
5646/// `SKEW` quantifies asymmetry: positive values indicate a longer right tail, negative values a
5647/// longer left tail.
5648///
5649/// # Remarks
5650/// - Requires at least three numeric values.
5651/// - Returns `#DIV/0!` when there are fewer than three numbers or zero sample standard deviation.
5652/// - Non-numeric values in ranges are ignored by statistical-collection rules.
5653/// - Uses the Excel-style sample skewness correction factor.
5654///
5655/// # Examples
5656///
5657/// ```yaml,sandbox
5658/// title: "Symmetric sample"
5659/// formula: "=SKEW({1,2,3})"
5660/// expected: 0
5661/// ```
5662///
5663/// ```yaml,sandbox
5664/// title: "Right-skewed sample"
5665/// formula: "=SKEW({1,1,2,10})"
5666/// expected: 1.9683567600862015
5667/// ```
5668#[derive(Debug)]
5669pub struct SkewFn;
5670/// [formualizer-docgen:schema:start]
5671/// Name: SKEW
5672/// Type: SkewFn
5673/// Min args: 1
5674/// Max args: variadic
5675/// Variadic: true
5676/// Signature: SKEW(arg1...: number@range)
5677/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
5678/// Caps: PURE, REDUCTION, NUMERIC_ONLY
5679/// [formualizer-docgen:schema:end]
5680impl Function for SkewFn {
5681    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
5682    fn name(&self) -> &'static str {
5683        "SKEW"
5684    }
5685    fn min_args(&self) -> usize {
5686        1
5687    }
5688    fn variadic(&self) -> bool {
5689        true
5690    }
5691    fn arg_schema(&self) -> &'static [ArgSchema] {
5692        &ARG_RANGE_NUM_LENIENT_ONE[..]
5693    }
5694    fn eval<'a, 'b, 'c>(
5695        &self,
5696        args: &'c [ArgumentHandle<'a, 'b>],
5697        _ctx: &dyn FunctionContext<'b>,
5698    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5699        let nums = collect_numeric_stats(args)?;
5700        let n = nums.len();
5701
5702        // SKEW requires at least 3 data points
5703        if n < 3 {
5704            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5705                ExcelError::new_div(),
5706            )));
5707        }
5708
5709        let n_f = n as f64;
5710        let mean = nums.iter().sum::<f64>() / n_f;
5711
5712        // Calculate sample standard deviation
5713        let mut sum_sq = 0.0;
5714        for &v in &nums {
5715            let d = v - mean;
5716            sum_sq += d * d;
5717        }
5718        let stdev = (sum_sq / (n_f - 1.0)).sqrt();
5719
5720        if stdev == 0.0 {
5721            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5722                ExcelError::new_div(),
5723            )));
5724        }
5725
5726        // Calculate sum of cubed deviations normalized by stdev
5727        let mut sum_cubed = 0.0;
5728        for &v in &nums {
5729            let d = (v - mean) / stdev;
5730            sum_cubed += d * d * d;
5731        }
5732
5733        // Excel SKEW formula: n / ((n-1)*(n-2)) * sum((xi - mean)/stdev)^3
5734        let skew = (n_f / ((n_f - 1.0) * (n_f - 2.0))) * sum_cubed;
5735        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(skew)))
5736    }
5737}
5738
5739/* ─────────────────────────── KURT ──────────────────────────── */
5740
5741/// Returns the sample excess kurtosis of a numeric distribution.
5742///
5743/// `KURT` indicates tail heaviness relative to a normal distribution after Excel-style sample
5744/// correction.
5745///
5746/// # Remarks
5747/// - Requires at least four numeric values.
5748/// - Returns `#DIV/0!` when there are fewer than four numbers or zero sample standard deviation.
5749/// - Positive values suggest heavier tails; negative values suggest lighter tails.
5750/// - Non-numeric values in ranges are ignored by statistical-collection rules.
5751///
5752/// # Examples
5753///
5754/// ```yaml,sandbox
5755/// title: "Uniformly spaced values"
5756/// formula: "=KURT({1,2,3,4})"
5757/// expected: -1.2
5758/// ```
5759///
5760/// ```yaml,sandbox
5761/// title: "Heavier-tail sample"
5762/// formula: "=KURT({1,1,1,2,10,10,10,10})"
5763/// expected: -2.3069755007920767
5764/// ```
5765#[derive(Debug)]
5766pub struct KurtFn;
5767/// [formualizer-docgen:schema:start]
5768/// Name: KURT
5769/// Type: KurtFn
5770/// Min args: 1
5771/// Max args: variadic
5772/// Variadic: true
5773/// Signature: KURT(arg1...: number@range)
5774/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
5775/// Caps: PURE, REDUCTION, NUMERIC_ONLY
5776/// [formualizer-docgen:schema:end]
5777impl Function for KurtFn {
5778    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
5779    fn name(&self) -> &'static str {
5780        "KURT"
5781    }
5782    fn min_args(&self) -> usize {
5783        1
5784    }
5785    fn variadic(&self) -> bool {
5786        true
5787    }
5788    fn arg_schema(&self) -> &'static [ArgSchema] {
5789        &ARG_RANGE_NUM_LENIENT_ONE[..]
5790    }
5791    fn eval<'a, 'b, 'c>(
5792        &self,
5793        args: &'c [ArgumentHandle<'a, 'b>],
5794        _ctx: &dyn FunctionContext<'b>,
5795    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5796        let nums = collect_numeric_stats(args)?;
5797        let n = nums.len();
5798
5799        // KURT requires at least 4 data points
5800        if n < 4 {
5801            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5802                ExcelError::new_div(),
5803            )));
5804        }
5805
5806        let n_f = n as f64;
5807        let mean = nums.iter().sum::<f64>() / n_f;
5808
5809        // Calculate sample standard deviation
5810        let mut sum_sq = 0.0;
5811        for &v in &nums {
5812            let d = v - mean;
5813            sum_sq += d * d;
5814        }
5815        let stdev = (sum_sq / (n_f - 1.0)).sqrt();
5816
5817        if stdev == 0.0 {
5818            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5819                ExcelError::new_div(),
5820            )));
5821        }
5822
5823        // Calculate sum of fourth powers of deviations normalized by stdev
5824        let mut sum_fourth = 0.0;
5825        for &v in &nums {
5826            let d = (v - mean) / stdev;
5827            sum_fourth += d * d * d * d;
5828        }
5829
5830        // Excel KURT formula (excess kurtosis):
5831        // n*(n+1) / ((n-1)*(n-2)*(n-3)) * sum((xi - mean)/stdev)^4 - 3*(n-1)^2 / ((n-2)*(n-3))
5832        let term1 = (n_f * (n_f + 1.0)) / ((n_f - 1.0) * (n_f - 2.0) * (n_f - 3.0)) * sum_fourth;
5833        let term2 = (3.0 * (n_f - 1.0) * (n_f - 1.0)) / ((n_f - 2.0) * (n_f - 3.0));
5834        let kurt = term1 - term2;
5835        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(kurt)))
5836    }
5837}
5838
5839/* ─────────────────────────── FISHER ──────────────────────────── */
5840
5841/// Returns the Fisher z-transformation of a correlation-like value `x`.
5842///
5843/// `FISHER` maps `(-1, 1)` into `(-inf, +inf)` and is commonly used in correlation inference.
5844///
5845/// # Remarks
5846/// - Input must satisfy `-1 < x < 1`.
5847/// - Returns `#NUM!` when `x <= -1` or `x >= 1`.
5848/// - The transformation is `0.5 * ln((1 + x) / (1 - x))`.
5849/// - Invalid numeric coercions propagate as spreadsheet errors.
5850///
5851/// # Examples
5852///
5853/// ```yaml,sandbox
5854/// title: "Fisher transform at zero"
5855/// formula: "=FISHER(0)"
5856/// expected: 0
5857/// ```
5858///
5859/// ```yaml,sandbox
5860/// title: "Fisher transform at x=0.5"
5861/// formula: "=FISHER(0.5)"
5862/// expected: 0.5493061443340549
5863/// ```
5864#[derive(Debug)]
5865pub struct FisherFn;
5866/// [formualizer-docgen:schema:start]
5867/// Name: FISHER
5868/// Type: FisherFn
5869/// Min args: 1
5870/// Max args: 1
5871/// Variadic: false
5872/// Signature: FISHER(arg1: number@range)
5873/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
5874/// Caps: PURE, NUMERIC_ONLY
5875/// [formualizer-docgen:schema:end]
5876impl Function for FisherFn {
5877    func_caps!(PURE, NUMERIC_ONLY);
5878    fn name(&self) -> &'static str {
5879        "FISHER"
5880    }
5881    fn min_args(&self) -> usize {
5882        1
5883    }
5884    fn arg_schema(&self) -> &'static [ArgSchema] {
5885        &ARG_RANGE_NUM_LENIENT_ONE[..]
5886    }
5887    fn eval<'a, 'b, 'c>(
5888        &self,
5889        args: &'c [ArgumentHandle<'a, 'b>],
5890        _ctx: &dyn FunctionContext<'b>,
5891    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5892        let x = coerce_num(&scalar_like_value(&args[0])?)?;
5893
5894        // FISHER requires -1 < x < 1
5895        if x <= -1.0 || x >= 1.0 {
5896            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5897                ExcelError::new_num(),
5898            )));
5899        }
5900
5901        // Fisher transformation: 0.5 * ln((1 + x) / (1 - x))
5902        let fisher = 0.5 * ((1.0 + x) / (1.0 - x)).ln();
5903        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
5904            fisher,
5905        )))
5906    }
5907}
5908
5909/* ─────────────────────────── FISHERINV ──────────────────────────── */
5910
5911/// Returns the inverse Fisher transformation of `y`.
5912///
5913/// `FISHERINV` maps Fisher z-values back to the open interval `(-1, 1)`.
5914///
5915/// # Remarks
5916/// - The inverse form is `(e^(2y) - 1) / (e^(2y) + 1)`.
5917/// - Output is always strictly between `-1` and `1` for finite inputs.
5918/// - This function is useful for converting transformed correlation estimates back to r-space.
5919/// - Invalid numeric coercions propagate as spreadsheet errors.
5920///
5921/// # Examples
5922///
5923/// ```yaml,sandbox
5924/// title: "Inverse Fisher at zero"
5925/// formula: "=FISHERINV(0)"
5926/// expected: 0
5927/// ```
5928///
5929/// ```yaml,sandbox
5930/// title: "Round-trip with FISHER(0.5)"
5931/// formula: "=FISHERINV(0.5493061443340549)"
5932/// expected: 0.5
5933/// ```
5934#[derive(Debug)]
5935pub struct FisherInvFn;
5936/// [formualizer-docgen:schema:start]
5937/// Name: FISHERINV
5938/// Type: FisherInvFn
5939/// Min args: 1
5940/// Max args: 1
5941/// Variadic: false
5942/// Signature: FISHERINV(arg1: number@range)
5943/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
5944/// Caps: PURE, NUMERIC_ONLY
5945/// [formualizer-docgen:schema:end]
5946impl Function for FisherInvFn {
5947    func_caps!(PURE, NUMERIC_ONLY);
5948    fn name(&self) -> &'static str {
5949        "FISHERINV"
5950    }
5951    fn min_args(&self) -> usize {
5952        1
5953    }
5954    fn arg_schema(&self) -> &'static [ArgSchema] {
5955        &ARG_RANGE_NUM_LENIENT_ONE[..]
5956    }
5957    fn eval<'a, 'b, 'c>(
5958        &self,
5959        args: &'c [ArgumentHandle<'a, 'b>],
5960        _ctx: &dyn FunctionContext<'b>,
5961    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5962        let y = coerce_num(&scalar_like_value(&args[0])?)?;
5963
5964        // Inverse Fisher transformation: (e^(2y) - 1) / (e^(2y) + 1)
5965        let e2y = (2.0 * y).exp();
5966        let fisherinv = (e2y - 1.0) / (e2y + 1.0);
5967        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
5968            fisherinv,
5969        )))
5970    }
5971}
5972
5973/* ─────────────────────────── FORECAST.LINEAR ──────────────────────────── */
5974
5975/// Returns a predicted y-value at `x` from simple linear regression over known data.
5976///
5977/// `FORECAST.LINEAR` fits `y = intercept + slope * x` and evaluates that line at the requested x.
5978///
5979/// # Remarks
5980/// - Requires `known_y` and `known_x` arrays with the same numeric length.
5981/// - Returns `#N/A` when arrays are empty or lengths do not match.
5982/// - Returns `#DIV/0!` when `known_x` has zero variance.
5983/// - Alias `FORECAST` is supported.
5984///
5985/// # Examples
5986///
5987/// ```yaml,sandbox
5988/// title: "Predict next point on a perfect line"
5989/// formula: "=FORECAST.LINEAR(4,{2,4,6},{1,2,3})"
5990/// expected: 8
5991/// ```
5992///
5993/// ```yaml,sandbox
5994/// title: "Forecast with non-zero intercept"
5995/// formula: "=FORECAST.LINEAR(5,{3,5,7},{1,2,3})"
5996/// expected: 11
5997/// ```
5998#[derive(Debug)]
5999pub struct ForecastLinearFn;
6000/// [formualizer-docgen:schema:start]
6001/// Name: FORECAST.LINEAR
6002/// Type: ForecastLinearFn
6003/// Min args: 3
6004/// Max args: 1
6005/// Variadic: false
6006/// Signature: FORECAST.LINEAR(arg1: number@range)
6007/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
6008/// Caps: PURE, NUMERIC_ONLY
6009/// [formualizer-docgen:schema:end]
6010impl Function for ForecastLinearFn {
6011    func_caps!(PURE, NUMERIC_ONLY);
6012    fn name(&self) -> &'static str {
6013        "FORECAST.LINEAR"
6014    }
6015    fn aliases(&self) -> &'static [&'static str] {
6016        &["FORECAST"]
6017    }
6018    fn min_args(&self) -> usize {
6019        3
6020    }
6021    fn arg_schema(&self) -> &'static [ArgSchema] {
6022        &ARG_RANGE_NUM_LENIENT_ONE[..]
6023    }
6024    fn eval<'a, 'b, 'c>(
6025        &self,
6026        args: &'c [ArgumentHandle<'a, 'b>],
6027        _ctx: &dyn FunctionContext<'b>,
6028    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
6029        // args[0] = x value to forecast
6030        // args[1] = known_y's
6031        // args[2] = known_x's
6032        let x = match coerce_num(&scalar_like_value(&args[0])?) {
6033            Ok(n) => n,
6034            Err(_) => {
6035                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6036                    ExcelError::new_value(),
6037                )));
6038            }
6039        };
6040
6041        let y_vals = collect_numeric_stats(&args[1..2])?;
6042        let x_vals = collect_numeric_stats(&args[2..3])?;
6043
6044        // Arrays must have same length
6045        if y_vals.len() != x_vals.len() {
6046            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6047                ExcelError::new_na(),
6048            )));
6049        }
6050
6051        if y_vals.is_empty() {
6052            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6053                ExcelError::new_na(),
6054            )));
6055        }
6056
6057        let n = x_vals.len() as f64;
6058        let mean_x = x_vals.iter().sum::<f64>() / n;
6059        let mean_y = y_vals.iter().sum::<f64>() / n;
6060
6061        let mut sum_xy = 0.0;
6062        let mut sum_x2 = 0.0;
6063
6064        for i in 0..x_vals.len() {
6065            let dx = x_vals[i] - mean_x;
6066            let dy = y_vals[i] - mean_y;
6067            sum_xy += dx * dy;
6068            sum_x2 += dx * dx;
6069        }
6070
6071        if sum_x2 == 0.0 {
6072            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6073                ExcelError::new_div(),
6074            )));
6075        }
6076
6077        let slope = sum_xy / sum_x2;
6078        let intercept = mean_y - slope * mean_x;
6079        let forecast = intercept + slope * x;
6080
6081        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
6082            forecast,
6083        )))
6084    }
6085}
6086
6087/* ─────────────────────────── LINEST ──────────────────────────── */
6088
6089/// Returns linear-regression coefficients and optional fit statistics.
6090///
6091/// `LINEST` fits a straight line to known y/x pairs and returns either `[slope, intercept]` or a
6092/// larger statistics matrix.
6093///
6094/// # Remarks
6095/// - `known_y` is required; `known_x` defaults to `1..n` when omitted.
6096/// - `const` controls whether an intercept is fitted (`TRUE` by default).
6097/// - `stats=TRUE` returns a `5x2` result block; otherwise it returns `1x2`.
6098/// - Returns spreadsheet errors for mismatched lengths, empty data, or degenerate x-values.
6099///
6100/// # Examples
6101///
6102/// ```yaml,sandbox
6103/// title: "Slope and intercept only"
6104/// formula: "=LINEST({2,4,6},{1,2,3})"
6105/// expected:
6106///   - [2, 0]
6107/// ```
6108///
6109/// ```yaml,sandbox
6110/// title: "Linear fit with non-zero intercept"
6111/// formula: "=LINEST({3,5,7},{1,2,3})"
6112/// expected:
6113///   - [2, 1]
6114/// ```
6115#[derive(Debug)]
6116pub struct LinestFn;
6117/// [formualizer-docgen:schema:start]
6118/// Name: LINEST
6119/// Type: LinestFn
6120/// Min args: 1
6121/// Max args: variadic
6122/// Variadic: true
6123/// Signature: LINEST(arg1...: number@range)
6124/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
6125/// Caps: PURE, NUMERIC_ONLY
6126/// [formualizer-docgen:schema:end]
6127impl Function for LinestFn {
6128    func_caps!(PURE, NUMERIC_ONLY);
6129    fn name(&self) -> &'static str {
6130        "LINEST"
6131    }
6132    fn min_args(&self) -> usize {
6133        1
6134    }
6135    fn variadic(&self) -> bool {
6136        true
6137    }
6138    fn arg_schema(&self) -> &'static [ArgSchema] {
6139        &ARG_RANGE_NUM_LENIENT_ONE[..]
6140    }
6141    fn eval<'a, 'b, 'c>(
6142        &self,
6143        args: &'c [ArgumentHandle<'a, 'b>],
6144        _ctx: &dyn FunctionContext<'b>,
6145    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
6146        // args[0] = known_y's (required)
6147        // args[1] = known_x's (optional, defaults to {1,2,3,...})
6148        // args[2] = const (optional, default TRUE - whether to compute intercept)
6149        // args[3] = stats (optional, default FALSE - whether to return additional statistics)
6150
6151        let y_vals = collect_numeric_stats(&args[0..1])?;
6152
6153        if y_vals.is_empty() {
6154            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6155                ExcelError::new_na(),
6156            )));
6157        }
6158
6159        // Get known_x's or generate default {1, 2, 3, ...}
6160        let x_vals = if args.len() >= 2 {
6161            collect_numeric_stats(&args[1..2])?
6162        } else {
6163            (1..=y_vals.len()).map(|i| i as f64).collect()
6164        };
6165
6166        // Arrays must have same length
6167        if y_vals.len() != x_vals.len() {
6168            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6169                ExcelError::new_ref(),
6170            )));
6171        }
6172
6173        // Parse const argument (default TRUE)
6174        let use_const = if args.len() >= 3 {
6175            match scalar_like_value(&args[2])? {
6176                LiteralValue::Boolean(b) => b,
6177                LiteralValue::Number(n) => n != 0.0,
6178                LiteralValue::Int(i) => i != 0,
6179                _ => true,
6180            }
6181        } else {
6182            true
6183        };
6184
6185        // Parse stats argument (default FALSE)
6186        let return_stats = if args.len() >= 4 {
6187            match scalar_like_value(&args[3])? {
6188                LiteralValue::Boolean(b) => b,
6189                LiteralValue::Number(n) => n != 0.0,
6190                LiteralValue::Int(i) => i != 0,
6191                _ => false,
6192            }
6193        } else {
6194            false
6195        };
6196
6197        let n = x_vals.len() as f64;
6198
6199        // Calculate regression coefficients
6200        let (slope, intercept) = if use_const {
6201            // Normal linear regression with intercept
6202            let mean_x = x_vals.iter().sum::<f64>() / n;
6203            let mean_y = y_vals.iter().sum::<f64>() / n;
6204
6205            let mut sum_xy = 0.0;
6206            let mut sum_x2 = 0.0;
6207
6208            for i in 0..x_vals.len() {
6209                let dx = x_vals[i] - mean_x;
6210                let dy = y_vals[i] - mean_y;
6211                sum_xy += dx * dy;
6212                sum_x2 += dx * dx;
6213            }
6214
6215            if sum_x2 == 0.0 {
6216                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6217                    ExcelError::new_div(),
6218                )));
6219            }
6220
6221            let slope = sum_xy / sum_x2;
6222            let intercept = mean_y - slope * mean_x;
6223            (slope, intercept)
6224        } else {
6225            // Regression through origin (intercept = 0)
6226            let mut sum_xy = 0.0;
6227            let mut sum_x2 = 0.0;
6228
6229            for i in 0..x_vals.len() {
6230                sum_xy += x_vals[i] * y_vals[i];
6231                sum_x2 += x_vals[i] * x_vals[i];
6232            }
6233
6234            if sum_x2 == 0.0 {
6235                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6236                    ExcelError::new_div(),
6237                )));
6238            }
6239
6240            let slope = sum_xy / sum_x2;
6241            (slope, 0.0)
6242        };
6243
6244        if !return_stats {
6245            // Return just slope and intercept as 1x2 array: [[slope, intercept]]
6246            let row = vec![LiteralValue::Number(slope), LiteralValue::Number(intercept)];
6247            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Array(vec![
6248                row,
6249            ])));
6250        }
6251
6252        // Calculate additional statistics for stats=TRUE
6253        // Row 1: [slope, intercept]
6254        // Row 2: [se_slope, se_intercept]
6255        // Row 3: [r_squared, se_y]
6256        // Row 4: [F_statistic, df]
6257        // Row 5: [ss_reg, ss_resid]
6258
6259        let mean_y = y_vals.iter().sum::<f64>() / n;
6260
6261        // Calculate residuals and sums of squares
6262        let mut ss_resid = 0.0; // Sum of squared residuals
6263        let mut ss_tot = 0.0; // Total sum of squares
6264
6265        for i in 0..x_vals.len() {
6266            let y_pred = slope * x_vals[i] + intercept;
6267            let residual = y_vals[i] - y_pred;
6268            ss_resid += residual * residual;
6269            let dy_tot = y_vals[i] - mean_y;
6270            ss_tot += dy_tot * dy_tot;
6271        }
6272
6273        let ss_reg = ss_tot - ss_resid; // Regression sum of squares
6274
6275        // R-squared
6276        let r_squared = if ss_tot == 0.0 {
6277            1.0 // Perfect fit or all y values are the same
6278        } else {
6279            1.0 - (ss_resid / ss_tot)
6280        };
6281
6282        // Degrees of freedom
6283        let df = if use_const {
6284            (n as i64 - 2).max(1) as f64 // n - k - 1 where k=1 (one predictor)
6285        } else {
6286            (n as i64 - 1).max(1) as f64 // n - k when no intercept
6287        };
6288
6289        // Standard error of y estimate
6290        let se_y = if df > 0.0 {
6291            (ss_resid / df).sqrt()
6292        } else {
6293            0.0
6294        };
6295
6296        // Standard errors of coefficients
6297        let mean_x = x_vals.iter().sum::<f64>() / n;
6298        let mut sum_x2_centered = 0.0;
6299        let mut sum_x2_raw = 0.0;
6300        for &xi in &x_vals {
6301            sum_x2_centered += (xi - mean_x).powi(2);
6302            sum_x2_raw += xi * xi;
6303        }
6304
6305        let se_slope = if sum_x2_centered > 0.0 && df > 0.0 {
6306            se_y / sum_x2_centered.sqrt()
6307        } else {
6308            f64::NAN
6309        };
6310
6311        let se_intercept = if use_const && sum_x2_centered > 0.0 && df > 0.0 {
6312            se_y * (sum_x2_raw / (n * sum_x2_centered)).sqrt()
6313        } else {
6314            f64::NAN
6315        };
6316
6317        // F-statistic
6318        let f_stat = if ss_resid > 0.0 && df > 0.0 {
6319            (ss_reg / 1.0) / (ss_resid / df) // MSR / MSE
6320        } else if ss_resid == 0.0 {
6321            f64::INFINITY // Perfect fit
6322        } else {
6323            f64::NAN
6324        };
6325
6326        // Build 5x2 result array
6327        let rows = vec![
6328            vec![LiteralValue::Number(slope), LiteralValue::Number(intercept)],
6329            vec![
6330                LiteralValue::Number(se_slope),
6331                LiteralValue::Number(se_intercept),
6332            ],
6333            vec![LiteralValue::Number(r_squared), LiteralValue::Number(se_y)],
6334            vec![LiteralValue::Number(f_stat), LiteralValue::Number(df)],
6335            vec![LiteralValue::Number(ss_reg), LiteralValue::Number(ss_resid)],
6336        ];
6337
6338        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Array(rows)))
6339    }
6340}
6341
6342/* ─────────────────────────── CONFIDENCE.NORM ──────────────────────────── */
6343
6344/// Returns the half-width of a confidence interval using a normal critical value.
6345///
6346/// `CONFIDENCE.NORM` computes `z_crit * standard_dev / sqrt(size)` for two-sided intervals.
6347///
6348/// # Remarks
6349/// - `alpha` must satisfy `0 < alpha < 1`.
6350/// - `standard_dev` must be greater than `0`.
6351/// - `size` must be at least `1`.
6352/// - Returns `#NUM!` when any input is outside valid bounds.
6353///
6354/// # Examples
6355///
6356/// ```yaml,sandbox
6357/// title: "95% confidence half-width"
6358/// formula: "=CONFIDENCE.NORM(0.05,2,100)"
6359/// expected: 0.3919927977622559
6360/// ```
6361///
6362/// ```yaml,sandbox
6363/// title: "90% confidence half-width"
6364/// formula: "=CONFIDENCE.NORM(0.1,5,25)"
6365/// expected: 1.644853625133699
6366/// ```
6367#[derive(Debug)]
6368pub struct ConfidenceNormFn;
6369/// [formualizer-docgen:schema:start]
6370/// Name: CONFIDENCE.NORM
6371/// Type: ConfidenceNormFn
6372/// Min args: 3
6373/// Max args: 3
6374/// Variadic: false
6375/// Signature: CONFIDENCE.NORM(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
6376/// 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}
6377/// Caps: PURE
6378/// [formualizer-docgen:schema:end]
6379impl Function for ConfidenceNormFn {
6380    func_caps!(PURE);
6381    fn name(&self) -> &'static str {
6382        "CONFIDENCE.NORM"
6383    }
6384    fn aliases(&self) -> &'static [&'static str] {
6385        &["CONFIDENCE"]
6386    }
6387    fn min_args(&self) -> usize {
6388        3
6389    }
6390    fn arg_schema(&self) -> &'static [ArgSchema] {
6391        use std::sync::LazyLock;
6392        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
6393            vec![
6394                ArgSchema::number_lenient_scalar(),
6395                ArgSchema::number_lenient_scalar(),
6396                ArgSchema::number_lenient_scalar(),
6397            ]
6398        });
6399        &SCHEMA[..]
6400    }
6401    fn eval<'a, 'b, 'c>(
6402        &self,
6403        args: &'c [ArgumentHandle<'a, 'b>],
6404        _ctx: &dyn FunctionContext<'b>,
6405    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
6406        let alpha = coerce_num(&scalar_like_value(&args[0])?)?;
6407        let std_dev = coerce_num(&scalar_like_value(&args[1])?)?;
6408        let size = coerce_num(&scalar_like_value(&args[2])?)?;
6409
6410        // Validate inputs
6411        if alpha <= 0.0 || alpha >= 1.0 {
6412            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6413                ExcelError::new_num(),
6414            )));
6415        }
6416        if std_dev <= 0.0 {
6417            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6418                ExcelError::new_num(),
6419            )));
6420        }
6421        if size < 1.0 {
6422            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6423                ExcelError::new_num(),
6424            )));
6425        }
6426
6427        // z_crit = NORM.S.INV(1 - alpha/2)
6428        let z_crit = match std_norm_inv(1.0 - alpha / 2.0) {
6429            Some(z) => z,
6430            None => {
6431                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6432                    ExcelError::new_num(),
6433                )));
6434            }
6435        };
6436
6437        let result = z_crit * std_dev / size.sqrt();
6438        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
6439            result,
6440        )))
6441    }
6442}
6443
6444/* ─────────────────────────── CONFIDENCE.T ──────────────────────────── */
6445
6446/// Returns the half-width of a confidence interval using a t critical value.
6447///
6448/// `CONFIDENCE.T` is typically used when population standard deviation is unknown and sample size
6449/// is limited.
6450///
6451/// # Remarks
6452/// - `alpha` must satisfy `0 < alpha < 1`.
6453/// - `standard_dev` must be greater than `0`.
6454/// - `size` must be at least `2` so that `df = size - 1` is valid.
6455/// - Returns `#NUM!` when inputs are outside valid bounds.
6456///
6457/// # Examples
6458///
6459/// ```yaml,sandbox
6460/// title: "95% t-interval half-width"
6461/// formula: "=CONFIDENCE.T(0.05,2,25)"
6462/// expected: 0.8256636934020788
6463/// ```
6464///
6465/// ```yaml,sandbox
6466/// title: "90% t-interval half-width"
6467/// formula: "=CONFIDENCE.T(0.1,5,10)"
6468/// expected: 2.9158049866307585
6469/// ```
6470#[derive(Debug)]
6471pub struct ConfidenceTFn;
6472/// [formualizer-docgen:schema:start]
6473/// Name: CONFIDENCE.T
6474/// Type: ConfidenceTFn
6475/// Min args: 3
6476/// Max args: 3
6477/// Variadic: false
6478/// Signature: CONFIDENCE.T(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
6479/// 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}
6480/// Caps: PURE
6481/// [formualizer-docgen:schema:end]
6482impl Function for ConfidenceTFn {
6483    func_caps!(PURE);
6484    fn name(&self) -> &'static str {
6485        "CONFIDENCE.T"
6486    }
6487    fn min_args(&self) -> usize {
6488        3
6489    }
6490    fn arg_schema(&self) -> &'static [ArgSchema] {
6491        use std::sync::LazyLock;
6492        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
6493            vec![
6494                ArgSchema::number_lenient_scalar(),
6495                ArgSchema::number_lenient_scalar(),
6496                ArgSchema::number_lenient_scalar(),
6497            ]
6498        });
6499        &SCHEMA[..]
6500    }
6501    fn eval<'a, 'b, 'c>(
6502        &self,
6503        args: &'c [ArgumentHandle<'a, 'b>],
6504        _ctx: &dyn FunctionContext<'b>,
6505    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
6506        let alpha = coerce_num(&scalar_like_value(&args[0])?)?;
6507        let std_dev = coerce_num(&scalar_like_value(&args[1])?)?;
6508        let size = coerce_num(&scalar_like_value(&args[2])?)?;
6509
6510        // Validate inputs - size must be >= 2 for t-distribution (df = size - 1 >= 1)
6511        if alpha <= 0.0 || alpha >= 1.0 {
6512            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6513                ExcelError::new_num(),
6514            )));
6515        }
6516        if std_dev <= 0.0 {
6517            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6518                ExcelError::new_num(),
6519            )));
6520        }
6521        if size < 2.0 {
6522            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6523                ExcelError::new_num(),
6524            )));
6525        }
6526
6527        let df = size - 1.0;
6528
6529        // t_crit = T.INV(1 - alpha/2, df)
6530        let t_crit = match t_inv(1.0 - alpha / 2.0, df) {
6531            Some(t) => t,
6532            None => {
6533                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6534                    ExcelError::new_num(),
6535                )));
6536            }
6537        };
6538
6539        let result = t_crit * std_dev / size.sqrt();
6540        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
6541            result,
6542        )))
6543    }
6544}
6545
6546/* ─────────────────────────── Z.TEST ──────────────────────────── */
6547
6548/// Returns the one-tailed p-value of a z-test against hypothesized mean `x`.
6549///
6550/// `Z.TEST` evaluates whether the sample mean is significantly greater than the target value.
6551///
6552/// # Remarks
6553/// - Uses provided `sigma` when supplied; otherwise computes population standard deviation.
6554/// - Returns `#NUM!` when `sigma <= 0`.
6555/// - Returns `#DIV/0!` when implied standard deviation is zero.
6556/// - Returns `#N/A` when the data array has no numeric values.
6557///
6558/// # Examples
6559///
6560/// ```yaml,sandbox
6561/// title: "Z-test with provided sigma"
6562/// formula: "=Z.TEST({1,2,3,4,5},2,1)"
6563/// expected: 0.012673659338734137
6564/// ```
6565///
6566/// ```yaml,sandbox
6567/// title: "Z-test with sigma estimated from sample"
6568/// formula: "=Z.TEST({1,2,3,4,5},2)"
6569/// expected: 0.056923149003329065
6570/// ```
6571#[derive(Debug)]
6572pub struct ZTestFn;
6573/// [formualizer-docgen:schema:start]
6574/// Name: Z.TEST
6575/// Type: ZTestFn
6576/// Min args: 2
6577/// Max args: variadic
6578/// Variadic: true
6579/// Signature: Z.TEST(arg1: number@range, arg2: number@scalar, arg3...: number@scalar)
6580/// 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}
6581/// Caps: PURE
6582/// [formualizer-docgen:schema:end]
6583impl Function for ZTestFn {
6584    func_caps!(PURE);
6585    fn name(&self) -> &'static str {
6586        "Z.TEST"
6587    }
6588    fn aliases(&self) -> &'static [&'static str] {
6589        &["ZTEST"]
6590    }
6591    fn min_args(&self) -> usize {
6592        2
6593    }
6594    fn variadic(&self) -> bool {
6595        true
6596    }
6597    fn arg_schema(&self) -> &'static [ArgSchema] {
6598        use std::sync::LazyLock;
6599        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
6600            vec![
6601                {
6602                    let mut s = ArgSchema::number_lenient_scalar();
6603                    s.shape = crate::args::ShapeKind::Range;
6604                    s.coercion = formualizer_common::CoercionPolicy::NumberLenientText;
6605                    s
6606                },
6607                ArgSchema::number_lenient_scalar(),
6608                ArgSchema::number_lenient_scalar(), // optional sigma
6609            ]
6610        });
6611        &SCHEMA[..]
6612    }
6613    fn eval<'a, 'b, 'c>(
6614        &self,
6615        args: &'c [ArgumentHandle<'a, 'b>],
6616        _ctx: &dyn FunctionContext<'b>,
6617    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
6618        // Collect numeric values from the array argument
6619        let data = collect_numeric_stats(&args[0..1])?;
6620
6621        if data.is_empty() {
6622            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6623                ExcelError::new_na(),
6624            )));
6625        }
6626
6627        let x = coerce_num(&scalar_like_value(&args[1])?)?;
6628
6629        let n = data.len() as f64;
6630        let mean: f64 = data.iter().sum::<f64>() / n;
6631
6632        // Calculate sigma: use provided value or compute population std dev
6633        let sigma = if args.len() > 2 {
6634            let s = coerce_num(&scalar_like_value(&args[2])?)?;
6635            if s <= 0.0 {
6636                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6637                    ExcelError::new_num(),
6638                )));
6639            }
6640            s
6641        } else {
6642            // Population standard deviation
6643            let variance: f64 = data.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / n;
6644            let std_dev = variance.sqrt();
6645            if std_dev == 0.0 {
6646                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6647                    ExcelError::new_div(),
6648                )));
6649            }
6650            std_dev
6651        };
6652
6653        // z = (mean - x) / (sigma / sqrt(n))
6654        let z = (mean - x) / (sigma / n.sqrt());
6655
6656        // P-value = 1 - NORM.S.DIST(z, TRUE)
6657        let p_value = 1.0 - std_norm_cdf(z);
6658
6659        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
6660            p_value,
6661        )))
6662    }
6663}
6664
6665/* ─────────────────────────── TREND ──────────────────────────── */
6666
6667/// Returns fitted y-values along a linear trend derived from known data.
6668///
6669/// `TREND` performs simple linear regression and returns predictions for `new_x` (or defaults).
6670///
6671/// # Remarks
6672/// - `known_y` is required; `known_x` defaults to `1..n` when omitted.
6673/// - `new_x` defaults to `known_x` when omitted.
6674/// - `const` defaults to `TRUE`; set to `FALSE` to force a zero intercept.
6675/// - Returns spreadsheet errors for empty data, mismatched lengths, or degenerate x-variance.
6676///
6677/// # Examples
6678///
6679/// ```yaml,sandbox
6680/// title: "Predict two future points on a line"
6681/// formula: "=TREND({2,4,6},{1,2,3},{4,5})"
6682/// expected:
6683///   - [8, 10]
6684/// ```
6685///
6686/// ```yaml,sandbox
6687/// title: "Default x-values with fitted trend"
6688/// formula: "=TREND({3,5,7})"
6689/// expected:
6690///   - [3, 5, 7]
6691/// ```
6692#[derive(Debug)]
6693pub struct TrendFn;
6694/// [formualizer-docgen:schema:start]
6695/// Name: TREND
6696/// Type: TrendFn
6697/// Min args: 1
6698/// Max args: variadic
6699/// Variadic: true
6700/// Signature: TREND(arg1...: number@range)
6701/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
6702/// Caps: PURE, NUMERIC_ONLY
6703/// [formualizer-docgen:schema:end]
6704impl Function for TrendFn {
6705    func_caps!(PURE, NUMERIC_ONLY);
6706    fn name(&self) -> &'static str {
6707        "TREND"
6708    }
6709    fn min_args(&self) -> usize {
6710        1
6711    }
6712    fn variadic(&self) -> bool {
6713        true
6714    }
6715    fn arg_schema(&self) -> &'static [ArgSchema] {
6716        &ARG_RANGE_NUM_LENIENT_ONE[..]
6717    }
6718    fn eval<'a, 'b, 'c>(
6719        &self,
6720        args: &'c [ArgumentHandle<'a, 'b>],
6721        _ctx: &dyn FunctionContext<'b>,
6722    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
6723        // TREND: args[0] = known_y's (required)
6724        // args[1] = known_x's (optional, defaults to {1,2,3,...})
6725        // args[2] = new_x's (optional, defaults to known_x's)
6726        // args[3] = const (optional, default TRUE - whether to compute intercept)
6727
6728        let y_vals = collect_numeric_stats(&args[0..1])?;
6729
6730        if y_vals.is_empty() {
6731            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6732                ExcelError::new_na(),
6733            )));
6734        }
6735
6736        // Helper to check if argument is empty/omitted
6737        // Note: Empty arguments are represented as empty text strings by the parser
6738        fn is_arg_empty(arg: &ArgumentHandle) -> bool {
6739            match scalar_like_value(arg) {
6740                Ok(LiteralValue::Empty) => true,
6741                Ok(LiteralValue::Text(s)) if s.is_empty() => true,
6742                _ => false,
6743            }
6744        }
6745
6746        // Get known_x's or generate default {1, 2, 3, ...}
6747        let x_vals = if args.len() >= 2 && !is_arg_empty(&args[1]) {
6748            collect_numeric_stats(&args[1..2])?
6749        } else {
6750            (1..=y_vals.len()).map(|i| i as f64).collect()
6751        };
6752
6753        // Arrays must have same length
6754        if y_vals.len() != x_vals.len() {
6755            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6756                ExcelError::new_ref(),
6757            )));
6758        }
6759
6760        // Get new_x's or use known_x's - check if argument is empty/omitted
6761        let new_x_vals = if args.len() >= 3 && !is_arg_empty(&args[2]) {
6762            collect_numeric_stats(&args[2..3])?
6763        } else {
6764            x_vals.clone()
6765        };
6766
6767        if new_x_vals.is_empty() {
6768            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6769                ExcelError::new_na(),
6770            )));
6771        }
6772
6773        // Parse const argument (default TRUE)
6774        let use_const = if args.len() >= 4 {
6775            match scalar_like_value(&args[3])? {
6776                LiteralValue::Boolean(b) => b,
6777                LiteralValue::Number(n) => n != 0.0,
6778                LiteralValue::Int(i) => i != 0,
6779                LiteralValue::Empty => true, // empty defaults to TRUE
6780                _ => true,
6781            }
6782        } else {
6783            true
6784        };
6785
6786        let n = x_vals.len() as f64;
6787
6788        // Calculate regression coefficients
6789        let (slope, intercept) = if use_const {
6790            // Normal linear regression with intercept
6791            let mean_x = x_vals.iter().sum::<f64>() / n;
6792            let mean_y = y_vals.iter().sum::<f64>() / n;
6793
6794            let mut sum_xy = 0.0;
6795            let mut sum_x2 = 0.0;
6796
6797            for i in 0..x_vals.len() {
6798                let dx = x_vals[i] - mean_x;
6799                let dy = y_vals[i] - mean_y;
6800                sum_xy += dx * dy;
6801                sum_x2 += dx * dx;
6802            }
6803
6804            if sum_x2 == 0.0 {
6805                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6806                    ExcelError::new_div(),
6807                )));
6808            }
6809
6810            let slope = sum_xy / sum_x2;
6811            let intercept = mean_y - slope * mean_x;
6812            (slope, intercept)
6813        } else {
6814            // Regression through origin (intercept = 0)
6815            let mut sum_xy = 0.0;
6816            let mut sum_x2 = 0.0;
6817
6818            for i in 0..x_vals.len() {
6819                sum_xy += x_vals[i] * y_vals[i];
6820                sum_x2 += x_vals[i] * x_vals[i];
6821            }
6822
6823            if sum_x2 == 0.0 {
6824                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6825                    ExcelError::new_div(),
6826                )));
6827            }
6828
6829            let slope = sum_xy / sum_x2;
6830            (slope, 0.0)
6831        };
6832
6833        // Calculate predicted y values for new_x's
6834        let predicted: Vec<LiteralValue> = new_x_vals
6835            .iter()
6836            .map(|&x| LiteralValue::Number(slope * x + intercept))
6837            .collect();
6838
6839        // Return as 1xN array (row vector)
6840        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Array(vec![
6841            predicted,
6842        ])))
6843    }
6844}
6845
6846/* ─────────────────────────── GROWTH ──────────────────────────── */
6847
6848/// Returns fitted values from an exponential trend model.
6849///
6850/// `GROWTH` fits `y = b * m^x` by linearizing in log space, then returns predictions for `new_x`.
6851///
6852/// # Remarks
6853/// - All known y-values must be strictly greater than `0`.
6854/// - `known_x` defaults to `1..n`; `new_x` defaults to `known_x`.
6855/// - `const` defaults to `TRUE`; set to `FALSE` to force `b = 1`.
6856/// - Returns spreadsheet errors for invalid domains, mismatched lengths, or degenerate x-variance.
6857///
6858/// # Examples
6859///
6860/// ```yaml,sandbox
6861/// title: "Exponential growth forecast"
6862/// formula: "=GROWTH({2,4,8},{1,2,3},{4,5})"
6863/// expected:
6864///   - [16, 32]
6865/// ```
6866///
6867/// ```yaml,sandbox
6868/// title: "Default x-values with perfect doubling pattern"
6869/// formula: "=GROWTH({3,6,12})"
6870/// expected:
6871///   - [3, 6, 12]
6872/// ```
6873#[derive(Debug)]
6874pub struct GrowthFn;
6875/// [formualizer-docgen:schema:start]
6876/// Name: GROWTH
6877/// Type: GrowthFn
6878/// Min args: 1
6879/// Max args: variadic
6880/// Variadic: true
6881/// Signature: GROWTH(arg1...: number@range)
6882/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
6883/// Caps: PURE, NUMERIC_ONLY
6884/// [formualizer-docgen:schema:end]
6885impl Function for GrowthFn {
6886    func_caps!(PURE, NUMERIC_ONLY);
6887    fn name(&self) -> &'static str {
6888        "GROWTH"
6889    }
6890    fn min_args(&self) -> usize {
6891        1
6892    }
6893    fn variadic(&self) -> bool {
6894        true
6895    }
6896    fn arg_schema(&self) -> &'static [ArgSchema] {
6897        &ARG_RANGE_NUM_LENIENT_ONE[..]
6898    }
6899    fn eval<'a, 'b, 'c>(
6900        &self,
6901        args: &'c [ArgumentHandle<'a, 'b>],
6902        _ctx: &dyn FunctionContext<'b>,
6903    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
6904        // GROWTH: args[0] = known_y's (required)
6905        // args[1] = known_x's (optional, defaults to {1,2,3,...})
6906        // args[2] = new_x's (optional, defaults to known_x's)
6907        // args[3] = const (optional, default TRUE - whether to compute intercept)
6908
6909        let y_vals = collect_numeric_stats(&args[0..1])?;
6910
6911        if y_vals.is_empty() {
6912            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6913                ExcelError::new_na(),
6914            )));
6915        }
6916
6917        // Check that all y values are positive (required for log transformation)
6918        for &y in &y_vals {
6919            if y <= 0.0 {
6920                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6921                    ExcelError::new_num(),
6922                )));
6923            }
6924        }
6925
6926        // Helper to check if argument is empty/omitted
6927        // Note: Empty arguments are represented as empty text strings by the parser
6928        fn is_arg_empty(arg: &ArgumentHandle) -> bool {
6929            match scalar_like_value(arg) {
6930                Ok(LiteralValue::Empty) => true,
6931                Ok(LiteralValue::Text(s)) if s.is_empty() => true,
6932                _ => false,
6933            }
6934        }
6935
6936        // Get known_x's or generate default {1, 2, 3, ...}
6937        let x_vals = if args.len() >= 2 && !is_arg_empty(&args[1]) {
6938            collect_numeric_stats(&args[1..2])?
6939        } else {
6940            (1..=y_vals.len()).map(|i| i as f64).collect()
6941        };
6942
6943        // Arrays must have same length
6944        if y_vals.len() != x_vals.len() {
6945            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6946                ExcelError::new_ref(),
6947            )));
6948        }
6949
6950        // Get new_x's or use known_x's - check if argument is empty/omitted
6951        let new_x_vals = if args.len() >= 3 && !is_arg_empty(&args[2]) {
6952            collect_numeric_stats(&args[2..3])?
6953        } else {
6954            x_vals.clone()
6955        };
6956
6957        if new_x_vals.is_empty() {
6958            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6959                ExcelError::new_na(),
6960            )));
6961        }
6962
6963        // Parse const argument (default TRUE)
6964        let use_const = if args.len() >= 4 {
6965            match scalar_like_value(&args[3])? {
6966                LiteralValue::Boolean(b) => b,
6967                LiteralValue::Number(n) => n != 0.0,
6968                LiteralValue::Int(i) => i != 0,
6969                LiteralValue::Empty => true, // empty defaults to TRUE
6970                _ => true,
6971            }
6972        } else {
6973            true
6974        };
6975
6976        // Transform to log space: ln(y) = ln(b) + x*ln(m)
6977        // This is linear regression on log-transformed y values
6978        let ln_y_vals: Vec<f64> = y_vals.iter().map(|&y| y.ln()).collect();
6979
6980        let n = x_vals.len() as f64;
6981
6982        // Calculate regression coefficients in log space
6983        let (ln_m, ln_b) = if use_const {
6984            // Normal linear regression with intercept
6985            let mean_x = x_vals.iter().sum::<f64>() / n;
6986            let mean_ln_y = ln_y_vals.iter().sum::<f64>() / n;
6987
6988            let mut sum_xy = 0.0;
6989            let mut sum_x2 = 0.0;
6990
6991            for i in 0..x_vals.len() {
6992                let dx = x_vals[i] - mean_x;
6993                let dy = ln_y_vals[i] - mean_ln_y;
6994                sum_xy += dx * dy;
6995                sum_x2 += dx * dx;
6996            }
6997
6998            if sum_x2 == 0.0 {
6999                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7000                    ExcelError::new_div(),
7001                )));
7002            }
7003
7004            let ln_m = sum_xy / sum_x2;
7005            let ln_b = mean_ln_y - ln_m * mean_x;
7006            (ln_m, ln_b)
7007        } else {
7008            // Regression through origin in log space (ln_b = 0, so b = 1)
7009            let mut sum_xy = 0.0;
7010            let mut sum_x2 = 0.0;
7011
7012            for i in 0..x_vals.len() {
7013                sum_xy += x_vals[i] * ln_y_vals[i];
7014                sum_x2 += x_vals[i] * x_vals[i];
7015            }
7016
7017            if sum_x2 == 0.0 {
7018                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7019                    ExcelError::new_div(),
7020                )));
7021            }
7022
7023            let ln_m = sum_xy / sum_x2;
7024            (ln_m, 0.0)
7025        };
7026
7027        // Convert back from log space: m = e^ln_m, b = e^ln_b
7028        let m = ln_m.exp();
7029        let b = ln_b.exp();
7030
7031        // Calculate predicted y values: y = b * m^x
7032        let predicted: Vec<LiteralValue> = new_x_vals
7033            .iter()
7034            .map(|&x| LiteralValue::Number(b * m.powf(x)))
7035            .collect();
7036
7037        // Return as 1xN array (row vector)
7038        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Array(vec![
7039            predicted,
7040        ])))
7041    }
7042}
7043
7044/* ─────────────────────────── LOGEST ──────────────────────────── */
7045
7046/// Returns parameters for an exponential model fitted to known data.
7047///
7048/// `LOGEST` fits `y = b * m^x` and returns either `[m, b]` or an expanded statistics matrix.
7049///
7050/// # Remarks
7051/// - All known y-values must be strictly greater than `0`.
7052/// - `known_x` defaults to `1..n` when omitted.
7053/// - `const` controls whether `b` is fitted (`TRUE` by default).
7054/// - `stats=TRUE` returns a `5x2` statistics block; otherwise returns `1x2`.
7055///
7056/// # Examples
7057///
7058/// ```yaml,sandbox
7059/// title: "Exponential base and intercept"
7060/// formula: "=LOGEST({2,4,8},{1,2,3})"
7061/// expected:
7062///   - [2, 1]
7063/// ```
7064///
7065/// ```yaml,sandbox
7066/// title: "Alternative growth series"
7067/// formula: "=LOGEST({3,6,12},{1,2,3})"
7068/// expected:
7069///   - [2, 1.5]
7070/// ```
7071#[derive(Debug)]
7072pub struct LogestFn;
7073/// [formualizer-docgen:schema:start]
7074/// Name: LOGEST
7075/// Type: LogestFn
7076/// Min args: 1
7077/// Max args: variadic
7078/// Variadic: true
7079/// Signature: LOGEST(arg1...: number@range)
7080/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
7081/// Caps: PURE, NUMERIC_ONLY
7082/// [formualizer-docgen:schema:end]
7083impl Function for LogestFn {
7084    func_caps!(PURE, NUMERIC_ONLY);
7085    fn name(&self) -> &'static str {
7086        "LOGEST"
7087    }
7088    fn min_args(&self) -> usize {
7089        1
7090    }
7091    fn variadic(&self) -> bool {
7092        true
7093    }
7094    fn arg_schema(&self) -> &'static [ArgSchema] {
7095        &ARG_RANGE_NUM_LENIENT_ONE[..]
7096    }
7097    fn eval<'a, 'b, 'c>(
7098        &self,
7099        args: &'c [ArgumentHandle<'a, 'b>],
7100        _ctx: &dyn FunctionContext<'b>,
7101    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
7102        // args[0] = known_y's (required)
7103        // args[1] = known_x's (optional, defaults to {1,2,3,...})
7104        // args[2] = const (optional, default TRUE - whether to compute b)
7105        // args[3] = stats (optional, default FALSE - whether to return additional statistics)
7106
7107        let y_vals = collect_numeric_stats(&args[0..1])?;
7108
7109        if y_vals.is_empty() {
7110            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7111                ExcelError::new_na(),
7112            )));
7113        }
7114
7115        // Check that all y values are positive (required for log transformation)
7116        for &y in &y_vals {
7117            if y <= 0.0 {
7118                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7119                    ExcelError::new_num(),
7120                )));
7121            }
7122        }
7123
7124        // Get known_x's or generate default {1, 2, 3, ...}
7125        let x_vals = if args.len() >= 2 {
7126            collect_numeric_stats(&args[1..2])?
7127        } else {
7128            (1..=y_vals.len()).map(|i| i as f64).collect()
7129        };
7130
7131        // Arrays must have same length
7132        if y_vals.len() != x_vals.len() {
7133            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7134                ExcelError::new_ref(),
7135            )));
7136        }
7137
7138        // Parse const argument (default TRUE)
7139        let use_const = if args.len() >= 3 {
7140            match scalar_like_value(&args[2])? {
7141                LiteralValue::Boolean(b) => b,
7142                LiteralValue::Number(n) => n != 0.0,
7143                LiteralValue::Int(i) => i != 0,
7144                _ => true,
7145            }
7146        } else {
7147            true
7148        };
7149
7150        // Parse stats argument (default FALSE)
7151        let return_stats = if args.len() >= 4 {
7152            match scalar_like_value(&args[3])? {
7153                LiteralValue::Boolean(b) => b,
7154                LiteralValue::Number(n) => n != 0.0,
7155                LiteralValue::Int(i) => i != 0,
7156                _ => false,
7157            }
7158        } else {
7159            false
7160        };
7161
7162        // Transform to log space: ln(y) = ln(b) + x*ln(m)
7163        let ln_y_vals: Vec<f64> = y_vals.iter().map(|&y| y.ln()).collect();
7164
7165        let n = x_vals.len() as f64;
7166
7167        // Calculate regression coefficients in log space
7168        let (ln_m, ln_b) = if use_const {
7169            // Normal linear regression with intercept
7170            let mean_x = x_vals.iter().sum::<f64>() / n;
7171            let mean_ln_y = ln_y_vals.iter().sum::<f64>() / n;
7172
7173            let mut sum_xy = 0.0;
7174            let mut sum_x2 = 0.0;
7175
7176            for i in 0..x_vals.len() {
7177                let dx = x_vals[i] - mean_x;
7178                let dy = ln_y_vals[i] - mean_ln_y;
7179                sum_xy += dx * dy;
7180                sum_x2 += dx * dx;
7181            }
7182
7183            if sum_x2 == 0.0 {
7184                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7185                    ExcelError::new_div(),
7186                )));
7187            }
7188
7189            let ln_m = sum_xy / sum_x2;
7190            let ln_b = mean_ln_y - ln_m * mean_x;
7191            (ln_m, ln_b)
7192        } else {
7193            // Regression through origin in log space (ln_b = 0, so b = 1)
7194            let mut sum_xy = 0.0;
7195            let mut sum_x2 = 0.0;
7196
7197            for i in 0..x_vals.len() {
7198                sum_xy += x_vals[i] * ln_y_vals[i];
7199                sum_x2 += x_vals[i] * x_vals[i];
7200            }
7201
7202            if sum_x2 == 0.0 {
7203                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7204                    ExcelError::new_div(),
7205                )));
7206            }
7207
7208            let ln_m = sum_xy / sum_x2;
7209            (ln_m, 0.0)
7210        };
7211
7212        // Convert from log space to get m and b
7213        let m = ln_m.exp();
7214        let b = ln_b.exp();
7215
7216        if !return_stats {
7217            // Return just m and b as 1x2 array: [[m, b]]
7218            let row = vec![LiteralValue::Number(m), LiteralValue::Number(b)];
7219            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Array(vec![
7220                row,
7221            ])));
7222        }
7223
7224        // Calculate additional statistics for stats=TRUE
7225        // Statistics are computed in log space, then converted
7226        // Row 1: [m, b]
7227        // Row 2: [se_m, se_b] - standard errors (converted from log space)
7228        // Row 3: [r_squared, se_y] - R-squared and standard error of y estimate
7229        // Row 4: [F_statistic, df] - F-statistic and degrees of freedom
7230        // Row 5: [ss_reg, ss_resid] - regression sum of squares and residual sum of squares
7231
7232        let mean_ln_y = ln_y_vals.iter().sum::<f64>() / n;
7233
7234        // Calculate residuals and sums of squares in log space
7235        let mut ss_resid = 0.0;
7236        let mut ss_tot = 0.0;
7237
7238        for i in 0..x_vals.len() {
7239            let ln_y_pred = ln_m * x_vals[i] + ln_b;
7240            let residual = ln_y_vals[i] - ln_y_pred;
7241            ss_resid += residual * residual;
7242            let dy_tot = ln_y_vals[i] - mean_ln_y;
7243            ss_tot += dy_tot * dy_tot;
7244        }
7245
7246        let ss_reg = ss_tot - ss_resid;
7247
7248        // R-squared (same in both spaces for transformed regression)
7249        let r_squared = if ss_tot == 0.0 {
7250            1.0
7251        } else {
7252            1.0 - (ss_resid / ss_tot)
7253        };
7254
7255        // Degrees of freedom
7256        let df = if use_const {
7257            (n as i64 - 2).max(1) as f64
7258        } else {
7259            (n as i64 - 1).max(1) as f64
7260        };
7261
7262        // Standard error of y estimate (in log space)
7263        let se_ln_y = if df > 0.0 {
7264            (ss_resid / df).sqrt()
7265        } else {
7266            0.0
7267        };
7268
7269        // Standard errors of coefficients in log space
7270        let mean_x = x_vals.iter().sum::<f64>() / n;
7271        let mut sum_x2_centered = 0.0;
7272        let mut sum_x2_raw = 0.0;
7273        for &xi in &x_vals {
7274            sum_x2_centered += (xi - mean_x).powi(2);
7275            sum_x2_raw += xi * xi;
7276        }
7277
7278        let se_ln_m = if sum_x2_centered > 0.0 && df > 0.0 {
7279            se_ln_y / sum_x2_centered.sqrt()
7280        } else {
7281            f64::NAN
7282        };
7283
7284        let se_ln_b = if use_const && sum_x2_centered > 0.0 && df > 0.0 {
7285            se_ln_y * (sum_x2_raw / (n * sum_x2_centered)).sqrt()
7286        } else {
7287            f64::NAN
7288        };
7289
7290        // Convert standard errors: se_m = m * se_ln_m (delta method approximation)
7291        let se_m = m * se_ln_m;
7292        let se_b = b * se_ln_b;
7293
7294        // Standard error of y estimate - convert from log space
7295        // This is an approximation; for exponential models, se_y in original space varies with x
7296        let se_y = se_ln_y;
7297
7298        // F-statistic
7299        let f_stat = if ss_resid > 0.0 && df > 0.0 {
7300            (ss_reg / 1.0) / (ss_resid / df)
7301        } else if ss_resid == 0.0 {
7302            f64::INFINITY
7303        } else {
7304            f64::NAN
7305        };
7306
7307        // Build 5x2 result array
7308        let rows = vec![
7309            vec![LiteralValue::Number(m), LiteralValue::Number(b)],
7310            vec![LiteralValue::Number(se_m), LiteralValue::Number(se_b)],
7311            vec![LiteralValue::Number(r_squared), LiteralValue::Number(se_y)],
7312            vec![LiteralValue::Number(f_stat), LiteralValue::Number(df)],
7313            vec![LiteralValue::Number(ss_reg), LiteralValue::Number(ss_resid)],
7314        ];
7315
7316        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Array(rows)))
7317    }
7318}
7319
7320/* ─────────────────────────── PERCENTRANK ──────────────────────────── */
7321
7322/// Returns the inclusive percentile rank of `x` within a numeric data array.
7323///
7324/// `PERCENTRANK.INC` maps values to `[0, 1]` and interpolates linearly between data points.
7325///
7326/// # Remarks
7327/// - `x` must be within the observed min/max range; otherwise returns `#N/A`.
7328/// - Optional `significance` controls decimal truncation and defaults to `3`.
7329/// - `significance` must be at least `1`.
7330/// - Returns `#NUM!` for invalid setup such as empty numeric input.
7331///
7332/// # Examples
7333///
7334/// ```yaml,sandbox
7335/// title: "Exact inclusive percentile rank"
7336/// formula: "=PERCENTRANK.INC({1,2,3,4,5},3)"
7337/// expected: 0.5
7338/// ```
7339///
7340/// ```yaml,sandbox
7341/// title: "Interpolated inclusive percentile rank"
7342/// formula: "=PERCENTRANK.INC({1,2,3,4,5},2.5)"
7343/// expected: 0.375
7344/// ```
7345#[derive(Debug)]
7346pub struct PercentRankIncFn;
7347/// [formualizer-docgen:schema:start]
7348/// Name: PERCENTRANK.INC
7349/// Type: PercentRankIncFn
7350/// Min args: 2
7351/// Max args: variadic
7352/// Variadic: true
7353/// Signature: PERCENTRANK.INC(arg1...: number@range)
7354/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
7355/// Caps: PURE, NUMERIC_ONLY
7356/// [formualizer-docgen:schema:end]
7357impl Function for PercentRankIncFn {
7358    func_caps!(PURE, NUMERIC_ONLY);
7359    fn name(&self) -> &'static str {
7360        "PERCENTRANK.INC"
7361    }
7362    fn aliases(&self) -> &'static [&'static str] {
7363        &["PERCENTRANK"]
7364    }
7365    fn min_args(&self) -> usize {
7366        2
7367    }
7368    fn variadic(&self) -> bool {
7369        true
7370    }
7371    fn arg_schema(&self) -> &'static [ArgSchema] {
7372        &ARG_RANGE_NUM_LENIENT_ONE[..]
7373    }
7374    fn eval<'a, 'b, 'c>(
7375        &self,
7376        args: &'c [ArgumentHandle<'a, 'b>],
7377        _ctx: &dyn FunctionContext<'b>,
7378    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
7379        if args.len() < 2 {
7380            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7381                ExcelError::new_num(),
7382            )));
7383        }
7384
7385        // Get x value (the value to find the rank of)
7386        let x = match coerce_num(&scalar_like_value(&args[1])?) {
7387            Ok(n) => n,
7388            Err(_) => {
7389                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7390                    ExcelError::new_num(),
7391                )));
7392            }
7393        };
7394
7395        // Get optional significance (default 3)
7396        let significance = if args.len() > 2 {
7397            match coerce_num(&scalar_like_value(&args[2])?) {
7398                Ok(n) => {
7399                    let s = n as i32;
7400                    if s < 1 {
7401                        return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7402                            ExcelError::new_num(),
7403                        )));
7404                    }
7405                    s as u32
7406                }
7407                Err(_) => 3,
7408            }
7409        } else {
7410            3
7411        };
7412
7413        // Collect and sort the data array
7414        let mut nums = collect_numeric_stats(&args[0..1])?;
7415        if nums.is_empty() {
7416            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7417                ExcelError::new_num(),
7418            )));
7419        }
7420        nums.sort_by(|a, b| a.partial_cmp(b).unwrap());
7421
7422        let n = nums.len();
7423
7424        // Check if x is outside the range
7425        if x < nums[0] || x > nums[n - 1] {
7426            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7427                ExcelError::new_na(),
7428            )));
7429        }
7430
7431        // Find the rank using linear interpolation
7432        // For PERCENTRANK.INC, the formula is: rank = (position) / (n-1)
7433        // where position is 0-based and uses linear interpolation
7434        let rank = if n == 1 {
7435            // Single element - rank is 0 (or 1.0 if we want, but Excel returns 0)
7436            0.0
7437        } else {
7438            let mut rank_val = 0.0;
7439            for i in 0..n - 1 {
7440                if (nums[i] - x).abs() < 1e-12 {
7441                    // Exact match at position i
7442                    rank_val = (i as f64) / ((n - 1) as f64);
7443                    break;
7444                } else if nums[i] < x && x < nums[i + 1] {
7445                    // Interpolate between positions i and i+1
7446                    let frac = (x - nums[i]) / (nums[i + 1] - nums[i]);
7447                    rank_val = ((i as f64) + frac) / ((n - 1) as f64);
7448                    break;
7449                } else if i == n - 2 && (nums[n - 1] - x).abs() < 1e-12 {
7450                    // Exact match at last position
7451                    rank_val = 1.0;
7452                }
7453            }
7454            rank_val
7455        };
7456
7457        // Truncate to significance decimal places
7458        let multiplier = 10_f64.powi(significance as i32);
7459        let truncated = (rank * multiplier).trunc() / multiplier;
7460
7461        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
7462            truncated,
7463        )))
7464    }
7465}
7466
7467/// Returns the exclusive percentile rank of `x` within a numeric data array.
7468///
7469/// `PERCENTRANK.EXC` uses an open ranking scale that excludes exact `0` and `1` endpoints.
7470///
7471/// # Remarks
7472/// - `x` must lie within the observed min/max range; otherwise returns `#N/A`.
7473/// - Output is based on position divided by `n + 1`, with interpolation between points.
7474/// - Optional `significance` defaults to `3` and must be at least `1`.
7475/// - Returns `#NUM!` for invalid setup such as empty numeric input.
7476///
7477/// # Examples
7478///
7479/// ```yaml,sandbox
7480/// title: "Exact exclusive percentile rank"
7481/// formula: "=PERCENTRANK.EXC({1,2,3,4,5},3)"
7482/// expected: 0.5
7483/// ```
7484///
7485/// ```yaml,sandbox
7486/// title: "Interpolated exclusive percentile rank"
7487/// formula: "=PERCENTRANK.EXC({1,2,3,4,5},2.5)"
7488/// expected: 0.416
7489/// ```
7490#[derive(Debug)]
7491pub struct PercentRankExcFn;
7492/// [formualizer-docgen:schema:start]
7493/// Name: PERCENTRANK.EXC
7494/// Type: PercentRankExcFn
7495/// Min args: 2
7496/// Max args: variadic
7497/// Variadic: true
7498/// Signature: PERCENTRANK.EXC(arg1...: number@range)
7499/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
7500/// Caps: PURE, NUMERIC_ONLY
7501/// [formualizer-docgen:schema:end]
7502impl Function for PercentRankExcFn {
7503    func_caps!(PURE, NUMERIC_ONLY);
7504    fn name(&self) -> &'static str {
7505        "PERCENTRANK.EXC"
7506    }
7507    fn min_args(&self) -> usize {
7508        2
7509    }
7510    fn variadic(&self) -> bool {
7511        true
7512    }
7513    fn arg_schema(&self) -> &'static [ArgSchema] {
7514        &ARG_RANGE_NUM_LENIENT_ONE[..]
7515    }
7516    fn eval<'a, 'b, 'c>(
7517        &self,
7518        args: &'c [ArgumentHandle<'a, 'b>],
7519        _ctx: &dyn FunctionContext<'b>,
7520    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
7521        if args.len() < 2 {
7522            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7523                ExcelError::new_num(),
7524            )));
7525        }
7526
7527        // Get x value (the value to find the rank of)
7528        let x = match coerce_num(&scalar_like_value(&args[1])?) {
7529            Ok(n) => n,
7530            Err(_) => {
7531                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7532                    ExcelError::new_num(),
7533                )));
7534            }
7535        };
7536
7537        // Get optional significance (default 3)
7538        let significance = if args.len() > 2 {
7539            match coerce_num(&scalar_like_value(&args[2])?) {
7540                Ok(n) => {
7541                    let s = n as i32;
7542                    if s < 1 {
7543                        return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7544                            ExcelError::new_num(),
7545                        )));
7546                    }
7547                    s as u32
7548                }
7549                Err(_) => 3,
7550            }
7551        } else {
7552            3
7553        };
7554
7555        // Collect and sort the data array
7556        let mut nums = collect_numeric_stats(&args[0..1])?;
7557        if nums.is_empty() {
7558            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7559                ExcelError::new_num(),
7560            )));
7561        }
7562        nums.sort_by(|a, b| a.partial_cmp(b).unwrap());
7563
7564        let n = nums.len();
7565
7566        // Check if x is outside the range
7567        if x < nums[0] || x > nums[n - 1] {
7568            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7569                ExcelError::new_na(),
7570            )));
7571        }
7572
7573        // For PERCENTRANK.EXC, the formula is: rank = position / (n+1)
7574        // where position is 1-based and uses linear interpolation
7575        let rank = {
7576            let mut rank_val = 0.0;
7577            for i in 0..n {
7578                if (nums[i] - x).abs() < 1e-12 {
7579                    // Exact match at position i (1-based: i+1)
7580                    rank_val = ((i + 1) as f64) / ((n + 1) as f64);
7581                    break;
7582                } else if i < n - 1 && nums[i] < x && x < nums[i + 1] {
7583                    // Interpolate between positions i and i+1 (1-based: i+1 and i+2)
7584                    let frac = (x - nums[i]) / (nums[i + 1] - nums[i]);
7585                    let position = ((i + 1) as f64) + frac;
7586                    rank_val = position / ((n + 1) as f64);
7587                    break;
7588                }
7589            }
7590            rank_val
7591        };
7592
7593        // Truncate to significance decimal places
7594        let multiplier = 10_f64.powi(significance as i32);
7595        let truncated = (rank * multiplier).trunc() / multiplier;
7596
7597        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
7598            truncated,
7599        )))
7600    }
7601}
7602
7603/* ─────────────────────────── FREQUENCY ──────────────────────────── */
7604
7605/// Returns a vertical frequency distribution for numeric data across bin cutoffs.
7606///
7607/// `FREQUENCY` counts values into `<= first bin`, intermediate right-closed bins, and an overflow
7608/// bucket above the final bin.
7609///
7610/// # Remarks
7611/// - Returns an array with `bins + 1` rows.
7612/// - Bins are sorted before counting.
7613/// - If `bins_array` has no numeric values, result is a single count of all data points.
7614/// - Non-numeric values in input ranges are ignored by statistical-collection rules.
7615///
7616/// # Examples
7617///
7618/// ```yaml,sandbox
7619/// title: "Frequency buckets with two bins"
7620/// formula: "=FREQUENCY({1,2,3,4,5},{2,4})"
7621/// expected:
7622///   - [2]
7623///   - [2]
7624///   - [1]
7625/// ```
7626///
7627/// ```yaml,sandbox
7628/// title: "Frequency with repeated values"
7629/// formula: "=FREQUENCY({1,1,2,2,3},{1,2})"
7630/// expected:
7631///   - [2]
7632///   - [2]
7633///   - [1]
7634/// ```
7635#[derive(Debug)]
7636pub struct FrequencyFn;
7637/// [formualizer-docgen:schema:start]
7638/// Name: FREQUENCY
7639/// Type: FrequencyFn
7640/// Min args: 2
7641/// Max args: 1
7642/// Variadic: false
7643/// Signature: FREQUENCY(arg1: number@range)
7644/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
7645/// Caps: PURE, NUMERIC_ONLY
7646/// [formualizer-docgen:schema:end]
7647impl Function for FrequencyFn {
7648    func_caps!(PURE, NUMERIC_ONLY);
7649    fn name(&self) -> &'static str {
7650        "FREQUENCY"
7651    }
7652    fn min_args(&self) -> usize {
7653        2
7654    }
7655    fn variadic(&self) -> bool {
7656        false
7657    }
7658    fn arg_schema(&self) -> &'static [ArgSchema] {
7659        &ARG_RANGE_NUM_LENIENT_ONE[..]
7660    }
7661    fn eval<'a, 'b, 'c>(
7662        &self,
7663        args: &'c [ArgumentHandle<'a, 'b>],
7664        _ctx: &dyn FunctionContext<'b>,
7665    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
7666        if args.len() < 2 {
7667            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7668                ExcelError::new_num(),
7669            )));
7670        }
7671
7672        // Collect data array
7673        let data = collect_numeric_stats(&args[0..1])?;
7674
7675        // Collect bins array
7676        let mut bins = collect_numeric_stats(&args[1..2])?;
7677
7678        // Handle empty bins - return single count of all data
7679        if bins.is_empty() {
7680            let rows = vec![vec![LiteralValue::Number(data.len() as f64)]];
7681            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Array(rows)));
7682        }
7683
7684        // Sort bins
7685        bins.sort_by(|a, b| a.partial_cmp(b).unwrap());
7686
7687        // Calculate frequencies
7688        // Result has bins.len() + 1 elements
7689        let mut frequencies = vec![0usize; bins.len() + 1];
7690
7691        for &value in &data {
7692            // Find which bin the value belongs to
7693            let mut found = false;
7694            for (i, &bin) in bins.iter().enumerate() {
7695                if i == 0 {
7696                    // First bin: count values <= bins[0]
7697                    if value <= bin {
7698                        frequencies[0] += 1;
7699                        found = true;
7700                        break;
7701                    }
7702                } else {
7703                    // Intermediate bins: count values > bins[i-1] AND <= bins[i]
7704                    if value <= bin {
7705                        frequencies[i] += 1;
7706                        found = true;
7707                        break;
7708                    }
7709                }
7710            }
7711            // Last bin: values > bins[last]
7712            if !found {
7713                frequencies[bins.len()] += 1;
7714            }
7715        }
7716
7717        // Return as vertical array (column vector)
7718        let rows: Vec<Vec<LiteralValue>> = frequencies
7719            .into_iter()
7720            .map(|f| vec![LiteralValue::Number(f as f64)])
7721            .collect();
7722
7723        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Array(rows)))
7724    }
7725}
7726
7727/* ─────────────────────────── T.DIST.2T ──────────────────────────── */
7728
7729/// Returns the two-tailed Student's t probability beyond `x`.
7730///
7731/// `T.DIST.2T` computes `P(|T| > x)` for the specified degrees of freedom.
7732///
7733/// # Remarks
7734/// - Requires `x >= 0` and `deg_freedom >= 1`.
7735/// - Represents a two-sided tail area.
7736/// - Returns `#NUM!` when arguments are outside valid ranges.
7737/// - Invalid numeric coercions propagate as spreadsheet errors.
7738///
7739/// # Examples
7740///
7741/// ```yaml,sandbox
7742/// title: "Two-tailed t probability at zero"
7743/// formula: "=T.DIST.2T(0,10)"
7744/// expected: 1
7745/// ```
7746///
7747/// ```yaml,sandbox
7748/// title: "Two-tailed t probability at x=2"
7749/// formula: "=T.DIST.2T(2,10)"
7750/// expected: 0.0733880342639167
7751/// ```
7752#[derive(Debug)]
7753pub struct TDist2TFn;
7754/// [formualizer-docgen:schema:start]
7755/// Name: T.DIST.2T
7756/// Type: TDist2TFn
7757/// Min args: 2
7758/// Max args: 2
7759/// Variadic: false
7760/// Signature: T.DIST.2T(arg1: number@scalar, arg2: number@scalar)
7761/// 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}
7762/// Caps: PURE
7763/// [formualizer-docgen:schema:end]
7764impl Function for TDist2TFn {
7765    func_caps!(PURE);
7766    fn name(&self) -> &'static str {
7767        "T.DIST.2T"
7768    }
7769    fn min_args(&self) -> usize {
7770        2
7771    }
7772    fn arg_schema(&self) -> &'static [ArgSchema] {
7773        use std::sync::LazyLock;
7774        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
7775            vec![
7776                ArgSchema::number_lenient_scalar(),
7777                ArgSchema::number_lenient_scalar(),
7778            ]
7779        });
7780        &SCHEMA[..]
7781    }
7782    fn eval<'a, 'b, 'c>(
7783        &self,
7784        args: &'c [ArgumentHandle<'a, 'b>],
7785        _ctx: &dyn FunctionContext<'b>,
7786    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
7787        let x = coerce_num(&scalar_like_value(&args[0])?)?;
7788        let df = coerce_num(&scalar_like_value(&args[1])?)?;
7789
7790        // x must be non-negative for T.DIST.2T, df must be >= 1
7791        if x < 0.0 || df < 1.0 {
7792            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7793                ExcelError::new_num(),
7794            )));
7795        }
7796
7797        // Two-tailed: P(|T| > x) = 2 * (1 - t_cdf(x, df))
7798        let p = 2.0 * (1.0 - t_cdf(x, df));
7799        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(p)))
7800    }
7801}
7802
7803/* ─────────────────────────── T.INV.2T ──────────────────────────── */
7804
7805/// Returns the positive t critical value for a two-tailed probability.
7806///
7807/// `T.INV.2T` solves for `t` such that `P(|T| > t) = probability`.
7808///
7809/// # Remarks
7810/// - `probability` must satisfy `0 < probability <= 1`.
7811/// - `deg_freedom` must be at least `1`.
7812/// - Returns `#NUM!` for invalid probability or degree-of-freedom arguments.
7813/// - Alias `TINV` is supported.
7814///
7815/// # Examples
7816///
7817/// ```yaml,sandbox
7818/// title: "Maximum two-tailed probability"
7819/// formula: "=T.INV.2T(1,10)"
7820/// expected: 0
7821/// ```
7822///
7823/// ```yaml,sandbox
7824/// title: "95% two-sided critical value"
7825/// formula: "=T.INV.2T(0.05,10)"
7826/// expected: 2.228138851986273
7827/// ```
7828#[derive(Debug)]
7829pub struct TInv2TFn;
7830/// [formualizer-docgen:schema:start]
7831/// Name: T.INV.2T
7832/// Type: TInv2TFn
7833/// Min args: 2
7834/// Max args: 2
7835/// Variadic: false
7836/// Signature: T.INV.2T(arg1: number@scalar, arg2: number@scalar)
7837/// 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}
7838/// Caps: PURE
7839/// [formualizer-docgen:schema:end]
7840impl Function for TInv2TFn {
7841    func_caps!(PURE);
7842    fn name(&self) -> &'static str {
7843        "T.INV.2T"
7844    }
7845    fn aliases(&self) -> &'static [&'static str] {
7846        &["TINV"]
7847    }
7848    fn min_args(&self) -> usize {
7849        2
7850    }
7851    fn arg_schema(&self) -> &'static [ArgSchema] {
7852        use std::sync::LazyLock;
7853        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
7854            vec![
7855                ArgSchema::number_lenient_scalar(),
7856                ArgSchema::number_lenient_scalar(),
7857            ]
7858        });
7859        &SCHEMA[..]
7860    }
7861    fn eval<'a, 'b, 'c>(
7862        &self,
7863        args: &'c [ArgumentHandle<'a, 'b>],
7864        _ctx: &dyn FunctionContext<'b>,
7865    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
7866        let p = coerce_num(&scalar_like_value(&args[0])?)?;
7867        let df = coerce_num(&scalar_like_value(&args[1])?)?;
7868
7869        // probability must be in (0, 1], df >= 1
7870        if p <= 0.0 || p > 1.0 || df < 1.0 {
7871            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7872                ExcelError::new_num(),
7873            )));
7874        }
7875
7876        // For two-tailed: we want t such that P(|T| > t) = p
7877        // P(|T| > t) = 2 * (1 - F(t)) where F is CDF
7878        // So 1 - F(t) = p/2, meaning F(t) = 1 - p/2
7879        // Thus t = t_inv(1 - p/2, df)
7880        match t_inv(1.0 - p / 2.0, df) {
7881            Some(result) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
7882                result,
7883            ))),
7884            None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7885                ExcelError::new_num(),
7886            ))),
7887        }
7888    }
7889}
7890
7891/* ─────────────────────────── T.TEST ──────────────────────────── */
7892
7893/// Returns the p-value from a Student t-test comparing two numeric samples.
7894///
7895/// `T.TEST` supports paired, equal-variance two-sample, and unequal-variance (Welch) modes.
7896///
7897/// # Remarks
7898/// - `tails` must be `1` (one-tailed) or `2` (two-tailed).
7899/// - `type` must be `1` (paired), `2` (two-sample equal variance), or `3` (Welch).
7900/// - Returns `#N/A` when paired mode arrays have different lengths.
7901/// - Returns `#NUM!` or `#DIV/0!` for invalid setup or degenerate variance conditions.
7902///
7903/// # Examples
7904///
7905/// ```yaml,sandbox
7906/// title: "Two-tailed equal-variance test with identical samples"
7907/// formula: "=T.TEST({1,2,3},{1,2,3},2,2)"
7908/// expected: 1
7909/// ```
7910///
7911/// ```yaml,sandbox
7912/// title: "One-tailed Welch test with identical samples"
7913/// formula: "=T.TEST({1,2,3},{1,2,3},1,3)"
7914/// expected: 0.5
7915/// ```
7916#[derive(Debug)]
7917pub struct TTestFn;
7918/// [formualizer-docgen:schema:start]
7919/// Name: T.TEST
7920/// Type: TTestFn
7921/// Min args: 4
7922/// Max args: 4
7923/// Variadic: false
7924/// Signature: T.TEST(arg1: number@range, arg2: number@range, arg3: number@scalar, arg4: number@scalar)
7925/// 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}
7926/// Caps: PURE
7927/// [formualizer-docgen:schema:end]
7928impl Function for TTestFn {
7929    func_caps!(PURE);
7930    fn name(&self) -> &'static str {
7931        "T.TEST"
7932    }
7933    fn aliases(&self) -> &'static [&'static str] {
7934        &["TTEST"]
7935    }
7936    fn min_args(&self) -> usize {
7937        4
7938    }
7939    fn arg_schema(&self) -> &'static [ArgSchema] {
7940        use std::sync::LazyLock;
7941        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
7942            vec![
7943                {
7944                    let mut s = ArgSchema::number_lenient_scalar();
7945                    s.shape = crate::args::ShapeKind::Range;
7946                    s.coercion = formualizer_common::CoercionPolicy::NumberLenientText;
7947                    s
7948                },
7949                {
7950                    let mut s = ArgSchema::number_lenient_scalar();
7951                    s.shape = crate::args::ShapeKind::Range;
7952                    s.coercion = formualizer_common::CoercionPolicy::NumberLenientText;
7953                    s
7954                },
7955                ArgSchema::number_lenient_scalar(), // tails
7956                ArgSchema::number_lenient_scalar(), // type
7957            ]
7958        });
7959        &SCHEMA[..]
7960    }
7961    fn eval<'a, 'b, 'c>(
7962        &self,
7963        args: &'c [ArgumentHandle<'a, 'b>],
7964        _ctx: &dyn FunctionContext<'b>,
7965    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
7966        let array1 = collect_numeric_stats(&args[0..1])?;
7967        let array2 = collect_numeric_stats(&args[1..2])?;
7968        let tails = coerce_num(&scalar_like_value(&args[2])?)? as i32;
7969        let test_type = coerce_num(&scalar_like_value(&args[3])?)? as i32;
7970
7971        // Validate tails (1 or 2) and type (1, 2, or 3)
7972        if !(1..=2).contains(&tails) || !(1..=3).contains(&test_type) {
7973            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7974                ExcelError::new_num(),
7975            )));
7976        }
7977
7978        let n1 = array1.len();
7979        let n2 = array2.len();
7980
7981        // For paired test, arrays must have same length
7982        if test_type == 1 && n1 != n2 {
7983            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7984                ExcelError::new_na(),
7985            )));
7986        }
7987
7988        // Need at least 2 data points for meaningful t-test
7989        if n1 < 2 || n2 < 2 {
7990            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7991                ExcelError::new_num(),
7992            )));
7993        }
7994
7995        let (t_stat, df) = match test_type {
7996            1 => {
7997                // Paired t-test
7998                let n = n1 as f64;
7999                let diffs: Vec<f64> = array1
8000                    .iter()
8001                    .zip(array2.iter())
8002                    .map(|(a, b)| a - b)
8003                    .collect();
8004                let mean_diff = diffs.iter().sum::<f64>() / n;
8005                let var_diff =
8006                    diffs.iter().map(|d| (d - mean_diff).powi(2)).sum::<f64>() / (n - 1.0);
8007                if var_diff == 0.0 {
8008                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8009                        ExcelError::new_div(),
8010                    )));
8011                }
8012                let se = (var_diff / n).sqrt();
8013                (mean_diff / se, n - 1.0)
8014            }
8015            2 => {
8016                // Two-sample equal variance (pooled)
8017                let n1f = n1 as f64;
8018                let n2f = n2 as f64;
8019                let mean1 = array1.iter().sum::<f64>() / n1f;
8020                let mean2 = array2.iter().sum::<f64>() / n2f;
8021                let var1 = array1.iter().map(|x| (x - mean1).powi(2)).sum::<f64>() / (n1f - 1.0);
8022                let var2 = array2.iter().map(|x| (x - mean2).powi(2)).sum::<f64>() / (n2f - 1.0);
8023
8024                // Pooled variance
8025                let sp2 = ((n1f - 1.0) * var1 + (n2f - 1.0) * var2) / (n1f + n2f - 2.0);
8026                if sp2 == 0.0 {
8027                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8028                        ExcelError::new_div(),
8029                    )));
8030                }
8031                let se = (sp2 * (1.0 / n1f + 1.0 / n2f)).sqrt();
8032                ((mean1 - mean2) / se, n1f + n2f - 2.0)
8033            }
8034            3 => {
8035                // Welch's t-test (unequal variance)
8036                let n1f = n1 as f64;
8037                let n2f = n2 as f64;
8038                let mean1 = array1.iter().sum::<f64>() / n1f;
8039                let mean2 = array2.iter().sum::<f64>() / n2f;
8040                let var1 = array1.iter().map(|x| (x - mean1).powi(2)).sum::<f64>() / (n1f - 1.0);
8041                let var2 = array2.iter().map(|x| (x - mean2).powi(2)).sum::<f64>() / (n2f - 1.0);
8042
8043                let s1_n = var1 / n1f;
8044                let s2_n = var2 / n2f;
8045                let se = (s1_n + s2_n).sqrt();
8046                if se == 0.0 {
8047                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8048                        ExcelError::new_div(),
8049                    )));
8050                }
8051
8052                // Welch-Satterthwaite degrees of freedom
8053                let df_num = (s1_n + s2_n).powi(2);
8054                let df_denom = s1_n.powi(2) / (n1f - 1.0) + s2_n.powi(2) / (n2f - 1.0);
8055                let df = if df_denom == 0.0 {
8056                    1.0
8057                } else {
8058                    df_num / df_denom
8059                };
8060                ((mean1 - mean2) / se, df)
8061            }
8062            _ => unreachable!(),
8063        };
8064
8065        // Calculate p-value based on tails
8066        let t_abs = t_stat.abs();
8067        let p = if tails == 1 {
8068            1.0 - t_cdf(t_abs, df)
8069        } else {
8070            2.0 * (1.0 - t_cdf(t_abs, df))
8071        };
8072
8073        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(p)))
8074    }
8075}
8076
8077/* ─────────────────────────── F.TEST ──────────────────────────── */
8078
8079/// Returns the two-tailed p-value from an F-test comparing sample variances.
8080///
8081/// `F.TEST` evaluates whether two samples have significantly different variances.
8082///
8083/// # Remarks
8084/// - Each array must contain at least two numeric values.
8085/// - Uses sample variances and computes a two-tailed probability.
8086/// - Returns `#DIV/0!` when either sample variance is zero.
8087/// - Alias `FTEST` is supported.
8088///
8089/// # Examples
8090///
8091/// ```yaml,sandbox
8092/// title: "Identical samples yield p-value 1"
8093/// formula: "=F.TEST({1,2,3,4},{1,2,3,4})"
8094/// expected: 1
8095/// ```
8096///
8097/// ```yaml,sandbox
8098/// title: "Different variances example"
8099/// formula: "=F.TEST({1,2,3,4},{1,1,1,5})"
8100/// expected: 0.5466810975407987
8101/// ```
8102#[derive(Debug)]
8103pub struct FTestFn;
8104/// [formualizer-docgen:schema:start]
8105/// Name: F.TEST
8106/// Type: FTestFn
8107/// Min args: 2
8108/// Max args: 2
8109/// Variadic: false
8110/// Signature: F.TEST(arg1: number@range, arg2: number@range)
8111/// 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}
8112/// Caps: PURE
8113/// [formualizer-docgen:schema:end]
8114impl Function for FTestFn {
8115    func_caps!(PURE);
8116    fn name(&self) -> &'static str {
8117        "F.TEST"
8118    }
8119    fn aliases(&self) -> &'static [&'static str] {
8120        &["FTEST"]
8121    }
8122    fn min_args(&self) -> usize {
8123        2
8124    }
8125    fn arg_schema(&self) -> &'static [ArgSchema] {
8126        use std::sync::LazyLock;
8127        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
8128            vec![
8129                {
8130                    let mut s = ArgSchema::number_lenient_scalar();
8131                    s.shape = crate::args::ShapeKind::Range;
8132                    s.coercion = formualizer_common::CoercionPolicy::NumberLenientText;
8133                    s
8134                },
8135                {
8136                    let mut s = ArgSchema::number_lenient_scalar();
8137                    s.shape = crate::args::ShapeKind::Range;
8138                    s.coercion = formualizer_common::CoercionPolicy::NumberLenientText;
8139                    s
8140                },
8141            ]
8142        });
8143        &SCHEMA[..]
8144    }
8145    fn eval<'a, 'b, 'c>(
8146        &self,
8147        args: &'c [ArgumentHandle<'a, 'b>],
8148        _ctx: &dyn FunctionContext<'b>,
8149    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
8150        let array1 = collect_numeric_stats(&args[0..1])?;
8151        let array2 = collect_numeric_stats(&args[1..2])?;
8152
8153        let n1 = array1.len();
8154        let n2 = array2.len();
8155
8156        // Need at least 2 points in each array
8157        if n1 < 2 || n2 < 2 {
8158            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8159                ExcelError::new_div(),
8160            )));
8161        }
8162
8163        let n1f = n1 as f64;
8164        let n2f = n2 as f64;
8165
8166        let mean1 = array1.iter().sum::<f64>() / n1f;
8167        let mean2 = array2.iter().sum::<f64>() / n2f;
8168
8169        let var1 = array1.iter().map(|x| (x - mean1).powi(2)).sum::<f64>() / (n1f - 1.0);
8170        let var2 = array2.iter().map(|x| (x - mean2).powi(2)).sum::<f64>() / (n2f - 1.0);
8171
8172        // Handle zero variance
8173        if var1 == 0.0 || var2 == 0.0 {
8174            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8175                ExcelError::new_div(),
8176            )));
8177        }
8178
8179        // F-statistic: Excel's F.TEST uses var1/var2 (order matters for degrees of freedom)
8180        // and returns two-tailed p-value
8181        let f = var1 / var2;
8182        let df1 = n1f - 1.0;
8183        let df2 = n2f - 1.0;
8184
8185        // Two-tailed p-value: min(F.DIST(f), 1-F.DIST(f)) * 2
8186        let p_lower = f_cdf(f, df1, df2);
8187        let p_upper = 1.0 - p_lower;
8188        let p = 2.0 * p_lower.min(p_upper);
8189
8190        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(p)))
8191    }
8192}
8193
8194/* ─────────────────────────── CHISQ.TEST ──────────────────────────── */
8195
8196/// Returns the right-tail p-value from a chi-square goodness-of-fit style comparison.
8197///
8198/// `CHISQ.TEST` compares observed and expected values and computes `1 - CHISQ.DIST(...)`.
8199///
8200/// # Remarks
8201/// - `actual_range` and `expected_range` must contain the same number of numeric points.
8202/// - Expected values must be strictly greater than `0`.
8203/// - Requires at least two categories (`df >= 1`).
8204/// - Returns `#N/A` for length mismatches or empty inputs, and `#NUM!` for invalid expected values.
8205///
8206/// # Examples
8207///
8208/// ```yaml,sandbox
8209/// title: "Perfect match between observed and expected"
8210/// formula: "=CHISQ.TEST({20,30,50},{20,30,50})"
8211/// expected: 1
8212/// ```
8213///
8214/// ```yaml,sandbox
8215/// title: "Two-category chi-square test"
8216/// formula: "=CHISQ.TEST({18,22},{20,20})"
8217/// expected: 0.5270892568655381
8218/// ```
8219#[derive(Debug)]
8220pub struct ChisqTestFn;
8221/// [formualizer-docgen:schema:start]
8222/// Name: CHISQ.TEST
8223/// Type: ChisqTestFn
8224/// Min args: 2
8225/// Max args: 2
8226/// Variadic: false
8227/// Signature: CHISQ.TEST(arg1: number@range, arg2: number@range)
8228/// 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}
8229/// Caps: PURE
8230/// [formualizer-docgen:schema:end]
8231impl Function for ChisqTestFn {
8232    func_caps!(PURE);
8233    fn name(&self) -> &'static str {
8234        "CHISQ.TEST"
8235    }
8236    fn aliases(&self) -> &'static [&'static str] {
8237        &["CHITEST"]
8238    }
8239    fn min_args(&self) -> usize {
8240        2
8241    }
8242    fn arg_schema(&self) -> &'static [ArgSchema] {
8243        use std::sync::LazyLock;
8244        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
8245            vec![
8246                {
8247                    let mut s = ArgSchema::number_lenient_scalar();
8248                    s.shape = crate::args::ShapeKind::Range;
8249                    s.coercion = formualizer_common::CoercionPolicy::NumberLenientText;
8250                    s
8251                },
8252                {
8253                    let mut s = ArgSchema::number_lenient_scalar();
8254                    s.shape = crate::args::ShapeKind::Range;
8255                    s.coercion = formualizer_common::CoercionPolicy::NumberLenientText;
8256                    s
8257                },
8258            ]
8259        });
8260        &SCHEMA[..]
8261    }
8262    fn eval<'a, 'b, 'c>(
8263        &self,
8264        args: &'c [ArgumentHandle<'a, 'b>],
8265        _ctx: &dyn FunctionContext<'b>,
8266    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
8267        let actual = collect_numeric_stats(&args[0..1])?;
8268        let expected = collect_numeric_stats(&args[1..2])?;
8269
8270        // Arrays must have same length
8271        if actual.len() != expected.len() {
8272            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8273                ExcelError::new_na(),
8274            )));
8275        }
8276
8277        if actual.is_empty() {
8278            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8279                ExcelError::new_na(),
8280            )));
8281        }
8282
8283        // Calculate chi-squared statistic: sum((observed - expected)^2 / expected)
8284        let mut chi_sq = 0.0;
8285        for (obs, exp) in actual.iter().zip(expected.iter()) {
8286            if *exp <= 0.0 {
8287                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8288                    ExcelError::new_num(),
8289                )));
8290            }
8291            chi_sq += (obs - exp).powi(2) / exp;
8292        }
8293
8294        // Degrees of freedom = number of categories - 1
8295        let df = (actual.len() - 1) as f64;
8296
8297        if df < 1.0 {
8298            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8299                ExcelError::new_num(),
8300            )));
8301        }
8302
8303        // P-value = 1 - CHISQ.DIST(chi_sq, df, TRUE) = right-tail probability
8304        let p = 1.0 - chisq_cdf(chi_sq, df);
8305
8306        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(p)))
8307    }
8308}
8309
8310/* ═══════════════════════════════════════════════════════════════════════════
8311   FZ-PAR-01: Statistical compatibility batch
8312   AVERAGEA, MAXA, MINA, STDEVA, STDEVPA, VARA, VARPA, SKEW.P,
8313   T.DIST.RT, CHISQ.DIST.RT, CHISQ.INV.RT, F.DIST.RT, F.INV.RT,
8314   BETA.INV, BINOM.DIST.RANGE, BINOM.INV, GAMMA, GAMMA.INV,
8315   GAMMALN, GAMMALN.PRECISE
8316═══════════════════════════════════════════════════════════════════════════ */
8317
8318/// Collect numeric inputs applying Excel "A"-variant semantics:
8319/// - Range references: include numbers as-is, booleans as 0/1, text as 0. Errors propagate.
8320///   Blank cells are skipped.
8321/// - Direct scalar arguments: same coercion as standard collect_numeric_stats.
8322fn collect_numeric_a(args: &[ArgumentHandle]) -> Result<Vec<f64>, ExcelError> {
8323    let mut out = Vec::new();
8324    for a in args {
8325        if let Some(arr) = a.inline_array_literal()? {
8326            for row in arr.into_iter() {
8327                for cell in row.into_iter() {
8328                    match cell {
8329                        LiteralValue::Error(e) => return Err(e),
8330                        LiteralValue::Number(n) => out.push(n),
8331                        LiteralValue::Int(i) => out.push(i as f64),
8332                        LiteralValue::Boolean(b) => out.push(if b { 1.0 } else { 0.0 }),
8333                        LiteralValue::Text(_) => out.push(0.0),
8334                        _ => {}
8335                    }
8336                }
8337            }
8338            continue;
8339        }
8340
8341        if let Ok(view) = a.range_view() {
8342            view.for_each_cell(&mut |v| {
8343                match v {
8344                    LiteralValue::Error(e) => return Err(e.clone()),
8345                    LiteralValue::Number(n) => out.push(*n),
8346                    LiteralValue::Int(i) => out.push(*i as f64),
8347                    LiteralValue::Boolean(b) => out.push(if *b { 1.0 } else { 0.0 }),
8348                    LiteralValue::Text(_) => out.push(0.0),
8349                    LiteralValue::Empty => {} // skip blanks
8350                    _ => {}
8351                }
8352                Ok(())
8353            })?;
8354        } else {
8355            let v = scalar_like_value(a)?;
8356            match v {
8357                LiteralValue::Error(e) => return Err(e),
8358                other => {
8359                    if let Ok(n) = coerce_num(&other) {
8360                        out.push(n);
8361                    }
8362                }
8363            }
8364        }
8365    }
8366    Ok(out)
8367}
8368
8369/// Helper: inverse of the regularized incomplete beta function.
8370/// Given p = I_x(a,b), find x. Uses Newton-Raphson with beta_i / beta PDF.
8371fn beta_inv_helper(p: f64, a: f64, b: f64) -> Option<f64> {
8372    if p <= 0.0 {
8373        return Some(0.0);
8374    }
8375    if p >= 1.0 {
8376        return Some(1.0);
8377    }
8378    if a <= 0.0 || b <= 0.0 {
8379        return None;
8380    }
8381
8382    // Initial guess from normal approximation (Abramowitz & Stegun 26.5.22)
8383    let mut x = 0.5f64;
8384
8385    // Newton-Raphson
8386    let ln_beta_ab = ln_gamma(a) + ln_gamma(b) - ln_gamma(a + b);
8387    for _ in 0..100 {
8388        let cdf = beta_i(x, a, b);
8389        // Beta PDF: x^(a-1) * (1-x)^(b-1) / B(a,b)
8390        let pdf = if x > 0.0 && x < 1.0 {
8391            ((a - 1.0) * x.ln() + (b - 1.0) * (1.0 - x).ln() - ln_beta_ab).exp()
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).clamp(1e-15, 1.0 - 1e-15);
8400        if (new_x - x).abs() < 1e-14 {
8401            x = new_x;
8402            break;
8403        }
8404        x = new_x;
8405    }
8406
8407    Some(x)
8408}
8409
8410/// Helper: inverse of GAMMA.DIST CDF. Given p = P(alpha, x/beta), find x.
8411fn gamma_inv_helper(p: f64, alpha: f64, beta: f64) -> Option<f64> {
8412    if p <= 0.0 {
8413        return Some(0.0);
8414    }
8415    if p >= 1.0 {
8416        return None;
8417    }
8418    if alpha <= 0.0 || beta <= 0.0 {
8419        return None;
8420    }
8421
8422    // Initial guess
8423    let mut x = alpha * beta;
8424    if p < 0.5 {
8425        x = x.min(beta);
8426    }
8427
8428    // Newton-Raphson on the standardized gamma CDF (gamma_p)
8429    for _ in 0..100 {
8430        let z = x / beta;
8431        let cdf = gamma_p(alpha, z);
8432        // Gamma PDF: z^(alpha-1) * e^(-z) / Gamma(alpha) / beta
8433        let pdf = if z > 0.0 {
8434            ((alpha - 1.0) * z.ln() - z - ln_gamma(alpha)).exp() / beta
8435        } else {
8436            1e-30
8437        };
8438        if pdf.abs() < 1e-30 {
8439            break;
8440        }
8441        let delta = (cdf - p) / pdf;
8442        let new_x = (x - delta).max(1e-15);
8443        if (new_x - x).abs() < 1e-12 * x.max(1e-15) {
8444            x = new_x;
8445            break;
8446        }
8447        x = new_x;
8448    }
8449
8450    Some(x)
8451}
8452
8453/* ─────────────────────────── AVERAGEA ──────────────────────────── */
8454
8455/// Returns the arithmetic mean while treating logical values and text as numeric inputs.
8456///
8457/// # Formula example
8458/// ```excel
8459/// # returns: 1
8460/// =AVERAGEA(TRUE,2,"x")
8461/// ```
8462///
8463/// ```yaml,sandbox
8464/// title: "Average with logical and text coercion"
8465/// formula: '=AVERAGEA(TRUE,2,"x")'
8466/// expected: 1
8467/// ```
8468///
8469/// ```yaml,docs
8470/// related:
8471///   - AVERAGE
8472///   - MAXA
8473///   - MINA
8474/// faq:
8475///   - q: "How does AVERAGEA treat text and booleans?"
8476///     a: "TRUE counts as 1, FALSE and text count as 0, and blanks are ignored."
8477/// ```
8478#[derive(Debug)]
8479pub struct AverageAFn;
8480/// [formualizer-docgen:schema:start]
8481/// Name: AVERAGEA
8482/// Type: AverageAFn
8483/// Min args: 1
8484/// Max args: variadic
8485/// Variadic: true
8486/// Signature: AVERAGEA(arg1...: number@range)
8487/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
8488/// Caps: PURE, REDUCTION
8489/// [formualizer-docgen:schema:end]
8490impl Function for AverageAFn {
8491    func_caps!(PURE, REDUCTION);
8492    fn name(&self) -> &'static str {
8493        "AVERAGEA"
8494    }
8495    fn min_args(&self) -> usize {
8496        1
8497    }
8498    fn variadic(&self) -> bool {
8499        true
8500    }
8501    fn arg_schema(&self) -> &'static [ArgSchema] {
8502        &ARG_RANGE_NUM_LENIENT_ONE[..]
8503    }
8504    fn eval<'a, 'b, 'c>(
8505        &self,
8506        args: &'c [ArgumentHandle<'a, 'b>],
8507        _ctx: &dyn FunctionContext<'b>,
8508    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
8509        let nums = collect_numeric_a(args)?;
8510        if nums.is_empty() {
8511            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8512                ExcelError::new_div(),
8513            )));
8514        }
8515        let avg = nums.iter().sum::<f64>() / nums.len() as f64;
8516        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(avg)))
8517    }
8518}
8519
8520/* ─────────────────────────── MAXA ──────────────────────────── */
8521
8522/// Returns the largest value after applying A-variant coercion rules.
8523///
8524/// # Formula example
8525/// ```excel
8526/// # returns: 1
8527/// =MAXA(TRUE,-2,"x")
8528/// ```
8529///
8530/// ```yaml,sandbox
8531/// title: "Maximum with logical and text coercion"
8532/// formula: '=MAXA(TRUE,-2,"x")'
8533/// expected: 1
8534/// ```
8535///
8536/// ```yaml,docs
8537/// related:
8538///   - MAX
8539///   - MINA
8540///   - AVERAGEA
8541/// faq:
8542///   - q: "What do text values contribute to MAXA?"
8543///     a: "Text contributes 0, so negative numeric inputs can still be smaller than text in the aggregate."
8544/// ```
8545#[derive(Debug)]
8546pub struct MaxAFn;
8547/// [formualizer-docgen:schema:start]
8548/// Name: MAXA
8549/// Type: MaxAFn
8550/// Min args: 1
8551/// Max args: variadic
8552/// Variadic: true
8553/// Signature: MAXA(arg1...: number@range)
8554/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
8555/// Caps: PURE, REDUCTION
8556/// [formualizer-docgen:schema:end]
8557impl Function for MaxAFn {
8558    func_caps!(PURE, REDUCTION);
8559    fn name(&self) -> &'static str {
8560        "MAXA"
8561    }
8562    fn min_args(&self) -> usize {
8563        1
8564    }
8565    fn variadic(&self) -> bool {
8566        true
8567    }
8568    fn arg_schema(&self) -> &'static [ArgSchema] {
8569        &ARG_RANGE_NUM_LENIENT_ONE[..]
8570    }
8571    fn eval<'a, 'b, 'c>(
8572        &self,
8573        args: &'c [ArgumentHandle<'a, 'b>],
8574        _ctx: &dyn FunctionContext<'b>,
8575    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
8576        let nums = collect_numeric_a(args)?;
8577        if nums.is_empty() {
8578            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0)));
8579        }
8580        let mx = nums.iter().copied().fold(f64::NEG_INFINITY, f64::max);
8581        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(mx)))
8582    }
8583}
8584
8585/* ─────────────────────────── MINA ──────────────────────────── */
8586
8587/// Returns the smallest value after applying A-variant coercion rules.
8588///
8589/// # Formula example
8590/// ```excel
8591/// # returns: -2
8592/// =MINA(TRUE,-2,"x")
8593/// ```
8594///
8595/// ```yaml,sandbox
8596/// title: "Minimum with logical and text coercion"
8597/// formula: '=MINA(TRUE,-2,"x")'
8598/// expected: -2
8599/// ```
8600///
8601/// ```yaml,docs
8602/// related:
8603///   - MIN
8604///   - MAXA
8605///   - AVERAGEA
8606/// faq:
8607///   - q: "Do text values affect MINA?"
8608///     a: "Yes. Text is coerced to 0, so it can become the minimum when all numeric inputs are positive."
8609/// ```
8610#[derive(Debug)]
8611pub struct MinAFn;
8612/// [formualizer-docgen:schema:start]
8613/// Name: MINA
8614/// Type: MinAFn
8615/// Min args: 1
8616/// Max args: variadic
8617/// Variadic: true
8618/// Signature: MINA(arg1...: number@range)
8619/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
8620/// Caps: PURE, REDUCTION
8621/// [formualizer-docgen:schema:end]
8622impl Function for MinAFn {
8623    func_caps!(PURE, REDUCTION);
8624    fn name(&self) -> &'static str {
8625        "MINA"
8626    }
8627    fn min_args(&self) -> usize {
8628        1
8629    }
8630    fn variadic(&self) -> bool {
8631        true
8632    }
8633    fn arg_schema(&self) -> &'static [ArgSchema] {
8634        &ARG_RANGE_NUM_LENIENT_ONE[..]
8635    }
8636    fn eval<'a, 'b, 'c>(
8637        &self,
8638        args: &'c [ArgumentHandle<'a, 'b>],
8639        _ctx: &dyn FunctionContext<'b>,
8640    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
8641        let nums = collect_numeric_a(args)?;
8642        if nums.is_empty() {
8643            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0)));
8644        }
8645        let mn = nums.iter().copied().fold(f64::INFINITY, f64::min);
8646        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(mn)))
8647    }
8648}
8649
8650/* ─────────────────────────── STDEVA ──────────────────────────── */
8651
8652/// Returns the sample standard deviation using A-variant coercion semantics.
8653///
8654/// # Formula example
8655/// ```excel
8656/// # returns: 1
8657/// =STDEVA(TRUE,2,"x")
8658/// ```
8659///
8660/// ```yaml,sandbox
8661/// title: "Sample deviation with coerced values"
8662/// formula: '=STDEVA(TRUE,2,"x")'
8663/// expected: 1
8664/// ```
8665///
8666/// ```yaml,docs
8667/// related:
8668///   - STDEV.P
8669///   - STDEVPA
8670///   - VARA
8671/// faq:
8672///   - q: "When does STDEVA return #DIV/0!?"
8673///     a: "It returns #DIV/0! when fewer than two coerced values remain after evaluation."
8674/// ```
8675#[derive(Debug)]
8676pub struct StdevAFn;
8677/// [formualizer-docgen:schema:start]
8678/// Name: STDEVA
8679/// Type: StdevAFn
8680/// Min args: 1
8681/// Max args: variadic
8682/// Variadic: true
8683/// Signature: STDEVA(arg1...: number@range)
8684/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
8685/// Caps: PURE, REDUCTION
8686/// [formualizer-docgen:schema:end]
8687impl Function for StdevAFn {
8688    func_caps!(PURE, REDUCTION);
8689    fn name(&self) -> &'static str {
8690        "STDEVA"
8691    }
8692    fn min_args(&self) -> usize {
8693        1
8694    }
8695    fn variadic(&self) -> bool {
8696        true
8697    }
8698    fn arg_schema(&self) -> &'static [ArgSchema] {
8699        &ARG_RANGE_NUM_LENIENT_ONE[..]
8700    }
8701    fn eval<'a, 'b, 'c>(
8702        &self,
8703        args: &'c [ArgumentHandle<'a, 'b>],
8704        _ctx: &dyn FunctionContext<'b>,
8705    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
8706        let nums = collect_numeric_a(args)?;
8707        let n = nums.len();
8708        if n < 2 {
8709            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8710                ExcelError::new_div(),
8711            )));
8712        }
8713        let mean = nums.iter().sum::<f64>() / n as f64;
8714        let ss: f64 = nums.iter().map(|v| (v - mean).powi(2)).sum();
8715        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
8716            (ss / (n - 1) as f64).sqrt(),
8717        )))
8718    }
8719}
8720
8721/* ─────────────────────────── STDEVPA ──────────────────────────── */
8722
8723/// Returns the population standard deviation using A-variant coercion semantics.
8724///
8725/// # Formula example
8726/// ```excel
8727/// # returns: 0.816496580927726
8728/// =STDEVPA(TRUE,2,"x")
8729/// ```
8730///
8731/// ```yaml,sandbox
8732/// title: "Population deviation with coerced values"
8733/// formula: '=STDEVPA(TRUE,2,"x")'
8734/// expected: 0.816496580927726
8735/// ```
8736///
8737/// ```yaml,docs
8738/// related:
8739///   - STDEVA
8740///   - VARPA
8741///   - STDEV.P
8742/// faq:
8743///   - q: "What is the difference between STDEVA and STDEVPA?"
8744///     a: "STDEVA uses the sample denominator n-1, while STDEVPA uses the population denominator n."
8745/// ```
8746#[derive(Debug)]
8747pub struct StdevPAFn;
8748/// [formualizer-docgen:schema:start]
8749/// Name: STDEVPA
8750/// Type: StdevPAFn
8751/// Min args: 1
8752/// Max args: variadic
8753/// Variadic: true
8754/// Signature: STDEVPA(arg1...: number@range)
8755/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
8756/// Caps: PURE, REDUCTION
8757/// [formualizer-docgen:schema:end]
8758impl Function for StdevPAFn {
8759    func_caps!(PURE, REDUCTION);
8760    fn name(&self) -> &'static str {
8761        "STDEVPA"
8762    }
8763    fn min_args(&self) -> usize {
8764        1
8765    }
8766    fn variadic(&self) -> bool {
8767        true
8768    }
8769    fn arg_schema(&self) -> &'static [ArgSchema] {
8770        &ARG_RANGE_NUM_LENIENT_ONE[..]
8771    }
8772    fn eval<'a, 'b, 'c>(
8773        &self,
8774        args: &'c [ArgumentHandle<'a, 'b>],
8775        _ctx: &dyn FunctionContext<'b>,
8776    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
8777        let nums = collect_numeric_a(args)?;
8778        let n = nums.len();
8779        if n == 0 {
8780            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8781                ExcelError::new_div(),
8782            )));
8783        }
8784        let mean = nums.iter().sum::<f64>() / n as f64;
8785        let ss: f64 = nums.iter().map(|v| (v - mean).powi(2)).sum();
8786        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
8787            (ss / n as f64).sqrt(),
8788        )))
8789    }
8790}
8791
8792/* ─────────────────────────── VARA ──────────────────────────── */
8793
8794/// Returns the sample variance using A-variant coercion semantics.
8795///
8796/// # Formula example
8797/// ```excel
8798/// # returns: 1
8799/// =VARA(TRUE,2,"x")
8800/// ```
8801///
8802/// ```yaml,sandbox
8803/// title: "Sample variance with coerced values"
8804/// formula: '=VARA(TRUE,2,"x")'
8805/// expected: 1
8806/// ```
8807///
8808/// ```yaml,docs
8809/// related:
8810///   - VARPA
8811///   - STDEVA
8812///   - AVERAGEA
8813/// faq:
8814///   - q: "How are blanks handled in VARA?"
8815///     a: "Blanks are ignored, while booleans and text are coerced under A-variant rules."
8816/// ```
8817#[derive(Debug)]
8818pub struct VarAFn;
8819/// [formualizer-docgen:schema:start]
8820/// Name: VARA
8821/// Type: VarAFn
8822/// Min args: 1
8823/// Max args: variadic
8824/// Variadic: true
8825/// Signature: VARA(arg1...: number@range)
8826/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
8827/// Caps: PURE, REDUCTION
8828/// [formualizer-docgen:schema:end]
8829impl Function for VarAFn {
8830    func_caps!(PURE, REDUCTION);
8831    fn name(&self) -> &'static str {
8832        "VARA"
8833    }
8834    fn min_args(&self) -> usize {
8835        1
8836    }
8837    fn variadic(&self) -> bool {
8838        true
8839    }
8840    fn arg_schema(&self) -> &'static [ArgSchema] {
8841        &ARG_RANGE_NUM_LENIENT_ONE[..]
8842    }
8843    fn eval<'a, 'b, 'c>(
8844        &self,
8845        args: &'c [ArgumentHandle<'a, 'b>],
8846        _ctx: &dyn FunctionContext<'b>,
8847    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
8848        let nums = collect_numeric_a(args)?;
8849        let n = nums.len();
8850        if n < 2 {
8851            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8852                ExcelError::new_div(),
8853            )));
8854        }
8855        let mean = nums.iter().sum::<f64>() / n as f64;
8856        let ss: f64 = nums.iter().map(|v| (v - mean).powi(2)).sum();
8857        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
8858            ss / (n - 1) as f64,
8859        )))
8860    }
8861}
8862
8863/* ─────────────────────────── VARPA ──────────────────────────── */
8864
8865/// Returns the population variance using A-variant coercion semantics.
8866///
8867/// # Formula example
8868/// ```excel
8869/// # returns: 0.6666666666666666
8870/// =VARPA(TRUE,2,"x")
8871/// ```
8872///
8873/// ```yaml,sandbox
8874/// title: "Population variance with coerced values"
8875/// formula: '=VARPA(TRUE,2,"x")'
8876/// expected: 0.6666666666666666
8877/// ```
8878///
8879/// ```yaml,docs
8880/// related:
8881///   - VARA
8882///   - STDEVPA
8883///   - STDEV.P
8884/// faq:
8885///   - q: "When does VARPA return #DIV/0!?"
8886///     a: "It returns #DIV/0! when no coerced values remain after evaluation."
8887/// ```
8888#[derive(Debug)]
8889pub struct VarPAFn;
8890/// [formualizer-docgen:schema:start]
8891/// Name: VARPA
8892/// Type: VarPAFn
8893/// Min args: 1
8894/// Max args: variadic
8895/// Variadic: true
8896/// Signature: VARPA(arg1...: number@range)
8897/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
8898/// Caps: PURE, REDUCTION
8899/// [formualizer-docgen:schema:end]
8900impl Function for VarPAFn {
8901    func_caps!(PURE, REDUCTION);
8902    fn name(&self) -> &'static str {
8903        "VARPA"
8904    }
8905    fn min_args(&self) -> usize {
8906        1
8907    }
8908    fn variadic(&self) -> bool {
8909        true
8910    }
8911    fn arg_schema(&self) -> &'static [ArgSchema] {
8912        &ARG_RANGE_NUM_LENIENT_ONE[..]
8913    }
8914    fn eval<'a, 'b, 'c>(
8915        &self,
8916        args: &'c [ArgumentHandle<'a, 'b>],
8917        _ctx: &dyn FunctionContext<'b>,
8918    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
8919        let nums = collect_numeric_a(args)?;
8920        let n = nums.len();
8921        if n == 0 {
8922            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8923                ExcelError::new_div(),
8924            )));
8925        }
8926        let mean = nums.iter().sum::<f64>() / n as f64;
8927        let ss: f64 = nums.iter().map(|v| (v - mean).powi(2)).sum();
8928        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
8929            ss / n as f64,
8930        )))
8931    }
8932}
8933
8934/* ─────────────────────────── SKEW.P ──────────────────────────── */
8935
8936/// Returns the population skewness of a numeric data set.
8937///
8938/// # Formula example
8939/// ```excel
8940/// # returns: 0
8941/// =SKEW.P(1,2,3)
8942/// ```
8943///
8944/// ```yaml,sandbox
8945/// title: "Symmetric data has zero skew"
8946/// formula: '=SKEW.P(1,2,3)'
8947/// expected: 0
8948/// ```
8949///
8950/// ```yaml,docs
8951/// related:
8952///   - SKEW
8953///   - KURT
8954///   - AVERAGE
8955/// faq:
8956///   - q: "When does SKEW.P return #DIV/0!?"
8957///     a: "It returns #DIV/0! when fewer than three numeric values are available or the population standard deviation is zero."
8958/// ```
8959#[derive(Debug)]
8960pub struct SkewPFn;
8961/// [formualizer-docgen:schema:start]
8962/// Name: SKEW.P
8963/// Type: SkewPFn
8964/// Min args: 1
8965/// Max args: variadic
8966/// Variadic: true
8967/// Signature: SKEW.P(arg1...: number@range)
8968/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
8969/// Caps: PURE, REDUCTION, NUMERIC_ONLY
8970/// [formualizer-docgen:schema:end]
8971impl Function for SkewPFn {
8972    func_caps!(PURE, REDUCTION, NUMERIC_ONLY);
8973    fn name(&self) -> &'static str {
8974        "SKEW.P"
8975    }
8976    fn min_args(&self) -> usize {
8977        1
8978    }
8979    fn variadic(&self) -> bool {
8980        true
8981    }
8982    fn arg_schema(&self) -> &'static [ArgSchema] {
8983        &ARG_RANGE_NUM_LENIENT_ONE[..]
8984    }
8985    fn eval<'a, 'b, 'c>(
8986        &self,
8987        args: &'c [ArgumentHandle<'a, 'b>],
8988        _ctx: &dyn FunctionContext<'b>,
8989    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
8990        let nums = collect_numeric_stats(args)?;
8991        let n = nums.len();
8992        if n < 3 {
8993            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8994                ExcelError::new_div(),
8995            )));
8996        }
8997        let n_f = n as f64;
8998        let mean = nums.iter().sum::<f64>() / n_f;
8999        let mut sum_sq = 0.0;
9000        for &v in &nums {
9001            sum_sq += (v - mean).powi(2);
9002        }
9003        let stdev_pop = (sum_sq / n_f).sqrt();
9004        if stdev_pop == 0.0 {
9005            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9006                ExcelError::new_div(),
9007            )));
9008        }
9009        let mut sum_cubed = 0.0;
9010        for &v in &nums {
9011            sum_cubed += ((v - mean) / stdev_pop).powi(3);
9012        }
9013        // Population skewness: (1/n) * sum((xi - mean)/sigma)^3
9014        let skew = sum_cubed / n_f;
9015        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(skew)))
9016    }
9017}
9018
9019/* ─────────────────────────── T.DIST.RT ──────────────────────────── */
9020
9021/// Returns the right-tailed Student's t-distribution probability.
9022///
9023/// # Formula example
9024/// ```excel
9025/// # returns: 0.5
9026/// =T.DIST.RT(0,10)
9027/// ```
9028///
9029/// ```yaml,sandbox
9030/// title: "Zero lies at the midpoint of the t distribution"
9031/// formula: '=T.DIST.RT(0,10)'
9032/// expected: 0.5
9033/// ```
9034///
9035/// ```yaml,docs
9036/// related:
9037///   - T.DIST.2T
9038///   - T.INV
9039///   - T.INV.2T
9040/// faq:
9041///   - q: "When does T.DIST.RT return #NUM!?"
9042///     a: "It returns #NUM! when the degrees of freedom are less than 1."
9043/// ```
9044#[derive(Debug)]
9045pub struct TDistRtFn;
9046/// [formualizer-docgen:schema:start]
9047/// Name: T.DIST.RT
9048/// Type: TDistRtFn
9049/// Min args: 2
9050/// Max args: 2
9051/// Variadic: false
9052/// Signature: T.DIST.RT(arg1: number@scalar, arg2: number@scalar)
9053/// 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}
9054/// Caps: PURE
9055/// [formualizer-docgen:schema:end]
9056impl Function for TDistRtFn {
9057    func_caps!(PURE);
9058    fn name(&self) -> &'static str {
9059        "T.DIST.RT"
9060    }
9061    fn min_args(&self) -> usize {
9062        2
9063    }
9064    fn arg_schema(&self) -> &'static [ArgSchema] {
9065        use std::sync::LazyLock;
9066        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
9067            vec![
9068                ArgSchema::number_lenient_scalar(),
9069                ArgSchema::number_lenient_scalar(),
9070            ]
9071        });
9072        &SCHEMA[..]
9073    }
9074    fn eval<'a, 'b, 'c>(
9075        &self,
9076        args: &'c [ArgumentHandle<'a, 'b>],
9077        _ctx: &dyn FunctionContext<'b>,
9078    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
9079        let x = coerce_num(&scalar_like_value(&args[0])?)?;
9080        let df = coerce_num(&scalar_like_value(&args[1])?)?;
9081        if df < 1.0 {
9082            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9083                ExcelError::new_num(),
9084            )));
9085        }
9086        let result = 1.0 - t_cdf(x, df);
9087        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
9088            result,
9089        )))
9090    }
9091}
9092
9093/* ─────────────────────────── CHISQ.DIST.RT ──────────────────────────── */
9094
9095/// Returns the right-tailed chi-squared distribution probability.
9096///
9097/// # Formula example
9098/// ```excel
9099/// # returns: 0.36787944117144233
9100/// =CHISQ.DIST.RT(2,2)
9101/// ```
9102///
9103/// ```yaml,sandbox
9104/// title: "Right-tail chi-squared probability"
9105/// formula: '=CHISQ.DIST.RT(2,2)'
9106/// expected: 0.36787944117144233
9107/// ```
9108///
9109/// ```yaml,docs
9110/// related:
9111///   - CHISQ.INV.RT
9112///   - CHISQ.TEST
9113///   - CHISQ.DIST
9114/// faq:
9115///   - q: "Which inputs return #NUM!?"
9116///     a: "Negative x values and degrees of freedom below 1 return #NUM!."
9117/// ```
9118#[derive(Debug)]
9119pub struct ChisqDistRtFn;
9120/// [formualizer-docgen:schema:start]
9121/// Name: CHISQ.DIST.RT
9122/// Type: ChisqDistRtFn
9123/// Min args: 2
9124/// Max args: 2
9125/// Variadic: false
9126/// Signature: CHISQ.DIST.RT(arg1: number@scalar, arg2: number@scalar)
9127/// 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}
9128/// Caps: PURE
9129/// [formualizer-docgen:schema:end]
9130impl Function for ChisqDistRtFn {
9131    func_caps!(PURE);
9132    fn name(&self) -> &'static str {
9133        "CHISQ.DIST.RT"
9134    }
9135    fn min_args(&self) -> usize {
9136        2
9137    }
9138    fn arg_schema(&self) -> &'static [ArgSchema] {
9139        use std::sync::LazyLock;
9140        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
9141            vec![
9142                ArgSchema::number_lenient_scalar(),
9143                ArgSchema::number_lenient_scalar(),
9144            ]
9145        });
9146        &SCHEMA[..]
9147    }
9148    fn eval<'a, 'b, 'c>(
9149        &self,
9150        args: &'c [ArgumentHandle<'a, 'b>],
9151        _ctx: &dyn FunctionContext<'b>,
9152    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
9153        let x = coerce_num(&scalar_like_value(&args[0])?)?;
9154        let df = coerce_num(&scalar_like_value(&args[1])?)?;
9155        if df < 1.0 || x < 0.0 {
9156            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9157                ExcelError::new_num(),
9158            )));
9159        }
9160        let result = 1.0 - chisq_cdf(x, df);
9161        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
9162            result,
9163        )))
9164    }
9165}
9166
9167/* ─────────────────────────── CHISQ.INV.RT ──────────────────────────── */
9168
9169/// Returns the inverse of the right-tailed chi-squared distribution.
9170///
9171/// # Formula example
9172/// ```excel
9173/// # returns: 1.3862943611198906
9174/// =CHISQ.INV.RT(0.5,2)
9175/// ```
9176///
9177/// ```yaml,sandbox
9178/// title: "Median right-tail inverse for 2 degrees of freedom"
9179/// formula: '=CHISQ.INV.RT(0.5,2)'
9180/// expected: 1.3862943611198906
9181/// ```
9182///
9183/// ```yaml,docs
9184/// related:
9185///   - CHISQ.DIST.RT
9186///   - CHISQ.INV
9187///   - CHISQ.TEST
9188/// faq:
9189///   - q: "What p-values are valid for CHISQ.INV.RT?"
9190///     a: "p must lie in the range 0 to 1, and p=0 returns #NUM! because the right-tail inverse diverges."
9191/// ```
9192#[derive(Debug)]
9193pub struct ChisqInvRtFn;
9194/// [formualizer-docgen:schema:start]
9195/// Name: CHISQ.INV.RT
9196/// Type: ChisqInvRtFn
9197/// Min args: 2
9198/// Max args: 2
9199/// Variadic: false
9200/// Signature: CHISQ.INV.RT(arg1: number@scalar, arg2: number@scalar)
9201/// 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}
9202/// Caps: PURE
9203/// [formualizer-docgen:schema:end]
9204impl Function for ChisqInvRtFn {
9205    func_caps!(PURE);
9206    fn name(&self) -> &'static str {
9207        "CHISQ.INV.RT"
9208    }
9209    fn min_args(&self) -> usize {
9210        2
9211    }
9212    fn arg_schema(&self) -> &'static [ArgSchema] {
9213        use std::sync::LazyLock;
9214        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
9215            vec![
9216                ArgSchema::number_lenient_scalar(),
9217                ArgSchema::number_lenient_scalar(),
9218            ]
9219        });
9220        &SCHEMA[..]
9221    }
9222    fn eval<'a, 'b, 'c>(
9223        &self,
9224        args: &'c [ArgumentHandle<'a, 'b>],
9225        _ctx: &dyn FunctionContext<'b>,
9226    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
9227        let p = coerce_num(&scalar_like_value(&args[0])?)?;
9228        let df = coerce_num(&scalar_like_value(&args[1])?)?;
9229        if df < 1.0 || !(0.0..=1.0).contains(&p) {
9230            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9231                ExcelError::new_num(),
9232            )));
9233        }
9234        // Right-tail: CHISQ.INV.RT(p, df) = CHISQ.INV(1-p, df)
9235        if p == 0.0 {
9236            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9237                ExcelError::new_num(),
9238            )));
9239        }
9240        match chisq_inv(1.0 - p, df) {
9241            Some(result) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
9242                result,
9243            ))),
9244            None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9245                ExcelError::new_num(),
9246            ))),
9247        }
9248    }
9249}
9250
9251/* ─────────────────────────── F.DIST.RT ──────────────────────────── */
9252
9253/// Returns the right-tailed F-distribution probability.
9254///
9255/// # Formula example
9256/// ```excel
9257/// # returns: 1
9258/// =F.DIST.RT(0,5,10)
9259/// ```
9260///
9261/// ```yaml,sandbox
9262/// title: "Zero leaves the entire right tail"
9263/// formula: '=F.DIST.RT(0,5,10)'
9264/// expected: 1
9265/// ```
9266///
9267/// ```yaml,docs
9268/// related:
9269///   - F.INV.RT
9270///   - F.TEST
9271///   - F.DIST
9272/// faq:
9273///   - q: "Which inputs return #NUM!?"
9274///     a: "Negative x values or degrees of freedom below 1 return #NUM!."
9275/// ```
9276#[derive(Debug)]
9277pub struct FDistRtFn;
9278/// [formualizer-docgen:schema:start]
9279/// Name: F.DIST.RT
9280/// Type: FDistRtFn
9281/// Min args: 3
9282/// Max args: 3
9283/// Variadic: false
9284/// Signature: F.DIST.RT(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
9285/// 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}
9286/// Caps: PURE
9287/// [formualizer-docgen:schema:end]
9288impl Function for FDistRtFn {
9289    func_caps!(PURE);
9290    fn name(&self) -> &'static str {
9291        "F.DIST.RT"
9292    }
9293    fn min_args(&self) -> usize {
9294        3
9295    }
9296    fn arg_schema(&self) -> &'static [ArgSchema] {
9297        use std::sync::LazyLock;
9298        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
9299            vec![
9300                ArgSchema::number_lenient_scalar(),
9301                ArgSchema::number_lenient_scalar(),
9302                ArgSchema::number_lenient_scalar(),
9303            ]
9304        });
9305        &SCHEMA[..]
9306    }
9307    fn eval<'a, 'b, 'c>(
9308        &self,
9309        args: &'c [ArgumentHandle<'a, 'b>],
9310        _ctx: &dyn FunctionContext<'b>,
9311    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
9312        let x = coerce_num(&scalar_like_value(&args[0])?)?;
9313        let d1 = coerce_num(&scalar_like_value(&args[1])?)?;
9314        let d2 = coerce_num(&scalar_like_value(&args[2])?)?;
9315        if d1 < 1.0 || d2 < 1.0 || x < 0.0 {
9316            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9317                ExcelError::new_num(),
9318            )));
9319        }
9320        let result = 1.0 - f_cdf(x, d1, d2);
9321        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
9322            result,
9323        )))
9324    }
9325}
9326
9327/* ─────────────────────────── F.INV.RT ──────────────────────────── */
9328
9329/// Returns the inverse of the right-tailed F-distribution.
9330///
9331/// # Formula example
9332/// ```excel
9333/// # returns: 0
9334/// =F.INV.RT(1,5,10)
9335/// ```
9336///
9337/// ```yaml,sandbox
9338/// title: "A full right tail maps to zero"
9339/// formula: '=F.INV.RT(1,5,10)'
9340/// expected: 0
9341/// ```
9342///
9343/// ```yaml,docs
9344/// related:
9345///   - F.DIST.RT
9346///   - F.INV
9347///   - F.TEST
9348/// faq:
9349///   - q: "What p-values are valid for F.INV.RT?"
9350///     a: "p must lie in the range 0 to 1, and p=0 returns #NUM! because the inverse diverges."
9351/// ```
9352#[derive(Debug)]
9353pub struct FInvRtFn;
9354/// [formualizer-docgen:schema:start]
9355/// Name: F.INV.RT
9356/// Type: FInvRtFn
9357/// Min args: 3
9358/// Max args: 3
9359/// Variadic: false
9360/// Signature: F.INV.RT(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
9361/// 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}
9362/// Caps: PURE
9363/// [formualizer-docgen:schema:end]
9364impl Function for FInvRtFn {
9365    func_caps!(PURE);
9366    fn name(&self) -> &'static str {
9367        "F.INV.RT"
9368    }
9369    fn min_args(&self) -> usize {
9370        3
9371    }
9372    fn arg_schema(&self) -> &'static [ArgSchema] {
9373        use std::sync::LazyLock;
9374        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
9375            vec![
9376                ArgSchema::number_lenient_scalar(),
9377                ArgSchema::number_lenient_scalar(),
9378                ArgSchema::number_lenient_scalar(),
9379            ]
9380        });
9381        &SCHEMA[..]
9382    }
9383    fn eval<'a, 'b, 'c>(
9384        &self,
9385        args: &'c [ArgumentHandle<'a, 'b>],
9386        _ctx: &dyn FunctionContext<'b>,
9387    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
9388        let p = coerce_num(&scalar_like_value(&args[0])?)?;
9389        let d1 = coerce_num(&scalar_like_value(&args[1])?)?;
9390        let d2 = coerce_num(&scalar_like_value(&args[2])?)?;
9391        if d1 < 1.0 || d2 < 1.0 || !(0.0..=1.0).contains(&p) {
9392            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9393                ExcelError::new_num(),
9394            )));
9395        }
9396        if p == 0.0 {
9397            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9398                ExcelError::new_num(),
9399            )));
9400        }
9401        // F.INV.RT(1, d1, d2) = 0 (entire right tail)
9402        if p == 1.0 {
9403            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0)));
9404        }
9405        // F.INV.RT(p, d1, d2) = F.INV(1-p, d1, d2)
9406        match f_inv(1.0 - p, d1, d2) {
9407            Some(result) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
9408                result,
9409            ))),
9410            None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9411                ExcelError::new_num(),
9412            ))),
9413        }
9414    }
9415}
9416
9417/* ─────────────────────────── BETA.INV ──────────────────────────── */
9418
9419/// Returns the inverse cumulative beta distribution, optionally scaled to custom bounds.
9420///
9421/// # Formula example
9422/// ```excel
9423/// # returns: 0.5
9424/// =BETA.INV(0.5,2,2)
9425/// ```
9426///
9427/// ```yaml,sandbox
9428/// title: "Symmetric beta inverse at the median"
9429/// formula: '=BETA.INV(0.5,2,2)'
9430/// expected: 0.5
9431/// ```
9432///
9433/// ```yaml,docs
9434/// related:
9435///   - BETA.DIST
9436///   - GAMMA.INV
9437///   - NORM.INV
9438/// faq:
9439///   - q: "When does BETA.INV return #NUM!?"
9440///     a: "It returns #NUM! for non-positive alpha or beta, invalid bounds, or probabilities outside 0..1."
9441/// ```
9442#[derive(Debug)]
9443pub struct BetaInvFn;
9444/// [formualizer-docgen:schema:start]
9445/// Name: BETA.INV
9446/// Type: BetaInvFn
9447/// Min args: 3
9448/// Max args: variadic
9449/// Variadic: true
9450/// Signature: BETA.INV(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar, arg5...: number@scalar)
9451/// 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}
9452/// Caps: PURE
9453/// [formualizer-docgen:schema:end]
9454impl Function for BetaInvFn {
9455    func_caps!(PURE);
9456    fn name(&self) -> &'static str {
9457        "BETA.INV"
9458    }
9459    fn min_args(&self) -> usize {
9460        3
9461    }
9462    fn variadic(&self) -> bool {
9463        true
9464    }
9465    fn arg_schema(&self) -> &'static [ArgSchema] {
9466        use std::sync::LazyLock;
9467        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
9468            vec![
9469                ArgSchema::number_lenient_scalar(),
9470                ArgSchema::number_lenient_scalar(),
9471                ArgSchema::number_lenient_scalar(),
9472                ArgSchema::number_lenient_scalar(),
9473                ArgSchema::number_lenient_scalar(),
9474            ]
9475        });
9476        &SCHEMA[..]
9477    }
9478    fn eval<'a, 'b, 'c>(
9479        &self,
9480        args: &'c [ArgumentHandle<'a, 'b>],
9481        _ctx: &dyn FunctionContext<'b>,
9482    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
9483        let p = coerce_num(&scalar_like_value(&args[0])?)?;
9484        let alpha = coerce_num(&scalar_like_value(&args[1])?)?;
9485        let beta_param = coerce_num(&scalar_like_value(&args[2])?)?;
9486        let a_bound = if args.len() > 3 {
9487            coerce_num(&scalar_like_value(&args[3])?)?
9488        } else {
9489            0.0
9490        };
9491        let b_bound = if args.len() > 4 {
9492            coerce_num(&scalar_like_value(&args[4])?)?
9493        } else {
9494            1.0
9495        };
9496
9497        if alpha <= 0.0 || beta_param <= 0.0 || a_bound >= b_bound || !(0.0..=1.0).contains(&p) {
9498            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9499                ExcelError::new_num(),
9500            )));
9501        }
9502
9503        match beta_inv_helper(p, alpha, beta_param) {
9504            Some(x_std) => {
9505                let result = a_bound + x_std * (b_bound - a_bound);
9506                Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
9507                    result,
9508                )))
9509            }
9510            None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9511                ExcelError::new_num(),
9512            ))),
9513        }
9514    }
9515}
9516
9517/* ─────────────────────────── BINOM.DIST.RANGE ──────────────────────────── */
9518
9519/// Returns the probability that a binomial random variable falls within a range of successes.
9520///
9521/// # Formula example
9522/// ```excel
9523/// # returns: 0.1171875
9524/// =BINOM.DIST.RANGE(10,0.5,3,3)
9525/// ```
9526///
9527/// ```yaml,sandbox
9528/// title: "Probability of exactly three successes"
9529/// formula: '=BINOM.DIST.RANGE(10,0.5,3,3)'
9530/// expected: 0.1171875
9531/// ```
9532///
9533/// ```yaml,docs
9534/// related:
9535///   - BINOM.DIST
9536///   - BINOM.INV
9537///   - POISSON.DIST
9538/// faq:
9539///   - q: "What happens if the upper bound is omitted?"
9540///     a: "The function treats the lower and upper bounds as the same value, yielding the probability of exactly that many successes."
9541/// ```
9542#[derive(Debug)]
9543pub struct BinomDistRangeFn;
9544/// [formualizer-docgen:schema:start]
9545/// Name: BINOM.DIST.RANGE
9546/// Type: BinomDistRangeFn
9547/// Min args: 3
9548/// Max args: variadic
9549/// Variadic: true
9550/// Signature: BINOM.DIST.RANGE(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4...: number@scalar)
9551/// 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}
9552/// Caps: PURE
9553/// [formualizer-docgen:schema:end]
9554impl Function for BinomDistRangeFn {
9555    func_caps!(PURE);
9556    fn name(&self) -> &'static str {
9557        "BINOM.DIST.RANGE"
9558    }
9559    fn min_args(&self) -> usize {
9560        3
9561    }
9562    fn variadic(&self) -> bool {
9563        true
9564    }
9565    fn arg_schema(&self) -> &'static [ArgSchema] {
9566        use std::sync::LazyLock;
9567        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
9568            vec![
9569                ArgSchema::number_lenient_scalar(),
9570                ArgSchema::number_lenient_scalar(),
9571                ArgSchema::number_lenient_scalar(),
9572                ArgSchema::number_lenient_scalar(),
9573            ]
9574        });
9575        &SCHEMA[..]
9576    }
9577    fn eval<'a, 'b, 'c>(
9578        &self,
9579        args: &'c [ArgumentHandle<'a, 'b>],
9580        _ctx: &dyn FunctionContext<'b>,
9581    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
9582        let n = coerce_num(&scalar_like_value(&args[0])?)?.trunc() as i64;
9583        let p = coerce_num(&scalar_like_value(&args[1])?)?;
9584        let s = coerce_num(&scalar_like_value(&args[2])?)?.trunc() as i64;
9585        let s2 = if args.len() > 3 {
9586            coerce_num(&scalar_like_value(&args[3])?)?.trunc() as i64
9587        } else {
9588            s
9589        };
9590
9591        if n < 0 || !(0.0..=1.0).contains(&p) || s < 0 || s > n || s2 < s || s2 > n {
9592            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9593                ExcelError::new_num(),
9594            )));
9595        }
9596
9597        let mut sum = 0.0;
9598        for k in s..=s2 {
9599            let ln_prob = ln_binom(n, k) + (k as f64) * p.ln() + ((n - k) as f64) * (1.0 - p).ln();
9600            sum += ln_prob.exp();
9601        }
9602        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(sum)))
9603    }
9604}
9605
9606/* ─────────────────────────── BINOM.INV ──────────────────────────── */
9607
9608/// Returns the smallest number of successes whose cumulative binomial probability meets a threshold.
9609///
9610/// # Formula example
9611/// ```excel
9612/// # returns: 5
9613/// =BINOM.INV(10,0.5,0.5)
9614/// ```
9615///
9616/// ```yaml,sandbox
9617/// title: "Median success threshold"
9618/// formula: '=BINOM.INV(10,0.5,0.5)'
9619/// expected: 5
9620/// ```
9621///
9622/// ```yaml,docs
9623/// related:
9624///   - BINOM.DIST
9625///   - BINOM.DIST.RANGE
9626///   - CRITBINOM
9627/// faq:
9628///   - q: "Is CRITBINOM supported?"
9629///     a: "Yes. CRITBINOM is registered as an alias of BINOM.INV."
9630/// ```
9631#[derive(Debug)]
9632pub struct BinomInvFn;
9633/// [formualizer-docgen:schema:start]
9634/// Name: BINOM.INV
9635/// Type: BinomInvFn
9636/// Min args: 3
9637/// Max args: 3
9638/// Variadic: false
9639/// Signature: BINOM.INV(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
9640/// 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}
9641/// Caps: PURE
9642/// [formualizer-docgen:schema:end]
9643impl Function for BinomInvFn {
9644    func_caps!(PURE);
9645    fn name(&self) -> &'static str {
9646        "BINOM.INV"
9647    }
9648    fn aliases(&self) -> &'static [&'static str] {
9649        &["CRITBINOM"]
9650    }
9651    fn min_args(&self) -> usize {
9652        3
9653    }
9654    fn arg_schema(&self) -> &'static [ArgSchema] {
9655        use std::sync::LazyLock;
9656        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
9657            vec![
9658                ArgSchema::number_lenient_scalar(),
9659                ArgSchema::number_lenient_scalar(),
9660                ArgSchema::number_lenient_scalar(),
9661            ]
9662        });
9663        &SCHEMA[..]
9664    }
9665    fn eval<'a, 'b, 'c>(
9666        &self,
9667        args: &'c [ArgumentHandle<'a, 'b>],
9668        _ctx: &dyn FunctionContext<'b>,
9669    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
9670        let n = coerce_num(&scalar_like_value(&args[0])?)?.trunc() as i64;
9671        let p = coerce_num(&scalar_like_value(&args[1])?)?;
9672        let alpha = coerce_num(&scalar_like_value(&args[2])?)?;
9673
9674        if n < 0 || !(0.0..=1.0).contains(&p) || !(0.0..=1.0).contains(&alpha) {
9675            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9676                ExcelError::new_num(),
9677            )));
9678        }
9679
9680        // Find smallest k such that BINOM.DIST(k, n, p, TRUE) >= alpha
9681        let mut cum = 0.0;
9682        for k in 0..=n {
9683            let ln_prob = ln_binom(n, k) + (k as f64) * p.ln() + ((n - k) as f64) * (1.0 - p).ln();
9684            cum += ln_prob.exp();
9685            if cum >= alpha {
9686                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
9687                    k as f64,
9688                )));
9689            }
9690        }
9691        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
9692            n as f64,
9693        )))
9694    }
9695}
9696
9697/* ─────────────────────────── GAMMA ──────────────────────────── */
9698
9699/// Returns the value of the gamma function.
9700///
9701/// # Formula example
9702/// ```excel
9703/// # returns: 24
9704/// =GAMMA(5)
9705/// ```
9706///
9707/// ```yaml,sandbox
9708/// title: "Gamma extends factorials"
9709/// formula: '=GAMMA(5)'
9710/// expected: 24
9711/// ```
9712///
9713/// ```yaml,docs
9714/// related:
9715///   - GAMMALN
9716///   - GAMMA.INV
9717///   - FACT
9718/// faq:
9719///   - q: "When does GAMMA return #NUM!?"
9720///     a: "It returns #NUM! for zero and negative integers, where the gamma function has poles."
9721/// ```
9722#[derive(Debug)]
9723pub struct GammaFn;
9724/// [formualizer-docgen:schema:start]
9725/// Name: GAMMA
9726/// Type: GammaFn
9727/// Min args: 1
9728/// Max args: 1
9729/// Variadic: false
9730/// Signature: GAMMA(arg1: number@scalar)
9731/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
9732/// Caps: PURE
9733/// [formualizer-docgen:schema:end]
9734impl Function for GammaFn {
9735    func_caps!(PURE);
9736    fn name(&self) -> &'static str {
9737        "GAMMA"
9738    }
9739    fn min_args(&self) -> usize {
9740        1
9741    }
9742    fn arg_schema(&self) -> &'static [ArgSchema] {
9743        use std::sync::LazyLock;
9744        static SCHEMA: LazyLock<Vec<ArgSchema>> =
9745            LazyLock::new(|| vec![ArgSchema::number_lenient_scalar()]);
9746        &SCHEMA[..]
9747    }
9748    fn eval<'a, 'b, 'c>(
9749        &self,
9750        args: &'c [ArgumentHandle<'a, 'b>],
9751        _ctx: &dyn FunctionContext<'b>,
9752    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
9753        let x = coerce_num(&scalar_like_value(&args[0])?)?;
9754        // GAMMA(0) and negative integers are #NUM!
9755        if x == 0.0 || (x < 0.0 && x == x.trunc()) {
9756            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9757                ExcelError::new_num(),
9758            )));
9759        }
9760        let result = ln_gamma(x).exp();
9761        if result.is_infinite() || result.is_nan() {
9762            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9763                ExcelError::new_num(),
9764            )));
9765        }
9766        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
9767            result,
9768        )))
9769    }
9770}
9771
9772/* ─────────────────────────── GAMMA.INV ──────────────────────────── */
9773
9774/// Returns the inverse cumulative gamma distribution.
9775///
9776/// # Formula example
9777/// ```excel
9778/// # returns: 0.6931471805599453
9779/// =GAMMA.INV(0.5,1,1)
9780/// ```
9781///
9782/// ```yaml,sandbox
9783/// title: "Exponential special case"
9784/// formula: '=GAMMA.INV(0.5,1,1)'
9785/// expected: 0.6931471805599453
9786/// ```
9787///
9788/// ```yaml,docs
9789/// related:
9790///   - GAMMA.DIST
9791///   - GAMMA
9792///   - BETA.INV
9793/// faq:
9794///   - q: "Is GAMMAINV supported?"
9795///     a: "Yes. GAMMAINV is registered as an alias of GAMMA.INV."
9796/// ```
9797#[derive(Debug)]
9798pub struct GammaInvFn;
9799/// [formualizer-docgen:schema:start]
9800/// Name: GAMMA.INV
9801/// Type: GammaInvFn
9802/// Min args: 3
9803/// Max args: 3
9804/// Variadic: false
9805/// Signature: GAMMA.INV(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
9806/// 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}
9807/// Caps: PURE
9808/// [formualizer-docgen:schema:end]
9809impl Function for GammaInvFn {
9810    func_caps!(PURE);
9811    fn name(&self) -> &'static str {
9812        "GAMMA.INV"
9813    }
9814    fn aliases(&self) -> &'static [&'static str] {
9815        &["GAMMAINV"]
9816    }
9817    fn min_args(&self) -> usize {
9818        3
9819    }
9820    fn arg_schema(&self) -> &'static [ArgSchema] {
9821        use std::sync::LazyLock;
9822        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
9823            vec![
9824                ArgSchema::number_lenient_scalar(),
9825                ArgSchema::number_lenient_scalar(),
9826                ArgSchema::number_lenient_scalar(),
9827            ]
9828        });
9829        &SCHEMA[..]
9830    }
9831    fn eval<'a, 'b, 'c>(
9832        &self,
9833        args: &'c [ArgumentHandle<'a, 'b>],
9834        _ctx: &dyn FunctionContext<'b>,
9835    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
9836        let p = coerce_num(&scalar_like_value(&args[0])?)?;
9837        let alpha = coerce_num(&scalar_like_value(&args[1])?)?;
9838        let beta = coerce_num(&scalar_like_value(&args[2])?)?;
9839
9840        if alpha <= 0.0 || beta <= 0.0 || !(0.0..=1.0).contains(&p) {
9841            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9842                ExcelError::new_num(),
9843            )));
9844        }
9845
9846        match gamma_inv_helper(p, alpha, beta) {
9847            Some(result) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
9848                result,
9849            ))),
9850            None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9851                ExcelError::new_num(),
9852            ))),
9853        }
9854    }
9855}
9856
9857/* ─────────────────────────── GAMMALN ──────────────────────────── */
9858
9859/// Returns the natural logarithm of the gamma function.
9860///
9861/// # Formula example
9862/// ```excel
9863/// # returns: 3.1780538303479458
9864/// =GAMMALN(5)
9865/// ```
9866///
9867/// ```yaml,sandbox
9868/// title: "Log gamma at 5"
9869/// formula: '=GAMMALN(5)'
9870/// expected: 3.1780538303479458
9871/// ```
9872///
9873/// ```yaml,docs
9874/// related:
9875///   - GAMMALN.PRECISE
9876///   - GAMMA
9877///   - LN
9878/// faq:
9879///   - q: "When does GAMMALN return #NUM!?"
9880///     a: "It returns #NUM! for zero or negative inputs."
9881/// ```
9882#[derive(Debug)]
9883pub struct GammaLnFn;
9884/// [formualizer-docgen:schema:start]
9885/// Name: GAMMALN
9886/// Type: GammaLnFn
9887/// Min args: 1
9888/// Max args: 1
9889/// Variadic: false
9890/// Signature: GAMMALN(arg1: number@scalar)
9891/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
9892/// Caps: PURE
9893/// [formualizer-docgen:schema:end]
9894impl Function for GammaLnFn {
9895    func_caps!(PURE);
9896    fn name(&self) -> &'static str {
9897        "GAMMALN"
9898    }
9899    fn min_args(&self) -> usize {
9900        1
9901    }
9902    fn arg_schema(&self) -> &'static [ArgSchema] {
9903        use std::sync::LazyLock;
9904        static SCHEMA: LazyLock<Vec<ArgSchema>> =
9905            LazyLock::new(|| vec![ArgSchema::number_lenient_scalar()]);
9906        &SCHEMA[..]
9907    }
9908    fn eval<'a, 'b, 'c>(
9909        &self,
9910        args: &'c [ArgumentHandle<'a, 'b>],
9911        _ctx: &dyn FunctionContext<'b>,
9912    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
9913        let x = coerce_num(&scalar_like_value(&args[0])?)?;
9914        if x <= 0.0 {
9915            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9916                ExcelError::new_num(),
9917            )));
9918        }
9919        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
9920            ln_gamma(x),
9921        )))
9922    }
9923}
9924
9925/* ─────────────────────────── GAMMALN.PRECISE ──────────────────────────── */
9926
9927/// Returns the natural logarithm of the gamma function using Excel's precise naming variant.
9928///
9929/// # Formula example
9930/// ```excel
9931/// # returns: 3.1780538303479458
9932/// =GAMMALN.PRECISE(5)
9933/// ```
9934///
9935/// ```yaml,sandbox
9936/// title: "Precise log gamma at 5"
9937/// formula: '=GAMMALN.PRECISE(5)'
9938/// expected: 3.1780538303479458
9939/// ```
9940///
9941/// ```yaml,docs
9942/// related:
9943///   - GAMMALN
9944///   - GAMMA
9945///   - LN
9946/// faq:
9947///   - q: "How does GAMMALN.PRECISE differ here?"
9948///     a: "This implementation uses the same core log-gamma calculation as GAMMALN, matching Excel's function naming split."
9949/// ```
9950#[derive(Debug)]
9951pub struct GammaLnPreciseFn;
9952/// [formualizer-docgen:schema:start]
9953/// Name: GAMMALN.PRECISE
9954/// Type: GammaLnPreciseFn
9955/// Min args: 1
9956/// Max args: 1
9957/// Variadic: false
9958/// Signature: GAMMALN.PRECISE(arg1: number@scalar)
9959/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
9960/// Caps: PURE
9961/// [formualizer-docgen:schema:end]
9962impl Function for GammaLnPreciseFn {
9963    func_caps!(PURE);
9964    fn name(&self) -> &'static str {
9965        "GAMMALN.PRECISE"
9966    }
9967    fn min_args(&self) -> usize {
9968        1
9969    }
9970    fn arg_schema(&self) -> &'static [ArgSchema] {
9971        use std::sync::LazyLock;
9972        static SCHEMA: LazyLock<Vec<ArgSchema>> =
9973            LazyLock::new(|| vec![ArgSchema::number_lenient_scalar()]);
9974        &SCHEMA[..]
9975    }
9976    fn eval<'a, 'b, 'c>(
9977        &self,
9978        args: &'c [ArgumentHandle<'a, 'b>],
9979        _ctx: &dyn FunctionContext<'b>,
9980    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
9981        let x = coerce_num(&scalar_like_value(&args[0])?)?;
9982        if x <= 0.0 {
9983            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9984                ExcelError::new_num(),
9985            )));
9986        }
9987        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
9988            ln_gamma(x),
9989        )))
9990    }
9991}
9992
9993pub fn register_builtins() {
9994    use std::sync::Arc;
9995    crate::function_registry::register_function(Arc::new(ForecastLinearFn));
9996    crate::function_registry::register_function(Arc::new(LinestFn));
9997    crate::function_registry::register_function(Arc::new(LARGE));
9998    crate::function_registry::register_function(Arc::new(SMALL));
9999    crate::function_registry::register_function(Arc::new(MEDIAN));
10000    crate::function_registry::register_function(Arc::new(StdevSample));
10001    crate::function_registry::register_function(Arc::new(StdevPop));
10002    crate::function_registry::register_function(Arc::new(VarSample));
10003    crate::function_registry::register_function(Arc::new(VarPop));
10004    crate::function_registry::register_function(Arc::new(PercentileInc));
10005    crate::function_registry::register_function(Arc::new(PercentileExc));
10006    crate::function_registry::register_function(Arc::new(QuartileInc));
10007    crate::function_registry::register_function(Arc::new(QuartileExc));
10008    crate::function_registry::register_function(Arc::new(RankEqFn));
10009    crate::function_registry::register_function(Arc::new(RankAvgFn));
10010    crate::function_registry::register_function(Arc::new(ModeSingleFn));
10011    crate::function_registry::register_function(Arc::new(ModeMultiFn));
10012    crate::function_registry::register_function(Arc::new(ProductFn));
10013    crate::function_registry::register_function(Arc::new(GeomeanFn));
10014    crate::function_registry::register_function(Arc::new(HarmeanFn));
10015    crate::function_registry::register_function(Arc::new(AvedevFn));
10016    crate::function_registry::register_function(Arc::new(DevsqFn));
10017    crate::function_registry::register_function(Arc::new(MaxIfsFn));
10018    crate::function_registry::register_function(Arc::new(MinIfsFn));
10019    crate::function_registry::register_function(Arc::new(TrimmeanFn));
10020    crate::function_registry::register_function(Arc::new(CorrelFn));
10021    crate::function_registry::register_function(Arc::new(SlopeFn));
10022    crate::function_registry::register_function(Arc::new(InterceptFn));
10023    // Covariance and correlation functions
10024    crate::function_registry::register_function(Arc::new(CovariancePFn));
10025    crate::function_registry::register_function(Arc::new(CovarianceSFn));
10026    crate::function_registry::register_function(Arc::new(PearsonFn));
10027    crate::function_registry::register_function(Arc::new(RsqFn));
10028    crate::function_registry::register_function(Arc::new(SteyxFn));
10029    crate::function_registry::register_function(Arc::new(SkewFn));
10030    crate::function_registry::register_function(Arc::new(KurtFn));
10031    crate::function_registry::register_function(Arc::new(FisherFn));
10032    crate::function_registry::register_function(Arc::new(FisherInvFn));
10033    // Statistical distributions
10034    crate::function_registry::register_function(Arc::new(NormSDistFn));
10035    crate::function_registry::register_function(Arc::new(NormSInvFn));
10036    crate::function_registry::register_function(Arc::new(NormDistFn));
10037    crate::function_registry::register_function(Arc::new(NormInvFn));
10038    crate::function_registry::register_function(Arc::new(LognormDistFn));
10039    crate::function_registry::register_function(Arc::new(LognormInvFn));
10040    crate::function_registry::register_function(Arc::new(PhiFn));
10041    crate::function_registry::register_function(Arc::new(GaussFn));
10042    crate::function_registry::register_function(Arc::new(StandardizeFn));
10043    crate::function_registry::register_function(Arc::new(TDistFn));
10044    crate::function_registry::register_function(Arc::new(TInvFn));
10045    crate::function_registry::register_function(Arc::new(ChisqDistFn));
10046    crate::function_registry::register_function(Arc::new(ChisqInvFn));
10047    crate::function_registry::register_function(Arc::new(FDistFn));
10048    crate::function_registry::register_function(Arc::new(FInvFn));
10049    // Discrete distributions
10050    crate::function_registry::register_function(Arc::new(BinomDistFn));
10051    crate::function_registry::register_function(Arc::new(PoissonDistFn));
10052    crate::function_registry::register_function(Arc::new(ExponDistFn));
10053    crate::function_registry::register_function(Arc::new(GammaDistFn));
10054    // Additional distributions
10055    crate::function_registry::register_function(Arc::new(WeibullDistFn));
10056    crate::function_registry::register_function(Arc::new(BetaDistFn));
10057    crate::function_registry::register_function(Arc::new(NegbinomDistFn));
10058    crate::function_registry::register_function(Arc::new(HypgeomDistFn));
10059    // Confidence intervals and hypothesis testing
10060    crate::function_registry::register_function(Arc::new(ConfidenceNormFn));
10061    crate::function_registry::register_function(Arc::new(ConfidenceTFn));
10062    crate::function_registry::register_function(Arc::new(ZTestFn));
10063    // Regression and trend functions
10064    crate::function_registry::register_function(Arc::new(TrendFn));
10065    crate::function_registry::register_function(Arc::new(GrowthFn));
10066    crate::function_registry::register_function(Arc::new(LogestFn));
10067    // Percent rank and frequency functions
10068    crate::function_registry::register_function(Arc::new(PercentRankIncFn));
10069    crate::function_registry::register_function(Arc::new(PercentRankExcFn));
10070    crate::function_registry::register_function(Arc::new(FrequencyFn));
10071    // Hypothesis testing functions
10072    crate::function_registry::register_function(Arc::new(TDist2TFn));
10073    crate::function_registry::register_function(Arc::new(TInv2TFn));
10074    crate::function_registry::register_function(Arc::new(TTestFn));
10075    crate::function_registry::register_function(Arc::new(FTestFn));
10076    crate::function_registry::register_function(Arc::new(ChisqTestFn));
10077    // FZ-PAR-01 batch
10078    crate::function_registry::register_function(Arc::new(AverageAFn));
10079    crate::function_registry::register_function(Arc::new(MaxAFn));
10080    crate::function_registry::register_function(Arc::new(MinAFn));
10081    crate::function_registry::register_function(Arc::new(StdevAFn));
10082    crate::function_registry::register_function(Arc::new(StdevPAFn));
10083    crate::function_registry::register_function(Arc::new(VarAFn));
10084    crate::function_registry::register_function(Arc::new(VarPAFn));
10085    crate::function_registry::register_function(Arc::new(SkewPFn));
10086    crate::function_registry::register_function(Arc::new(TDistRtFn));
10087    crate::function_registry::register_function(Arc::new(ChisqDistRtFn));
10088    crate::function_registry::register_function(Arc::new(ChisqInvRtFn));
10089    crate::function_registry::register_function(Arc::new(FDistRtFn));
10090    crate::function_registry::register_function(Arc::new(FInvRtFn));
10091    crate::function_registry::register_function(Arc::new(BetaInvFn));
10092    crate::function_registry::register_function(Arc::new(BinomDistRangeFn));
10093    crate::function_registry::register_function(Arc::new(BinomInvFn));
10094    crate::function_registry::register_function(Arc::new(GammaFn));
10095    crate::function_registry::register_function(Arc::new(GammaInvFn));
10096    crate::function_registry::register_function(Arc::new(GammaLnFn));
10097    crate::function_registry::register_function(Arc::new(GammaLnPreciseFn));
10098}
10099
10100#[cfg(test)]
10101mod tests_basic_stats {
10102    use super::*;
10103    use crate::test_workbook::TestWorkbook;
10104    use crate::traits::ArgumentHandle;
10105    use formualizer_common::LiteralValue;
10106    use formualizer_parse::parser::{ASTNode, ASTNodeType};
10107    fn interp(wb: &TestWorkbook) -> crate::interpreter::Interpreter<'_> {
10108        wb.interpreter()
10109    }
10110    fn arr(vals: Vec<f64>) -> ASTNode {
10111        ASTNode::new(
10112            ASTNodeType::Literal(LiteralValue::Array(vec![
10113                vals.into_iter().map(LiteralValue::Number).collect(),
10114            ])),
10115            None,
10116        )
10117    }
10118    #[test]
10119    fn median_even() {
10120        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(MEDIAN));
10121        let ctx = interp(&wb);
10122        let node = arr(vec![1.0, 3.0, 5.0, 7.0]);
10123        let f = ctx.context.get_function("", "MEDIAN").unwrap();
10124        let out = f
10125            .dispatch(
10126                &[ArgumentHandle::new(&node, &ctx)],
10127                &ctx.function_context(None),
10128            )
10129            .unwrap();
10130        assert_eq!(out, LiteralValue::Number(4.0));
10131    }
10132    #[test]
10133    fn median_odd() {
10134        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(MEDIAN));
10135        let ctx = interp(&wb);
10136        let node = arr(vec![1.0, 9.0, 5.0]);
10137        let f = ctx.context.get_function("", "MEDIAN").unwrap();
10138        let out = f
10139            .dispatch(
10140                &[ArgumentHandle::new(&node, &ctx)],
10141                &ctx.function_context(None),
10142            )
10143            .unwrap();
10144        assert_eq!(out, LiteralValue::Number(5.0));
10145    }
10146    #[test]
10147    fn large_basic() {
10148        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(LARGE));
10149        let ctx = interp(&wb);
10150        let nums = arr(vec![10.0, 20.0, 30.0]);
10151        let k = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(2.0)), None);
10152        let f = ctx.context.get_function("", "LARGE").unwrap();
10153        let out = f
10154            .dispatch(
10155                &[
10156                    ArgumentHandle::new(&nums, &ctx),
10157                    ArgumentHandle::new(&k, &ctx),
10158                ],
10159                &ctx.function_context(None),
10160            )
10161            .unwrap();
10162        assert_eq!(out, LiteralValue::Number(20.0));
10163    }
10164    #[test]
10165    fn small_basic() {
10166        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(SMALL));
10167        let ctx = interp(&wb);
10168        let nums = arr(vec![10.0, 20.0, 30.0]);
10169        let k = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(2.0)), None);
10170        let f = ctx.context.get_function("", "SMALL").unwrap();
10171        let out = f
10172            .dispatch(
10173                &[
10174                    ArgumentHandle::new(&nums, &ctx),
10175                    ArgumentHandle::new(&k, &ctx),
10176                ],
10177                &ctx.function_context(None),
10178            )
10179            .unwrap();
10180        assert_eq!(out, LiteralValue::Number(20.0));
10181    }
10182    #[test]
10183    fn percentile_inc_quarter() {
10184        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(PercentileInc));
10185        let ctx = interp(&wb);
10186        let nums = arr(vec![1.0, 2.0, 3.0, 4.0]);
10187        let p = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(0.25)), None);
10188        let f = ctx.context.get_function("", "PERCENTILE.INC").unwrap();
10189        match f
10190            .dispatch(
10191                &[
10192                    ArgumentHandle::new(&nums, &ctx),
10193                    ArgumentHandle::new(&p, &ctx),
10194                ],
10195                &ctx.function_context(None),
10196            )
10197            .unwrap()
10198            .into_literal()
10199        {
10200            LiteralValue::Number(v) => assert!((v - 1.75).abs() < 1e-9),
10201            other => panic!("unexpected {other:?}"),
10202        }
10203    }
10204    #[test]
10205    fn rank_eq_descending() {
10206        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(RankEqFn));
10207        let ctx = interp(&wb);
10208        // target 5 among {10,5,1} descending => ranks 1,2,3 => expect 2
10209        let target = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(5.0)), None);
10210        let arr_node = arr(vec![10.0, 5.0, 1.0]);
10211        let f = ctx.context.get_function("", "RANK.EQ").unwrap();
10212        let out = f
10213            .dispatch(
10214                &[
10215                    ArgumentHandle::new(&target, &ctx),
10216                    ArgumentHandle::new(&arr_node, &ctx),
10217                ],
10218                &ctx.function_context(None),
10219            )
10220            .unwrap();
10221        assert_eq!(out, LiteralValue::Number(2.0));
10222    }
10223    #[test]
10224    fn rank_eq_ascending_order_arg() {
10225        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(RankEqFn));
10226        let ctx = interp(&wb);
10227        // ascending order=1: array {1,5,10}; target 5 => rank 2
10228        let target = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(5.0)), None);
10229        let arr_node = arr(vec![1.0, 5.0, 10.0]);
10230        let order = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(1.0)), None);
10231        let f = ctx.context.get_function("", "RANK.EQ").unwrap();
10232        let out = f
10233            .dispatch(
10234                &[
10235                    ArgumentHandle::new(&target, &ctx),
10236                    ArgumentHandle::new(&arr_node, &ctx),
10237                    ArgumentHandle::new(&order, &ctx),
10238                ],
10239                &ctx.function_context(None),
10240            )
10241            .unwrap();
10242        assert_eq!(out, LiteralValue::Number(2.0));
10243    }
10244    #[test]
10245    fn rank_avg_ties() {
10246        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(RankAvgFn));
10247        let ctx = interp(&wb);
10248        // descending array {5,5,1} target 5 positions 1 and 2 avg -> 1.5
10249        let target = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(5.0)), None);
10250        let arr_node = arr(vec![5.0, 5.0, 1.0]);
10251        let f = ctx.context.get_function("", "RANK.AVG").unwrap();
10252        let out = f
10253            .dispatch(
10254                &[
10255                    ArgumentHandle::new(&target, &ctx),
10256                    ArgumentHandle::new(&arr_node, &ctx),
10257                ],
10258                &ctx.function_context(None),
10259            )
10260            .unwrap()
10261            .into_literal();
10262        match out {
10263            LiteralValue::Number(v) => assert!((v - 1.5).abs() < 1e-12),
10264            other => panic!("expected number got {other:?}"),
10265        }
10266    }
10267    #[test]
10268    fn stdev_var_sample_population() {
10269        let wb = TestWorkbook::new()
10270            .with_function(std::sync::Arc::new(StdevSample))
10271            .with_function(std::sync::Arc::new(StdevPop))
10272            .with_function(std::sync::Arc::new(VarSample))
10273            .with_function(std::sync::Arc::new(VarPop));
10274        let ctx = interp(&wb);
10275        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...
10276        let stdev_p = ctx.context.get_function("", "STDEV.P").unwrap();
10277        let stdev_s = ctx.context.get_function("", "STDEV.S").unwrap();
10278        let var_p = ctx.context.get_function("", "VAR.P").unwrap();
10279        let var_s = ctx.context.get_function("", "VAR.S").unwrap();
10280        let args = [ArgumentHandle::new(&arr_node, &ctx)];
10281        match var_p
10282            .dispatch(&args, &ctx.function_context(None))
10283            .unwrap()
10284            .into_literal()
10285        {
10286            LiteralValue::Number(v) => assert!((v - 4.0).abs() < 1e-12),
10287            other => panic!("unexpected {other:?}"),
10288        }
10289        match var_s
10290            .dispatch(&args, &ctx.function_context(None))
10291            .unwrap()
10292            .into_literal()
10293        {
10294            LiteralValue::Number(v) => assert!((v - 4.571428571428571).abs() < 1e-9),
10295            other => panic!("unexpected {other:?}"),
10296        }
10297        match stdev_p
10298            .dispatch(&args, &ctx.function_context(None))
10299            .unwrap()
10300            .into_literal()
10301        {
10302            LiteralValue::Number(v) => assert!((v - 2.0).abs() < 1e-12),
10303            other => panic!("unexpected {other:?}"),
10304        }
10305        match stdev_s
10306            .dispatch(&args, &ctx.function_context(None))
10307            .unwrap()
10308            .into_literal()
10309        {
10310            LiteralValue::Number(v) => assert!((v - 2.138089935).abs() < 1e-9),
10311            other => panic!("unexpected {other:?}"),
10312        }
10313    }
10314    #[test]
10315    fn quartile_inc_exc() {
10316        let wb = TestWorkbook::new()
10317            .with_function(std::sync::Arc::new(QuartileInc))
10318            .with_function(std::sync::Arc::new(QuartileExc));
10319        let ctx = interp(&wb);
10320        let arr_node = arr(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
10321        let q1 = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(1.0)), None);
10322        let q2 = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(2.0)), None);
10323        let f_inc = ctx.context.get_function("", "QUARTILE.INC").unwrap();
10324        let f_exc = ctx.context.get_function("", "QUARTILE.EXC").unwrap();
10325        let arg_inc_q1 = [
10326            ArgumentHandle::new(&arr_node, &ctx),
10327            ArgumentHandle::new(&q1, &ctx),
10328        ];
10329        let arg_inc_q2 = [
10330            ArgumentHandle::new(&arr_node, &ctx),
10331            ArgumentHandle::new(&q2, &ctx),
10332        ];
10333        match f_inc
10334            .dispatch(&arg_inc_q1, &ctx.function_context(None))
10335            .unwrap()
10336            .into_literal()
10337        {
10338            LiteralValue::Number(v) => assert!((v - 2.0).abs() < 1e-12),
10339            other => panic!("unexpected {other:?}"),
10340        }
10341        match f_inc
10342            .dispatch(&arg_inc_q2, &ctx.function_context(None))
10343            .unwrap()
10344            .into_literal()
10345        {
10346            LiteralValue::Number(v) => assert!((v - 3.0).abs() < 1e-12),
10347            other => panic!("unexpected {other:?}"),
10348        }
10349        // QUARTILE.EXC Q1 for 5-point set uses exclusive percentile => 1.5
10350        match f_exc
10351            .dispatch(&arg_inc_q1, &ctx.function_context(None))
10352            .unwrap()
10353            .into_literal()
10354        {
10355            LiteralValue::Number(v) => assert!((v - 1.5).abs() < 1e-12),
10356            other => panic!("unexpected {other:?}"),
10357        }
10358        match f_exc
10359            .dispatch(&arg_inc_q2, &ctx.function_context(None))
10360            .unwrap()
10361            .into_literal()
10362        {
10363            LiteralValue::Number(v) => assert!((v - 3.0).abs() < 1e-12),
10364            other => panic!("unexpected {other:?}"),
10365        }
10366    }
10367
10368    // --- eval()/dispatch equivalence tests for variance / stdev ---
10369    #[test]
10370    fn fold_equivalence_var_stdev() {
10371        use crate::function::Function as _; // trait import
10372        let wb = TestWorkbook::new()
10373            .with_function(std::sync::Arc::new(VarSample))
10374            .with_function(std::sync::Arc::new(VarPop))
10375            .with_function(std::sync::Arc::new(StdevSample))
10376            .with_function(std::sync::Arc::new(StdevPop));
10377        let ctx = interp(&wb);
10378        let arr_node = arr(vec![1.0, 2.0, 5.0, 5.0, 9.0]);
10379        let args = [ArgumentHandle::new(&arr_node, &ctx)];
10380
10381        let var_s_fn = VarSample; // concrete instance to call eval()
10382        let var_p_fn = VarPop;
10383        let stdev_s_fn = StdevSample;
10384        let stdev_p_fn = StdevPop;
10385
10386        let fctx = ctx.function_context(None);
10387        // Dispatch results (will use fold path)
10388        let disp_var_s = ctx
10389            .context
10390            .get_function("", "VAR.S")
10391            .unwrap()
10392            .dispatch(&args, &fctx)
10393            .unwrap()
10394            .into_literal();
10395        let disp_var_p = ctx
10396            .context
10397            .get_function("", "VAR.P")
10398            .unwrap()
10399            .dispatch(&args, &fctx)
10400            .unwrap()
10401            .into_literal();
10402        let disp_stdev_s = ctx
10403            .context
10404            .get_function("", "STDEV.S")
10405            .unwrap()
10406            .dispatch(&args, &fctx)
10407            .unwrap()
10408            .into_literal();
10409        let disp_stdev_p = ctx
10410            .context
10411            .get_function("", "STDEV.P")
10412            .unwrap()
10413            .dispatch(&args, &fctx)
10414            .unwrap()
10415            .into_literal();
10416
10417        // Scalar path results
10418        let scalar_var_s = var_s_fn.eval(&args, &fctx).unwrap().into_literal();
10419        let scalar_var_p = var_p_fn.eval(&args, &fctx).unwrap().into_literal();
10420        let scalar_stdev_s = stdev_s_fn.eval(&args, &fctx).unwrap().into_literal();
10421        let scalar_stdev_p = stdev_p_fn.eval(&args, &fctx).unwrap().into_literal();
10422
10423        fn assert_close(a: &LiteralValue, b: &LiteralValue) {
10424            match (a, b) {
10425                (LiteralValue::Number(x), LiteralValue::Number(y)) => {
10426                    assert!((x - y).abs() < 1e-12, "mismatch {x} vs {y}")
10427                }
10428                _ => assert_eq!(a, b),
10429            }
10430        }
10431        assert_close(&disp_var_s, &scalar_var_s);
10432        assert_close(&disp_var_p, &scalar_var_p);
10433        assert_close(&disp_stdev_s, &scalar_stdev_s);
10434        assert_close(&disp_stdev_p, &scalar_stdev_p);
10435    }
10436
10437    #[test]
10438    fn fold_equivalence_edge_cases() {
10439        use crate::function::Function as _;
10440        let wb = TestWorkbook::new()
10441            .with_function(std::sync::Arc::new(VarSample))
10442            .with_function(std::sync::Arc::new(VarPop))
10443            .with_function(std::sync::Arc::new(StdevSample))
10444            .with_function(std::sync::Arc::new(StdevPop));
10445        let ctx = interp(&wb);
10446        // Single numeric element -> sample variance/div0, population variance 0
10447        let single = arr(vec![42.0]);
10448        let args_single = [ArgumentHandle::new(&single, &ctx)];
10449        let fctx = ctx.function_context(None);
10450        let disp_var_s = ctx
10451            .context
10452            .get_function("", "VAR.S")
10453            .unwrap()
10454            .dispatch(&args_single, &fctx)
10455            .unwrap();
10456        let scalar_var_s = VarSample.eval(&args_single, &fctx).unwrap().into_literal();
10457        assert_eq!(disp_var_s, scalar_var_s);
10458        let disp_var_p = ctx
10459            .context
10460            .get_function("", "VAR.P")
10461            .unwrap()
10462            .dispatch(&args_single, &fctx)
10463            .unwrap();
10464        let scalar_var_p = VarPop.eval(&args_single, &fctx).unwrap().into_literal();
10465        assert_eq!(disp_var_p, scalar_var_p);
10466        let disp_stdev_p = ctx
10467            .context
10468            .get_function("", "STDEV.P")
10469            .unwrap()
10470            .dispatch(&args_single, &fctx)
10471            .unwrap();
10472        let scalar_stdev_p = StdevPop.eval(&args_single, &fctx).unwrap().into_literal();
10473        assert_eq!(disp_stdev_p, scalar_stdev_p);
10474        let disp_stdev_s = ctx
10475            .context
10476            .get_function("", "STDEV.S")
10477            .unwrap()
10478            .dispatch(&args_single, &fctx)
10479            .unwrap();
10480        let scalar_stdev_s = StdevSample
10481            .eval(&args_single, &fctx)
10482            .unwrap()
10483            .into_literal();
10484        assert_eq!(disp_stdev_s, scalar_stdev_s);
10485    }
10486
10487    #[test]
10488    fn legacy_aliases_match_modern() {
10489        let wb = TestWorkbook::new()
10490            .with_function(std::sync::Arc::new(PercentileInc))
10491            .with_function(std::sync::Arc::new(QuartileInc))
10492            .with_function(std::sync::Arc::new(RankEqFn));
10493        let ctx = interp(&wb);
10494        let arr_node = arr(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
10495        let p = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(0.4)), None);
10496        let q2 = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(2.0)), None);
10497        let target = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(4.0)), None);
10498        let args_p = [
10499            ArgumentHandle::new(&arr_node, &ctx),
10500            ArgumentHandle::new(&p, &ctx),
10501        ];
10502        let args_q = [
10503            ArgumentHandle::new(&arr_node, &ctx),
10504            ArgumentHandle::new(&q2, &ctx),
10505        ];
10506        let args_rank = [
10507            ArgumentHandle::new(&target, &ctx),
10508            ArgumentHandle::new(&arr_node, &ctx),
10509        ];
10510        let modern_p = ctx
10511            .context
10512            .get_function("", "PERCENTILE.INC")
10513            .unwrap()
10514            .dispatch(&args_p, &ctx.function_context(None))
10515            .unwrap()
10516            .into_literal();
10517        let legacy_p = ctx
10518            .context
10519            .get_function("", "PERCENTILE")
10520            .unwrap()
10521            .dispatch(&args_p, &ctx.function_context(None))
10522            .unwrap()
10523            .into_literal();
10524        assert_eq!(modern_p, legacy_p);
10525        let modern_q = ctx
10526            .context
10527            .get_function("", "QUARTILE.INC")
10528            .unwrap()
10529            .dispatch(&args_q, &ctx.function_context(None))
10530            .unwrap()
10531            .into_literal();
10532        let legacy_q = ctx
10533            .context
10534            .get_function("", "QUARTILE")
10535            .unwrap()
10536            .dispatch(&args_q, &ctx.function_context(None))
10537            .unwrap()
10538            .into_literal();
10539        assert_eq!(modern_q, legacy_q);
10540        let modern_rank = ctx
10541            .context
10542            .get_function("", "RANK.EQ")
10543            .unwrap()
10544            .dispatch(&args_rank, &ctx.function_context(None))
10545            .unwrap()
10546            .into_literal();
10547        let legacy_rank = ctx
10548            .context
10549            .get_function("", "RANK")
10550            .unwrap()
10551            .dispatch(&args_rank, &ctx.function_context(None))
10552            .unwrap()
10553            .into_literal();
10554        assert_eq!(modern_rank, legacy_rank);
10555    }
10556
10557    #[test]
10558    fn mode_single_basic_and_alias() {
10559        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(ModeSingleFn));
10560        let ctx = interp(&wb);
10561        let arr_node = arr(vec![5.0, 2.0, 2.0, 3.0, 3.0, 3.0]);
10562        let args = [ArgumentHandle::new(&arr_node, &ctx)];
10563        let mode_sngl = ctx
10564            .context
10565            .get_function("", "MODE.SNGL")
10566            .unwrap()
10567            .dispatch(&args, &ctx.function_context(None))
10568            .unwrap()
10569            .into_literal();
10570        assert_eq!(mode_sngl, LiteralValue::Number(3.0));
10571        let mode_alias = ctx
10572            .context
10573            .get_function("", "MODE")
10574            .unwrap()
10575            .dispatch(&args, &ctx.function_context(None))
10576            .unwrap()
10577            .into_literal();
10578        assert_eq!(mode_alias, mode_sngl);
10579    }
10580
10581    #[test]
10582    fn mode_single_no_duplicates() {
10583        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(ModeSingleFn));
10584        let ctx = interp(&wb);
10585        let arr_node = arr(vec![1.0, 2.0, 3.0]);
10586        let args = [ArgumentHandle::new(&arr_node, &ctx)];
10587        let out = ctx
10588            .context
10589            .get_function("", "MODE.SNGL")
10590            .unwrap()
10591            .dispatch(&args, &ctx.function_context(None))
10592            .unwrap()
10593            .into_literal();
10594        match out {
10595            LiteralValue::Error(e) => assert!(e.to_string().contains("#N/A")),
10596            _ => panic!("expected #N/A"),
10597        }
10598    }
10599
10600    #[test]
10601    fn mode_multi_basic() {
10602        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(ModeMultiFn));
10603        let ctx = interp(&wb);
10604        let arr_node = arr(vec![2.0, 3.0, 2.0, 4.0, 3.0, 5.0, 2.0, 3.0]);
10605        let args = [ArgumentHandle::new(&arr_node, &ctx)];
10606        let out = ctx
10607            .context
10608            .get_function("", "MODE.MULT")
10609            .unwrap()
10610            .dispatch(&args, &ctx.function_context(None))
10611            .unwrap()
10612            .into_literal();
10613        let expected = LiteralValue::Array(vec![
10614            vec![LiteralValue::Number(2.0)],
10615            vec![LiteralValue::Number(3.0)],
10616        ]);
10617        assert_eq!(out, expected);
10618    }
10619
10620    #[test]
10621    fn large_small_fold_vs_scalar() {
10622        let wb = TestWorkbook::new()
10623            .with_function(std::sync::Arc::new(LARGE))
10624            .with_function(std::sync::Arc::new(SMALL));
10625        let ctx = interp(&wb);
10626        let arr_node = arr(vec![10.0, 5.0, 7.0, 12.0, 9.0]);
10627        let k_node = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(2.0)), None);
10628        let args = [
10629            ArgumentHandle::new(&arr_node, &ctx),
10630            ArgumentHandle::new(&k_node, &ctx),
10631        ];
10632        let f_large = ctx.context.get_function("", "LARGE").unwrap();
10633        let disp_large = f_large
10634            .dispatch(&args, &ctx.function_context(None))
10635            .unwrap()
10636            .into_literal();
10637        let scalar_large = LARGE
10638            .eval(&args, &ctx.function_context(None))
10639            .unwrap()
10640            .into_literal();
10641        assert_eq!(disp_large, scalar_large);
10642        let f_small = ctx.context.get_function("", "SMALL").unwrap();
10643        let disp_small = f_small
10644            .dispatch(&args, &ctx.function_context(None))
10645            .unwrap()
10646            .into_literal();
10647        let scalar_small = SMALL
10648            .eval(&args, &ctx.function_context(None))
10649            .unwrap()
10650            .into_literal();
10651        assert_eq!(disp_small, scalar_small);
10652    }
10653
10654    #[test]
10655    fn mode_fold_vs_scalar() {
10656        let wb = TestWorkbook::new()
10657            .with_function(std::sync::Arc::new(ModeSingleFn))
10658            .with_function(std::sync::Arc::new(ModeMultiFn));
10659        let ctx = interp(&wb);
10660        let arr_node = arr(vec![2.0, 3.0, 2.0, 4.0, 3.0, 3.0, 2.0]);
10661        let args = [ArgumentHandle::new(&arr_node, &ctx)];
10662        let f_single = ctx.context.get_function("", "MODE.SNGL").unwrap();
10663        let disp_single = f_single
10664            .dispatch(&args, &ctx.function_context(None))
10665            .unwrap()
10666            .into_literal();
10667        let scalar_single = ModeSingleFn
10668            .eval(&args, &ctx.function_context(None))
10669            .unwrap()
10670            .into_literal();
10671        assert_eq!(disp_single, scalar_single);
10672        let f_multi = ctx.context.get_function("", "MODE.MULT").unwrap();
10673        let disp_multi = f_multi
10674            .dispatch(&args, &ctx.function_context(None))
10675            .unwrap()
10676            .into_literal();
10677        let scalar_multi = ModeMultiFn
10678            .eval(&args, &ctx.function_context(None))
10679            .unwrap()
10680            .into_literal();
10681        assert_eq!(disp_multi, scalar_multi);
10682    }
10683
10684    #[test]
10685    fn median_fold_vs_scalar_even() {
10686        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(MEDIAN));
10687        let ctx = interp(&wb);
10688        let arr_node = arr(vec![7.0, 1.0, 9.0, 5.0]); // sorted: 1,5,7,9 median=(5+7)/2=6
10689        let args = [ArgumentHandle::new(&arr_node, &ctx)];
10690        let f_med = ctx.context.get_function("", "MEDIAN").unwrap();
10691        let disp = f_med
10692            .dispatch(&args, &ctx.function_context(None))
10693            .unwrap()
10694            .into_literal();
10695        let scalar = MEDIAN
10696            .eval(&args, &ctx.function_context(None))
10697            .unwrap()
10698            .into_literal();
10699        assert_eq!(disp, scalar);
10700        assert_eq!(disp, LiteralValue::Number(6.0));
10701    }
10702
10703    #[test]
10704    fn median_fold_vs_scalar_odd() {
10705        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(MEDIAN));
10706        let ctx = interp(&wb);
10707        let arr_node = arr(vec![9.0, 2.0, 5.0]); // sorted 2,5,9 median=5
10708        let args = [ArgumentHandle::new(&arr_node, &ctx)];
10709        let f_med = ctx.context.get_function("", "MEDIAN").unwrap();
10710        let disp = f_med
10711            .dispatch(&args, &ctx.function_context(None))
10712            .unwrap()
10713            .into_literal();
10714        let scalar = MEDIAN
10715            .eval(&args, &ctx.function_context(None))
10716            .unwrap()
10717            .into_literal();
10718        assert_eq!(disp, scalar);
10719        assert_eq!(disp, LiteralValue::Number(5.0));
10720    }
10721
10722    // Newly added edge case tests for statistical semantics.
10723    #[test]
10724    fn percentile_inc_edges() {
10725        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(PercentileInc));
10726        let ctx = interp(&wb);
10727        let arr_node = arr(vec![10.0, 20.0, 30.0, 40.0]);
10728        let p0 = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(0.0)), None);
10729        let p1 = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(1.0)), None);
10730        let f = ctx.context.get_function("", "PERCENTILE.INC").unwrap();
10731        let args0 = [
10732            ArgumentHandle::new(&arr_node, &ctx),
10733            ArgumentHandle::new(&p0, &ctx),
10734        ];
10735        let args1 = [
10736            ArgumentHandle::new(&arr_node, &ctx),
10737            ArgumentHandle::new(&p1, &ctx),
10738        ];
10739        assert_eq!(
10740            f.dispatch(&args0, &ctx.function_context(None))
10741                .unwrap()
10742                .into_literal(),
10743            LiteralValue::Number(10.0)
10744        );
10745        assert_eq!(
10746            f.dispatch(&args1, &ctx.function_context(None))
10747                .unwrap()
10748                .into_literal(),
10749            LiteralValue::Number(40.0)
10750        );
10751    }
10752
10753    #[test]
10754    fn percentile_exc_invalid() {
10755        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(PercentileExc));
10756        let ctx = interp(&wb);
10757        let arr_node = arr(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
10758        let p_bad0 = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(0.0)), None);
10759        let p_bad1 = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(1.0)), None);
10760        let f = ctx.context.get_function("", "PERCENTILE.EXC").unwrap();
10761        for bad in [&p_bad0, &p_bad1] {
10762            let args = [
10763                ArgumentHandle::new(&arr_node, &ctx),
10764                ArgumentHandle::new(bad, &ctx),
10765            ];
10766            match f
10767                .dispatch(&args, &ctx.function_context(None))
10768                .unwrap()
10769                .into_literal()
10770            {
10771                LiteralValue::Error(e) => assert!(e.to_string().contains("#NUM!")),
10772                other => panic!("expected #NUM! got {other:?}"),
10773            }
10774        }
10775    }
10776
10777    #[test]
10778    fn quartile_invalids() {
10779        let wb = TestWorkbook::new()
10780            .with_function(std::sync::Arc::new(QuartileInc))
10781            .with_function(std::sync::Arc::new(QuartileExc));
10782        let ctx = interp(&wb);
10783        let arr_node = arr(vec![1.0, 2.0, 3.0]);
10784        // QUARTILE.INC invalid q=5
10785        let q5 = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(5.0)), None);
10786        let args_bad_inc = [
10787            ArgumentHandle::new(&arr_node, &ctx),
10788            ArgumentHandle::new(&q5, &ctx),
10789        ];
10790        match ctx
10791            .context
10792            .get_function("", "QUARTILE.INC")
10793            .unwrap()
10794            .dispatch(&args_bad_inc, &ctx.function_context(None))
10795            .unwrap()
10796            .into_literal()
10797        {
10798            LiteralValue::Error(e) => assert!(e.to_string().contains("#NUM!")),
10799            other => panic!("expected #NUM! {other:?}"),
10800        }
10801        // QUARTILE.EXC invalid q=0
10802        let q0 = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(0.0)), None);
10803        let args_bad_exc = [
10804            ArgumentHandle::new(&arr_node, &ctx),
10805            ArgumentHandle::new(&q0, &ctx),
10806        ];
10807        match ctx
10808            .context
10809            .get_function("", "QUARTILE.EXC")
10810            .unwrap()
10811            .dispatch(&args_bad_exc, &ctx.function_context(None))
10812            .unwrap()
10813            .into_literal()
10814        {
10815            LiteralValue::Error(e) => assert!(e.to_string().contains("#NUM!")),
10816            other => panic!("expected #NUM! {other:?}"),
10817        }
10818    }
10819
10820    #[test]
10821    fn rank_target_not_found() {
10822        let wb = TestWorkbook::new()
10823            .with_function(std::sync::Arc::new(RankEqFn))
10824            .with_function(std::sync::Arc::new(RankAvgFn));
10825        let ctx = interp(&wb);
10826        let arr_node = arr(vec![1.0, 2.0, 3.0]);
10827        let target = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(4.0)), None);
10828        let args = [
10829            ArgumentHandle::new(&target, &ctx),
10830            ArgumentHandle::new(&arr_node, &ctx),
10831        ];
10832        for name in ["RANK.EQ", "RANK.AVG"] {
10833            match ctx
10834                .context
10835                .get_function("", name)
10836                .unwrap()
10837                .dispatch(&args, &ctx.function_context(None))
10838                .unwrap()
10839                .into_literal()
10840            {
10841                LiteralValue::Error(e) => assert!(e.to_string().contains("#N/A")),
10842                other => panic!("expected #N/A {other:?}"),
10843            }
10844        }
10845    }
10846
10847    #[test]
10848    fn mode_mult_ordering() {
10849        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(ModeMultiFn));
10850        let ctx = interp(&wb);
10851        // Two modes with same frequency; ensure ascending ordering in array result
10852        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
10853        let args = [ArgumentHandle::new(&arr_node, &ctx)];
10854        let out = ctx
10855            .context
10856            .get_function("", "MODE.MULT")
10857            .unwrap()
10858            .dispatch(&args, &ctx.function_context(None))
10859            .unwrap()
10860            .into_literal();
10861        match out {
10862            LiteralValue::Array(rows) => {
10863                let vals: Vec<f64> = rows
10864                    .into_iter()
10865                    .map(|r| {
10866                        if let LiteralValue::Number(n) = r[0] {
10867                            n
10868                        } else {
10869                            panic!("expected number")
10870                        }
10871                    })
10872                    .collect();
10873                assert_eq!(vals, vec![2.0, 5.0]);
10874            }
10875            other => panic!("expected array {other:?}"),
10876        }
10877    }
10878
10879    #[test]
10880    fn boolean_and_text_in_range_are_ignored() {
10881        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(StdevPop));
10882        let ctx = interp(&wb);
10883        let mixed = ASTNode::new(
10884            ASTNodeType::Literal(LiteralValue::Array(vec![vec![
10885                LiteralValue::Number(1.0),
10886                LiteralValue::Text("ABC".into()),
10887                LiteralValue::Boolean(true),
10888                LiteralValue::Number(4.0),
10889            ]])),
10890            None,
10891        );
10892        let f = ctx.context.get_function("", "STDEV.P").unwrap();
10893        let out = f
10894            .dispatch(
10895                &[ArgumentHandle::new(&mixed, &ctx)],
10896                &ctx.function_context(None),
10897            )
10898            .unwrap()
10899            .into_literal();
10900        // NOTE: Inline array literal is treated as a direct scalar argument (not a range reference),
10901        // so boolean TRUE is coerced to 1. Dataset becomes {1,1,4}; population stdev = sqrt(6/3)=sqrt(2).
10902        match out {
10903            LiteralValue::Number(v) => {
10904                assert!((v - 2f64.sqrt()).abs() < 1e-12, "expected sqrt(2) got {v}")
10905            }
10906            other => panic!("unexpected {other:?}"),
10907        }
10908    }
10909
10910    #[test]
10911    fn boolean_direct_arg_coerces() {
10912        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(StdevPop));
10913        let ctx = interp(&wb);
10914        let one = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(1.0)), None);
10915        let t = ASTNode::new(ASTNodeType::Literal(LiteralValue::Boolean(true)), None);
10916        let f = ctx.context.get_function("", "STDEV.P").unwrap();
10917        let args = [
10918            ArgumentHandle::new(&one, &ctx),
10919            ArgumentHandle::new(&t, &ctx),
10920        ];
10921        let out = f
10922            .dispatch(&args, &ctx.function_context(None))
10923            .unwrap()
10924            .into_literal();
10925        assert_eq!(out, LiteralValue::Number(0.0));
10926    }
10927}
10928
10929#[cfg(test)]
10930mod tests_selection_vs_sort_oracle {
10931    //! Property tests: the quickselect paths must be bit-identical to the
10932    //! previous full-sort implementations (the "oracle" below reproduces the
10933    //! removed sort-based code exactly).
10934
10935    use super::*;
10936    use rand::rngs::SmallRng;
10937    use rand::{Rng, SeedableRng};
10938
10939    /* ───── full-sort reference oracle (the pre-quickselect implementation) ───── */
10940
10941    fn sorted_asc(data: &[f64]) -> Vec<f64> {
10942        let mut s = data.to_vec();
10943        s.sort_by(|a, b| a.partial_cmp(b).unwrap());
10944        s
10945    }
10946
10947    fn oracle_large(data: &[f64], k: usize) -> f64 {
10948        let mut s = data.to_vec();
10949        s.sort_by(|a, b| b.partial_cmp(a).unwrap());
10950        s[k - 1]
10951    }
10952
10953    fn oracle_small(data: &[f64], k: usize) -> f64 {
10954        sorted_asc(data)[k - 1]
10955    }
10956
10957    fn oracle_median(data: &[f64]) -> f64 {
10958        let s = sorted_asc(data);
10959        let n = s.len();
10960        let mid = n / 2;
10961        if n % 2 == 1 {
10962            s[mid]
10963        } else {
10964            (s[mid - 1] + s[mid]) / 2.0
10965        }
10966    }
10967
10968    fn oracle_percentile_inc(data: &[f64], p: f64) -> Result<f64, ExcelError> {
10969        let sorted = sorted_asc(data);
10970        if sorted.is_empty() || !(0.0..=1.0).contains(&p) {
10971            return Err(ExcelError::new_num());
10972        }
10973        if sorted.len() == 1 {
10974            return Ok(sorted[0]);
10975        }
10976        let n = sorted.len() as f64;
10977        let rank = p * (n - 1.0);
10978        let lo = rank.floor() as usize;
10979        let hi = rank.ceil() as usize;
10980        if lo == hi {
10981            return Ok(sorted[lo]);
10982        }
10983        let frac = rank - (lo as f64);
10984        Ok(sorted[lo] + (sorted[hi] - sorted[lo]) * frac)
10985    }
10986
10987    fn oracle_percentile_exc(data: &[f64], p: f64) -> Result<f64, ExcelError> {
10988        let sorted = sorted_asc(data);
10989        if sorted.is_empty() || !(0.0..=1.0).contains(&p) || p <= 0.0 || p >= 1.0 {
10990            return Err(ExcelError::new_num());
10991        }
10992        let n = sorted.len() as f64;
10993        let rank = p * (n + 1.0);
10994        if rank < 1.0 || rank > n {
10995            return Err(ExcelError::new_num());
10996        }
10997        let lo = rank.floor();
10998        let hi = rank.ceil();
10999        if (lo - hi).abs() < f64::EPSILON {
11000            return Ok(sorted[(lo as usize) - 1]);
11001        }
11002        let frac = rank - lo;
11003        let lo_idx = (lo as usize) - 1;
11004        let hi_idx = (hi as usize) - 1;
11005        Ok(sorted[lo_idx] + (sorted[hi_idx] - sorted[lo_idx]) * frac)
11006    }
11007
11008    fn assert_bits_eq(new: f64, old: f64, what: &str, data: &[f64]) {
11009        assert_eq!(
11010            new.to_bits(),
11011            old.to_bits(),
11012            "{what}: selection {new:?} != sort oracle {old:?} for {data:?}"
11013        );
11014    }
11015
11016    /// 1000 seeded random vectors (mixed continuous values and a small grid
11017    /// to force exact-duplicate ties) x random k / percentile.
11018    #[test]
11019    fn quickselect_paths_match_full_sort_oracle_bitwise() {
11020        let mut rng = SmallRng::seed_from_u64(0x5EED_57A7_u64);
11021        for _case in 0..1000 {
11022            let n = rng.gen_range(1..=64);
11023            let data: Vec<f64> = (0..n)
11024                .map(|_| {
11025                    if rng.gen_bool(0.35) {
11026                        // Tie-heavy grid: exact duplicates are common.
11027                        f64::from(rng.gen_range(-4i32..=4)) * 0.5
11028                    } else {
11029                        rng.gen_range(-1.0e6..1.0e6)
11030                    }
11031                })
11032                .collect();
11033
11034            // LARGE / SMALL across every k (includes k == 1 and k == n).
11035            for k in 1..=n {
11036                let mut buf = data.clone();
11037                let idx = buf.len() - k;
11038                assert_bits_eq(
11039                    nth_smallest(&mut buf, idx),
11040                    oracle_large(&data, k),
11041                    "LARGE",
11042                    &data,
11043                );
11044                let mut buf = data.clone();
11045                assert_bits_eq(
11046                    nth_smallest(&mut buf, k - 1),
11047                    oracle_small(&data, k),
11048                    "SMALL",
11049                    &data,
11050                );
11051            }
11052
11053            // MEDIAN (odd and even n both covered across cases).
11054            {
11055                let mut buf = data.clone();
11056                let mid = buf.len() / 2;
11057                let med = if buf.len() % 2 == 1 {
11058                    nth_smallest(&mut buf, mid)
11059                } else if buf.len() >= 2 {
11060                    let (lo, hi) = adjacent_smallest(&mut buf, mid - 1);
11061                    (lo + hi) / 2.0
11062                } else {
11063                    buf[0]
11064                };
11065                assert_bits_eq(med, oracle_median(&data), "MEDIAN", &data);
11066            }
11067
11068            // PERCENTILE.INC / .EXC with random p, plus the exact endpoints
11069            // and quartile points (0.25/0.5/0.75 also covers QUARTILE.*).
11070            let ps = [
11071                rng.r#gen::<f64>(),
11072                0.0,
11073                0.25,
11074                0.5,
11075                0.75,
11076                1.0,
11077                rng.gen_range(-0.5..1.5), // sometimes out of range
11078            ];
11079            for p in ps {
11080                let mut buf = data.clone();
11081                match (percentile_inc(&mut buf, p), oracle_percentile_inc(&data, p)) {
11082                    (Ok(a), Ok(b)) => assert_bits_eq(a, b, "PERCENTILE.INC", &data),
11083                    (Err(ea), Err(eb)) => assert_eq!(ea.kind, eb.kind),
11084                    (a, b) => panic!("PERCENTILE.INC divergence p={p}: {a:?} vs {b:?}"),
11085                }
11086                let mut buf = data.clone();
11087                match (percentile_exc(&mut buf, p), oracle_percentile_exc(&data, p)) {
11088                    (Ok(a), Ok(b)) => assert_bits_eq(a, b, "PERCENTILE.EXC", &data),
11089                    (Err(ea), Err(eb)) => assert_eq!(ea.kind, eb.kind),
11090                    (a, b) => panic!("PERCENTILE.EXC divergence p={p}: {a:?} vs {b:?}"),
11091                }
11092            }
11093        }
11094    }
11095
11096    /// Empty input keeps the error contract.
11097    #[test]
11098    fn percentile_empty_input_still_num_error() {
11099        let mut empty: Vec<f64> = vec![];
11100        assert!(percentile_inc(&mut empty, 0.5).is_err());
11101        assert!(percentile_exc(&mut empty, 0.5).is_err());
11102    }
11103
11104    /// Perf probe (run explicitly):
11105    /// `cargo test -p formualizer-eval --release --lib large_quickselect_perf_probe -- --ignored --nocapture`
11106    #[test]
11107    #[ignore = "perf probe; run manually with --release --nocapture"]
11108    fn large_quickselect_perf_probe() {
11109        let mut rng = SmallRng::seed_from_u64(42);
11110        let data: Vec<f64> = (0..100_000).map(|_| rng.gen_range(-1.0e9..1.0e9)).collect();
11111        let k = 50usize;
11112        let rounds = 50u32;
11113
11114        let t = std::time::Instant::now();
11115        let mut old_v = 0.0;
11116        for _ in 0..rounds {
11117            let mut s = data.clone();
11118            s.sort_by(|a, b| b.partial_cmp(a).unwrap());
11119            old_v = s[k - 1];
11120        }
11121        let old_t = t.elapsed();
11122
11123        let t = std::time::Instant::now();
11124        let mut new_v = 0.0;
11125        for _ in 0..rounds {
11126            let mut s = data.clone();
11127            let idx = s.len() - k;
11128            new_v = nth_smallest(&mut s, idx);
11129        }
11130        let new_t = t.elapsed();
11131
11132        println!(
11133            "LARGE over 100k x{rounds}: full-sort {:?} ({:?}/op), quickselect {:?} ({:?}/op)",
11134            old_t,
11135            old_t / rounds,
11136            new_t,
11137            new_t / rounds
11138        );
11139        assert_eq!(new_v.to_bits(), old_v.to_bits());
11140        assert!(
11141            new_t < old_t,
11142            "quickselect should beat a full sort on 100k values"
11143        );
11144    }
11145}