Skip to main content

formualizer_eval/builtins/stats/
mod.rs

1//! Statistical basic functions (Sprint 6)
2//!
3//! Implementations target Excel semantic parity for:
4//! LARGE, SMALL, RANK.EQ, RANK.AVG, MEDIAN, STDEV.S, STDEV.P, VAR.S, VAR.P,
5//! PERCENTILE.INC, PERCENTILE.EXC, QUARTILE.INC, QUARTILE.EXC.
6//!
7//! Notes:
8//! - We currently materialize numeric values into a Vec<f64>. For large ranges this could be
9//!   optimized with streaming selection algorithms (nth_element / partial sort). TODO(perf).
10//! - Text/boolean coercion nuance: For Excel statistical functions, values coming from range
11//!   references should ignore text and logical values (they are skipped), while direct scalar
12//!   arguments still coerce (e.g. =STDEV(1,TRUE) treats TRUE as 1). This file now implements that
13//!   distinction. TODO(excel-nuance): refine numeric text literal vs non‑numeric text handling.
14//! - Errors encountered in any argument propagate immediately.
15//! - Empty numeric sets produce Excel-specific errors (#NUM! for LARGE/SMALL, #N/A for rank target
16//!   out of range, #DIV/0! for STDEV/VAR sample with n < 2, etc.).
17
18use super::super::builtins::utils::{ARG_RANGE_NUM_LENIENT_ONE, coerce_num};
19use crate::args::ArgSchema;
20use crate::function::Function;
21use crate::traits::{ArgumentHandle, FunctionContext};
22use formualizer_common::{ExcelError, LiteralValue};
23// use std::collections::BTreeMap; // removed unused import
24use formualizer_macros::func_caps;
25
26fn scalar_like_value(arg: &ArgumentHandle<'_, '_>) -> Result<LiteralValue, ExcelError> {
27    Ok(match arg.value()? {
28        crate::traits::CalcValue::Scalar(v) => v,
29        crate::traits::CalcValue::Range(rv) => rv.get_cell(0, 0),
30        crate::traits::CalcValue::Callable(_) => LiteralValue::Error(
31            ExcelError::new(formualizer_common::ExcelErrorKind::Calc)
32                .with_message("LAMBDA value must be invoked"),
33        ),
34    })
35}
36
37/// Collect numeric inputs applying Excel statistical semantics:
38/// - Range references: include only numeric cells; skip text, logical, blank. Errors propagate.
39/// - Direct scalar arguments: attempt numeric coercion (so TRUE/FALSE, numeric text are included if
40///   coerce_num succeeds). Non-numeric text is ignored (Excel would treat a direct non-numeric text
41///   argument as #VALUE! in some contexts; covered by TODO for finer parity).
42fn collect_numeric_stats(args: &[ArgumentHandle]) -> Result<Vec<f64>, ExcelError> {
43    let mut out = Vec::new();
44    for a in args {
45        // Special-case: inline array literal argument should be treated like a list of direct scalar
46        // arguments (not a by-ref range). This allows boolean/text coercion per element akin to
47        // passing multiple scalars to the function.
48        if let Some(arr) = a.inline_array_literal()? {
49            for row in arr.into_iter() {
50                for cell in row.into_iter() {
51                    match cell {
52                        LiteralValue::Error(e) => return Err(e),
53                        other => {
54                            if let Ok(n) = coerce_num(&other) {
55                                out.push(n);
56                            }
57                        }
58                    }
59                }
60            }
61            continue;
62        }
63
64        if let Ok(view) = a.range_view() {
65            view.for_each_cell(&mut |v| {
66                match v {
67                    LiteralValue::Error(e) => return Err(e.clone()),
68                    LiteralValue::Number(n) => out.push(*n),
69                    LiteralValue::Int(i) => out.push(*i as f64),
70                    _ => {}
71                }
72                Ok(())
73            })?;
74        } else {
75            let v = scalar_like_value(a)?;
76            match v {
77                LiteralValue::Error(e) => return Err(e),
78                other => {
79                    if let Ok(n) = coerce_num(&other) {
80                        out.push(n);
81                    }
82                }
83            }
84        }
85    }
86    Ok(out)
87}
88
89fn percentile_inc(sorted: &[f64], p: f64) -> Result<f64, ExcelError> {
90    if sorted.is_empty() {
91        return Err(ExcelError::new_num());
92    }
93    if !(0.0..=1.0).contains(&p) {
94        return Err(ExcelError::new_num());
95    }
96    if sorted.len() == 1 {
97        return Ok(sorted[0]);
98    }
99    let n = sorted.len() as f64;
100    let rank = p * (n - 1.0); // 0-based rank
101    let lo = rank.floor() as usize;
102    let hi = rank.ceil() as usize;
103    if lo == hi {
104        return Ok(sorted[lo]);
105    }
106    let frac = rank - (lo as f64);
107    Ok(sorted[lo] + (sorted[hi] - sorted[lo]) * frac)
108}
109
110fn percentile_exc(sorted: &[f64], p: f64) -> Result<f64, ExcelError> {
111    // Excel PERCENTILE.EXC requires 0 < p < 1 and uses (n+1) basis; invalid if rank<1 or >n
112    if sorted.is_empty() {
113        return Err(ExcelError::new_num());
114    }
115    if !(0.0..=1.0).contains(&p) || p <= 0.0 || p >= 1.0 {
116        return Err(ExcelError::new_num());
117    }
118    let n = sorted.len() as f64;
119    let rank = p * (n + 1.0); // 1..n domain
120    if rank < 1.0 || rank > n {
121        return Err(ExcelError::new_num());
122    }
123    let lo = rank.floor();
124    let hi = rank.ceil();
125    if (lo - hi).abs() < f64::EPSILON {
126        return Ok(sorted[(lo as usize) - 1]);
127    }
128    let frac = rank - lo;
129    let lo_idx = (lo as usize) - 1;
130    let hi_idx = (hi as usize) - 1;
131    Ok(sorted[lo_idx] + (sorted[hi_idx] - sorted[lo_idx]) * frac)
132}
133
134/// Returns the rank position of a number within a data set, with ties sharing the same rank.
135///
136/// `RANK.EQ` defaults to descending order (largest value is rank 1), and can switch to ascending
137/// order when `order` is non-zero.
138///
139/// # Remarks
140/// - Omitting `order`, or setting `order` to `0`, ranks values in descending order.
141/// - Any non-zero `order` ranks values in ascending order.
142/// - Tied values receive the same rank (the first matching position in the sorted list).
143/// - Returns `#N/A` if `number` is not found in `ref`.
144///
145/// # Examples
146///
147/// ```yaml,sandbox
148/// title: "Descending rank with direct values"
149/// formula: "=RANK.EQ(7,{10,7,4,2})"
150/// expected: 2
151/// ```
152///
153/// ```yaml,sandbox
154/// title: "Ascending rank with ties in a range"
155/// grid:
156///   A1: 50
157///   A2: 20
158///   A3: 20
159///   A4: 10
160///   A5: 5
161/// formula: "=RANK.EQ(A2,A1:A5,1)"
162/// expected: 3
163/// ```
164///
165/// ```yaml,docs
166/// related:
167///   - RANK.AVG
168///   - LARGE
169///   - SMALL
170/// faq:
171///   - q: "When does RANK.EQ return #N/A?"
172///     a: "It returns #N/A when the target number does not appear in the reference set."
173/// ```
174#[derive(Debug)]
175pub struct RankEqFn;
176/// [formualizer-docgen:schema:start]
177/// Name: RANK.EQ
178/// Type: RankEqFn
179/// Min args: 2
180/// Max args: variadic
181/// Variadic: true
182/// Signature: RANK.EQ(arg1...: number@range)
183/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
184/// Caps: PURE, NUMERIC_ONLY
185/// [formualizer-docgen:schema:end]
186impl Function for RankEqFn {
187    func_caps!(PURE, NUMERIC_ONLY);
188    fn name(&self) -> &'static str {
189        "RANK.EQ"
190    }
191    fn aliases(&self) -> &'static [&'static str] {
192        &["RANK"]
193    }
194    fn min_args(&self) -> usize {
195        2
196    }
197    fn variadic(&self) -> bool {
198        true
199    } // allow optional order
200    fn arg_schema(&self) -> &'static [ArgSchema] {
201        &ARG_RANGE_NUM_LENIENT_ONE[..]
202    }
203    fn eval<'a, 'b, 'c>(
204        &self,
205        args: &'c [ArgumentHandle<'a, 'b>],
206        _ctx: &dyn FunctionContext<'b>,
207    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
208        if args.len() < 2 {
209            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
210                ExcelError::new_na(),
211            )));
212        }
213        let target = match coerce_num(&args[0].value()?.into_literal()) {
214            Ok(n) => n,
215            Err(_) => {
216                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
217                    ExcelError::new_na(),
218                )));
219            }
220        };
221        // optional order arg at end if 3 args
222        let order = if args.len() >= 3 {
223            coerce_num(&args[2].value()?.into_literal()).unwrap_or(0.0)
224        } else {
225            0.0
226        };
227        let nums = collect_numeric_stats(&args[1..2])?; // only one ref range per Excel spec
228        if nums.is_empty() {
229            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
230                ExcelError::new_na(),
231            )));
232        }
233        let mut sorted = nums; // copy
234        if order.abs() < 1e-12 {
235            // descending
236            sorted.sort_by(|a, b| b.partial_cmp(a).unwrap());
237        } else {
238            // ascending
239            sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
240        }
241        for (i, &v) in sorted.iter().enumerate() {
242            if (v - target).abs() < 1e-12 {
243                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
244                    (i + 1) as f64,
245                )));
246            }
247        }
248        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
249            ExcelError::new_na(),
250        )))
251    }
252}
253
254/// Returns the rank position of a number, averaging the rank positions for ties.
255///
256/// Use `RANK.AVG` when tied values should share the average of their occupied rank positions.
257///
258/// # Remarks
259/// - Omitting `order`, or setting `order` to `0`, ranks values in descending order.
260/// - Any non-zero `order` ranks values in ascending order.
261/// - If `number` appears multiple times, the function returns the mean of those rank positions.
262/// - Returns `#N/A` if `number` is not found in `ref`.
263///
264/// # Examples
265///
266/// ```yaml,sandbox
267/// title: "Average rank for tied values"
268/// formula: "=RANK.AVG(20,{30,20,20,10})"
269/// expected: 2.5
270/// ```
271///
272/// ```yaml,sandbox
273/// title: "Ascending average rank from a range"
274/// grid:
275///   A1: 50
276///   A2: 20
277///   A3: 20
278///   A4: 10
279///   A5: 5
280/// formula: "=RANK.AVG(A2,A1:A5,1)"
281/// expected: 3.5
282/// ```
283///
284/// ```yaml,docs
285/// related:
286///   - RANK.EQ
287///   - LARGE
288///   - SMALL
289/// faq:
290///   - q: "How are ties handled by RANK.AVG?"
291///     a: "All tied occurrences share the average of their rank positions."
292/// ```
293#[derive(Debug)]
294pub struct RankAvgFn;
295/// [formualizer-docgen:schema:start]
296/// Name: RANK.AVG
297/// Type: RankAvgFn
298/// Min args: 2
299/// Max args: variadic
300/// Variadic: true
301/// Signature: RANK.AVG(arg1...: number@range)
302/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
303/// Caps: PURE, NUMERIC_ONLY
304/// [formualizer-docgen:schema:end]
305impl Function for RankAvgFn {
306    func_caps!(PURE, NUMERIC_ONLY);
307    fn name(&self) -> &'static str {
308        "RANK.AVG"
309    }
310    fn min_args(&self) -> usize {
311        2
312    }
313    fn variadic(&self) -> bool {
314        true
315    }
316    fn arg_schema(&self) -> &'static [ArgSchema] {
317        &ARG_RANGE_NUM_LENIENT_ONE[..]
318    }
319    fn eval<'a, 'b, 'c>(
320        &self,
321        args: &'c [ArgumentHandle<'a, 'b>],
322        _ctx: &dyn FunctionContext<'b>,
323    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
324        if args.len() < 2 {
325            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
326                ExcelError::new_na(),
327            )));
328        }
329        let t0 = scalar_like_value(&args[0])?;
330        let target = match coerce_num(&t0) {
331            Ok(n) => n,
332            Err(_) => {
333                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
334                    ExcelError::new_na(),
335                )));
336            }
337        };
338        let order = if args.len() >= 3 {
339            let ord = scalar_like_value(&args[2])?;
340            coerce_num(&ord).unwrap_or(0.0)
341        } else {
342            0.0
343        };
344        let nums = collect_numeric_stats(&args[1..2])?;
345        if nums.is_empty() {
346            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
347                ExcelError::new_na(),
348            )));
349        }
350        let mut sorted = nums;
351        if order.abs() < 1e-12 {
352            sorted.sort_by(|a, b| b.partial_cmp(a).unwrap());
353        } else {
354            sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
355        }
356        let mut positions = Vec::new();
357        for (i, &v) in sorted.iter().enumerate() {
358            if (v - target).abs() < 1e-12 {
359                positions.push(i + 1);
360            }
361        }
362        if positions.is_empty() {
363            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
364                ExcelError::new_na(),
365            )));
366        }
367        let avg = positions.iter().copied().sum::<usize>() as f64 / positions.len() as f64;
368        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(avg)))
369    }
370}
371
372/// Returns the k-th largest value in a data set.
373///
374/// `LARGE` is useful for top-N analysis, such as highest score, second-highest sale, or third-best
375/// result.
376///
377/// # Remarks
378/// - `k` must be at least `1`.
379/// - Returns `#NUM!` if `k` is greater than the count of numeric values.
380/// - Non-numeric values in referenced ranges are ignored.
381///
382/// # Examples
383///
384/// ```yaml,sandbox
385/// title: "Second-largest from direct values"
386/// formula: "=LARGE({4,9,1,7},2)"
387/// expected: 7
388/// ```
389///
390/// ```yaml,sandbox
391/// title: "Third-largest from a range"
392/// grid:
393///   A1: 3
394///   A2: 12
395///   A3: 8
396///   A4: 5
397/// formula: "=LARGE(A1:A4,3)"
398/// expected: 5
399/// ```
400///
401/// ```yaml,docs
402/// related:
403///   - SMALL
404///   - MAX
405///   - RANK.EQ
406/// faq:
407///   - q: "When does LARGE return #NUM!?"
408///     a: "It returns #NUM! when k < 1, k exceeds numeric count, or no numeric values exist."
409/// ```
410#[derive(Debug)]
411pub struct LARGE;
412/// [formualizer-docgen:schema:start]
413/// Name: LARGE
414/// Type: LARGE
415/// Min args: 2
416/// Max args: variadic
417/// Variadic: true
418/// Signature: LARGE(arg1...: number@range)
419/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
420/// Caps: PURE, REDUCTION, NUMERIC_ONLY
421/// [formualizer-docgen:schema:end]
422impl Function for LARGE {
423    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
424    fn name(&self) -> &'static str {
425        "LARGE"
426    }
427    fn min_args(&self) -> usize {
428        2
429    }
430    fn variadic(&self) -> bool {
431        true
432    }
433    fn arg_schema(&self) -> &'static [ArgSchema] {
434        &ARG_RANGE_NUM_LENIENT_ONE[..]
435    }
436    fn eval<'a, 'b, 'c>(
437        &self,
438        args: &'c [ArgumentHandle<'a, 'b>],
439        _ctx: &dyn FunctionContext<'b>,
440    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
441        if args.len() < 2 {
442            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
443                ExcelError::new_num(),
444            )));
445        }
446        let k = match coerce_num(&args.last().unwrap().value()?.into_literal()) {
447            Ok(n) => n,
448            Err(_) => {
449                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
450                    ExcelError::new_num(),
451                )));
452            }
453        };
454        let k = k as i64;
455        if k < 1 {
456            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
457                ExcelError::new_num(),
458            )));
459        }
460        let mut nums = collect_numeric_stats(&args[..args.len() - 1])?;
461        if nums.is_empty() || k as usize > nums.len() {
462            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
463                ExcelError::new_num(),
464            )));
465        }
466        nums.sort_by(|a, b| b.partial_cmp(a).unwrap());
467        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
468            nums[(k as usize) - 1],
469        )))
470    }
471}
472
473/// Returns the k-th smallest value in a data set.
474///
475/// `SMALL` is often used to find low outliers, minimum thresholds, or bottom-N values.
476///
477/// # Remarks
478/// - `k` must be at least `1`.
479/// - Returns `#NUM!` if `k` is greater than the count of numeric values.
480/// - Non-numeric values in referenced ranges are ignored.
481///
482/// # Examples
483///
484/// ```yaml,sandbox
485/// title: "Second-smallest from direct values"
486/// formula: "=SMALL({4,9,1,7},2)"
487/// expected: 4
488/// ```
489///
490/// ```yaml,sandbox
491/// title: "Third-smallest from a range"
492/// grid:
493///   A1: 3
494///   A2: 12
495///   A3: 8
496///   A4: 5
497/// formula: "=SMALL(A1:A4,3)"
498/// expected: 8
499/// ```
500///
501/// ```yaml,docs
502/// related:
503///   - LARGE
504///   - MIN
505///   - RANK.EQ
506/// faq:
507///   - q: "Does SMALL include text in referenced ranges?"
508///     a: "No. Non-numeric range values are ignored when selecting the k-th smallest value."
509/// ```
510#[derive(Debug)]
511pub struct SMALL;
512/// [formualizer-docgen:schema:start]
513/// Name: SMALL
514/// Type: SMALL
515/// Min args: 2
516/// Max args: variadic
517/// Variadic: true
518/// Signature: SMALL(arg1...: number@range)
519/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
520/// Caps: PURE, REDUCTION, NUMERIC_ONLY
521/// [formualizer-docgen:schema:end]
522impl Function for SMALL {
523    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
524    fn name(&self) -> &'static str {
525        "SMALL"
526    }
527    fn min_args(&self) -> usize {
528        2
529    }
530    fn variadic(&self) -> bool {
531        true
532    }
533    fn arg_schema(&self) -> &'static [ArgSchema] {
534        &ARG_RANGE_NUM_LENIENT_ONE[..]
535    }
536    fn eval<'a, 'b, 'c>(
537        &self,
538        args: &'c [ArgumentHandle<'a, 'b>],
539        _ctx: &dyn FunctionContext<'b>,
540    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
541        if args.len() < 2 {
542            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
543                ExcelError::new_num(),
544            )));
545        }
546        let k = match coerce_num(&args.last().unwrap().value()?.into_literal()) {
547            Ok(n) => n,
548            Err(_) => {
549                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
550                    ExcelError::new_num(),
551                )));
552            }
553        };
554        let k = k as i64;
555        if k < 1 {
556            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
557                ExcelError::new_num(),
558            )));
559        }
560        let mut nums = collect_numeric_stats(&args[..args.len() - 1])?;
561        if nums.is_empty() || k as usize > nums.len() {
562            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
563                ExcelError::new_num(),
564            )));
565        }
566        nums.sort_by(|a, b| a.partial_cmp(b).unwrap());
567        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
568            nums[(k as usize) - 1],
569        )))
570    }
571}
572
573/// Returns the middle value of a numeric data set.
574///
575/// For an even number of values, `MEDIAN` returns the average of the two center values.
576///
577/// # Remarks
578/// - Ignores non-numeric values in referenced ranges.
579/// - Returns `#NUM!` when no numeric values are available.
580/// - Supports both scalar arguments and range inputs.
581///
582/// # Examples
583///
584/// ```yaml,sandbox
585/// title: "Median of an odd-sized set"
586/// formula: "=MEDIAN(1,3,8)"
587/// expected: 3
588/// ```
589///
590/// ```yaml,sandbox
591/// title: "Median of an even-sized range"
592/// grid:
593///   A1: 1
594///   A2: 2
595///   A3: 10
596///   A4: 12
597/// formula: "=MEDIAN(A1:A4)"
598/// expected: 6
599/// ```
600///
601/// ```yaml,docs
602/// related:
603///   - AVERAGE
604///   - MODE.SNGL
605///   - QUARTILE.INC
606/// faq:
607///   - q: "When does MEDIAN return #NUM!?"
608///     a: "MEDIAN returns #NUM! when no numeric values are available after filtering/coercion."
609/// ```
610#[derive(Debug)]
611pub struct MEDIAN;
612/// [formualizer-docgen:schema:start]
613/// Name: MEDIAN
614/// Type: MEDIAN
615/// Min args: 1
616/// Max args: variadic
617/// Variadic: true
618/// Signature: MEDIAN(arg1...: number@range)
619/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
620/// Caps: PURE, REDUCTION, NUMERIC_ONLY
621/// [formualizer-docgen:schema:end]
622impl Function for MEDIAN {
623    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
624    fn name(&self) -> &'static str {
625        "MEDIAN"
626    }
627    fn min_args(&self) -> usize {
628        1
629    }
630    fn variadic(&self) -> bool {
631        true
632    }
633    fn arg_schema(&self) -> &'static [ArgSchema] {
634        &ARG_RANGE_NUM_LENIENT_ONE[..]
635    }
636    fn eval<'a, 'b, 'c>(
637        &self,
638        args: &'c [ArgumentHandle<'a, 'b>],
639        _ctx: &dyn FunctionContext<'b>,
640    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
641        let mut nums = collect_numeric_stats(args)?;
642        if nums.is_empty() {
643            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
644                ExcelError::new_num(),
645            )));
646        }
647        nums.sort_by(|a, b| a.partial_cmp(b).unwrap());
648        let n = nums.len();
649        let mid = n / 2;
650        let med = if n % 2 == 1 {
651            nums[mid]
652        } else {
653            (nums[mid - 1] + nums[mid]) / 2.0
654        };
655        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(med)))
656    }
657}
658
659/// Estimates sample standard deviation using `n-1` in the denominator.
660///
661/// `STDEV.S` measures spread when your values represent a sample of a larger population.
662///
663/// # Remarks
664/// - Requires at least two numeric values.
665/// - Returns `#DIV/0!` when fewer than two numeric values are provided.
666/// - Non-numeric values in referenced ranges are ignored.
667///
668/// # Examples
669///
670/// ```yaml,sandbox
671/// title: "Sample standard deviation from scalar arguments"
672/// formula: "=STDEV.S(2,4,6)"
673/// expected: 2
674/// ```
675///
676/// ```yaml,sandbox
677/// title: "Sample standard deviation from a range"
678/// grid:
679///   A1: 5
680///   A2: 7
681///   A3: 9
682/// formula: "=STDEV.S(A1:A3)"
683/// expected: 2
684/// ```
685///
686/// ```yaml,docs
687/// related:
688///   - STDEV.P
689///   - VAR.S
690///   - VAR.P
691/// faq:
692///   - q: "Why does STDEV.S return #DIV/0!?"
693///     a: "Sample standard deviation needs at least two numeric values."
694/// ```
695#[derive(Debug)]
696pub struct StdevSample; // sample
697/// [formualizer-docgen:schema:start]
698/// Name: STDEV.S
699/// Type: StdevSample
700/// Min args: 1
701/// Max args: variadic
702/// Variadic: true
703/// Signature: STDEV.S(arg1...: number@range)
704/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
705/// Caps: PURE, REDUCTION, NUMERIC_ONLY, STREAM_OK
706/// [formualizer-docgen:schema:end]
707impl Function for StdevSample {
708    func_caps!(PURE, NUMERIC_ONLY, REDUCTION, STREAM_OK);
709    fn name(&self) -> &'static str {
710        "STDEV.S"
711    }
712    fn aliases(&self) -> &'static [&'static str] {
713        &["STDEV"]
714    }
715    fn min_args(&self) -> usize {
716        1
717    }
718    fn variadic(&self) -> bool {
719        true
720    }
721    fn arg_schema(&self) -> &'static [ArgSchema] {
722        &ARG_RANGE_NUM_LENIENT_ONE[..]
723    }
724    fn eval<'a, 'b, 'c>(
725        &self,
726        args: &'c [ArgumentHandle<'a, 'b>],
727        _ctx: &dyn FunctionContext<'b>,
728    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
729        let nums = collect_numeric_stats(args)?;
730        let n = nums.len();
731        if n < 2 {
732            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
733                ExcelError::from_error_string("#DIV/0!"),
734            )));
735        }
736        let mean = nums.iter().sum::<f64>() / (n as f64);
737        let mut ss = 0.0;
738        for &v in &nums {
739            let d = v - mean;
740            ss += d * d;
741        }
742        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
743            (ss / ((n - 1) as f64)).sqrt(),
744        )))
745    }
746}
747
748/// Returns population standard deviation using `n` in the denominator.
749///
750/// Use `STDEV.P` when your values represent the entire population, not a sample.
751///
752/// # Remarks
753/// - Requires at least one numeric value.
754/// - Returns `#DIV/0!` when no numeric values are provided.
755/// - Non-numeric values in referenced ranges are ignored.
756///
757/// # Examples
758///
759/// ```yaml,sandbox
760/// title: "Population standard deviation from scalar arguments"
761/// formula: "=STDEV.P(2,4,6)"
762/// expected: 1.632993161855452
763/// ```
764///
765/// ```yaml,sandbox
766/// title: "Population standard deviation from a range"
767/// grid:
768///   A1: 1
769///   A2: 2
770///   A3: 3
771/// formula: "=STDEV.P(A1:A3)"
772/// expected: 0.816496580927726
773/// ```
774///
775/// ```yaml,docs
776/// related:
777///   - STDEV.S
778///   - VAR.P
779///   - VAR.S
780/// faq:
781///   - q: "When does STDEV.P return #DIV/0!?"
782///     a: "It returns #DIV/0! when no numeric values are provided."
783/// ```
784#[derive(Debug)]
785pub struct StdevPop; // population
786/// [formualizer-docgen:schema:start]
787/// Name: STDEV.P
788/// Type: StdevPop
789/// Min args: 1
790/// Max args: variadic
791/// Variadic: true
792/// Signature: STDEV.P(arg1...: number@range)
793/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
794/// Caps: PURE, REDUCTION, NUMERIC_ONLY, STREAM_OK
795/// [formualizer-docgen:schema:end]
796impl Function for StdevPop {
797    func_caps!(PURE, NUMERIC_ONLY, REDUCTION, STREAM_OK);
798    fn name(&self) -> &'static str {
799        "STDEV.P"
800    }
801    fn aliases(&self) -> &'static [&'static str] {
802        &["STDEVP"]
803    }
804    fn min_args(&self) -> usize {
805        1
806    }
807    fn variadic(&self) -> bool {
808        true
809    }
810    fn arg_schema(&self) -> &'static [ArgSchema] {
811        &ARG_RANGE_NUM_LENIENT_ONE[..]
812    }
813    fn eval<'a, 'b, 'c>(
814        &self,
815        args: &'c [ArgumentHandle<'a, 'b>],
816        _ctx: &dyn FunctionContext<'b>,
817    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
818        let nums = collect_numeric_stats(args)?;
819        let n = nums.len();
820        if n == 0 {
821            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
822                ExcelError::from_error_string("#DIV/0!"),
823            )));
824        }
825        let mean = nums.iter().sum::<f64>() / (n as f64);
826        let mut ss = 0.0;
827        for &v in &nums {
828            let d = v - mean;
829            ss += d * d;
830        }
831        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
832            (ss / (n as f64)).sqrt(),
833        )))
834    }
835}
836
837/// Estimates sample variance using `n-1` in the denominator.
838///
839/// `VAR.S` is the squared counterpart of `STDEV.S` for sample-based variability.
840///
841/// # Remarks
842/// - Requires at least two numeric values.
843/// - Returns `#DIV/0!` when fewer than two numeric values are provided.
844/// - Non-numeric values in referenced ranges are ignored.
845///
846/// # Examples
847///
848/// ```yaml,sandbox
849/// title: "Sample variance from scalar arguments"
850/// formula: "=VAR.S(2,4,6)"
851/// expected: 4
852/// ```
853///
854/// ```yaml,sandbox
855/// title: "Sample variance from a range"
856/// grid:
857///   A1: 1
858///   A2: 2
859///   A3: 3
860/// formula: "=VAR.S(A1:A3)"
861/// expected: 1
862/// ```
863///
864/// ```yaml,docs
865/// related:
866///   - VAR.P
867///   - STDEV.S
868///   - STDEV.P
869/// faq:
870///   - q: "Why does VAR.S return #DIV/0!?"
871///     a: "Sample variance requires at least two numeric observations."
872/// ```
873#[derive(Debug)]
874pub struct VarSample; // sample variance
875/// [formualizer-docgen:schema:start]
876/// Name: VAR.S
877/// Type: VarSample
878/// Min args: 1
879/// Max args: variadic
880/// Variadic: true
881/// Signature: VAR.S(arg1...: number@range)
882/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
883/// Caps: PURE, REDUCTION, NUMERIC_ONLY, STREAM_OK
884/// [formualizer-docgen:schema:end]
885impl Function for VarSample {
886    func_caps!(PURE, NUMERIC_ONLY, REDUCTION, STREAM_OK);
887    fn name(&self) -> &'static str {
888        "VAR.S"
889    }
890    fn aliases(&self) -> &'static [&'static str] {
891        &["VAR"]
892    }
893    fn min_args(&self) -> usize {
894        1
895    }
896    fn variadic(&self) -> bool {
897        true
898    }
899    fn arg_schema(&self) -> &'static [ArgSchema] {
900        &ARG_RANGE_NUM_LENIENT_ONE[..]
901    }
902    fn eval<'a, 'b, 'c>(
903        &self,
904        args: &'c [ArgumentHandle<'a, 'b>],
905        _ctx: &dyn FunctionContext<'b>,
906    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
907        let nums = collect_numeric_stats(args)?;
908        let n = nums.len();
909        if n < 2 {
910            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
911                ExcelError::from_error_string("#DIV/0!"),
912            )));
913        }
914        let mean = nums.iter().sum::<f64>() / (n as f64);
915        let mut ss = 0.0;
916        for &v in &nums {
917            let d = v - mean;
918            ss += d * d;
919        }
920        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
921            ss / ((n - 1) as f64),
922        )))
923    }
924}
925
926/// Returns population variance using `n` in the denominator.
927///
928/// `VAR.P` describes dispersion for a complete population of numeric values.
929///
930/// # Remarks
931/// - Requires at least one numeric value.
932/// - Returns `#DIV/0!` when no numeric values are provided.
933/// - Non-numeric values in referenced ranges are ignored.
934///
935/// # Examples
936///
937/// ```yaml,sandbox
938/// title: "Population variance from scalar arguments"
939/// formula: "=VAR.P(2,4,6)"
940/// expected: 2.6666666666666665
941/// ```
942///
943/// ```yaml,sandbox
944/// title: "Population variance from a range"
945/// grid:
946///   A1: 1
947///   A2: 2
948///   A3: 3
949/// formula: "=VAR.P(A1:A3)"
950/// expected: 0.6666666666666666
951/// ```
952///
953/// ```yaml,docs
954/// related:
955///   - VAR.S
956///   - STDEV.P
957///   - STDEV.S
958/// faq:
959///   - q: "What is the denominator difference vs VAR.S?"
960///     a: "VAR.P divides by n, while VAR.S divides by n-1."
961/// ```
962#[derive(Debug)]
963pub struct VarPop; // population variance
964/// [formualizer-docgen:schema:start]
965/// Name: VAR.P
966/// Type: VarPop
967/// Min args: 1
968/// Max args: variadic
969/// Variadic: true
970/// Signature: VAR.P(arg1...: number@range)
971/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
972/// Caps: PURE, REDUCTION, NUMERIC_ONLY, STREAM_OK
973/// [formualizer-docgen:schema:end]
974impl Function for VarPop {
975    func_caps!(PURE, NUMERIC_ONLY, REDUCTION, STREAM_OK);
976    fn name(&self) -> &'static str {
977        "VAR.P"
978    }
979    fn aliases(&self) -> &'static [&'static str] {
980        &["VARP"]
981    }
982    fn min_args(&self) -> usize {
983        1
984    }
985    fn variadic(&self) -> bool {
986        true
987    }
988    fn arg_schema(&self) -> &'static [ArgSchema] {
989        &ARG_RANGE_NUM_LENIENT_ONE[..]
990    }
991    fn eval<'a, 'b, 'c>(
992        &self,
993        args: &'c [ArgumentHandle<'a, 'b>],
994        _ctx: &dyn FunctionContext<'b>,
995    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
996        let nums = collect_numeric_stats(args)?;
997        let n = nums.len();
998        if n == 0 {
999            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1000                ExcelError::from_error_string("#DIV/0!"),
1001            )));
1002        }
1003        let mean = nums.iter().sum::<f64>() / (n as f64);
1004        let mut ss = 0.0;
1005        for &v in &nums {
1006            let d = v - mean;
1007            ss += d * d;
1008        }
1009        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
1010            ss / (n as f64),
1011        )))
1012    }
1013}
1014
1015// MODE.SNGL (alias MODE) and MODE.MULT
1016/// Returns the most frequently occurring value in a data set.
1017///
1018/// `MODE.SNGL` returns a single mode value and reports `#N/A` if no value repeats.
1019///
1020/// # Remarks
1021/// - Returns the first mode encountered after sorting when frequencies tie.
1022/// - Returns `#N/A` when every numeric value appears only once.
1023/// - Alias `MODE` is supported.
1024///
1025/// # Examples
1026///
1027/// ```yaml,sandbox
1028/// title: "Single mode from scalar arguments"
1029/// formula: "=MODE.SNGL(1,2,2,3)"
1030/// expected: 2
1031/// ```
1032///
1033/// ```yaml,sandbox
1034/// title: "Single mode from a range"
1035/// grid:
1036///   A1: 4
1037///   A2: 4
1038///   A3: 6
1039///   A4: 6
1040///   A5: 6
1041/// formula: "=MODE.SNGL(A1:A5)"
1042/// expected: 6
1043/// ```
1044///
1045/// ```yaml,docs
1046/// related:
1047///   - MODE.MULT
1048///   - MEDIAN
1049///   - AVERAGE
1050/// faq:
1051///   - q: "When does MODE.SNGL return #N/A?"
1052///     a: "It returns #N/A when no value repeats in the numeric dataset."
1053/// ```
1054#[derive(Debug)]
1055pub struct ModeSingleFn;
1056/// [formualizer-docgen:schema:start]
1057/// Name: MODE.SNGL
1058/// Type: ModeSingleFn
1059/// Min args: 1
1060/// Max args: variadic
1061/// Variadic: true
1062/// Signature: MODE.SNGL(arg1...: number@range)
1063/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1064/// Caps: PURE, REDUCTION, NUMERIC_ONLY
1065/// [formualizer-docgen:schema:end]
1066impl Function for ModeSingleFn {
1067    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
1068    fn name(&self) -> &'static str {
1069        "MODE.SNGL"
1070    }
1071    fn aliases(&self) -> &'static [&'static str] {
1072        &["MODE"]
1073    }
1074    fn min_args(&self) -> usize {
1075        1
1076    }
1077    fn variadic(&self) -> bool {
1078        true
1079    }
1080    fn arg_schema(&self) -> &'static [ArgSchema] {
1081        &ARG_RANGE_NUM_LENIENT_ONE[..]
1082    }
1083    fn eval<'a, 'b, 'c>(
1084        &self,
1085        args: &'c [ArgumentHandle<'a, 'b>],
1086        _ctx: &dyn FunctionContext<'b>,
1087    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1088        let mut nums = collect_numeric_stats(args)?;
1089        if nums.is_empty() {
1090            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1091                ExcelError::new_na(),
1092            )));
1093        }
1094        nums.sort_by(|a, b| a.partial_cmp(b).unwrap());
1095        let mut best_val = nums[0];
1096        let mut best_cnt = 1usize;
1097        let mut cur_val = nums[0];
1098        let mut cur_cnt = 1usize;
1099        for &v in &nums[1..] {
1100            if (v - cur_val).abs() < 1e-12 {
1101                cur_cnt += 1;
1102            } else {
1103                if cur_cnt > best_cnt {
1104                    best_cnt = cur_cnt;
1105                    best_val = cur_val;
1106                }
1107                cur_val = v;
1108                cur_cnt = 1;
1109            }
1110        }
1111        if cur_cnt > best_cnt {
1112            best_cnt = cur_cnt;
1113            best_val = cur_val;
1114        }
1115        if best_cnt <= 1 {
1116            Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1117                ExcelError::new_na(),
1118            )))
1119        } else {
1120            Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
1121                best_val,
1122            )))
1123        }
1124    }
1125}
1126
1127/// Returns all modal values as a vertical array.
1128///
1129/// Use `MODE.MULT` when a data set can have multiple values with the same highest frequency.
1130///
1131/// # Remarks
1132/// - Returns each tied mode as a separate row in the result array.
1133/// - Returns `#N/A` when every numeric value appears only once.
1134/// - Non-numeric values in referenced ranges are ignored.
1135///
1136/// # Examples
1137///
1138/// ```yaml,sandbox
1139/// title: "Multiple modes from direct values"
1140/// formula: "=MODE.MULT({1,2,2,3,3,4})"
1141/// expected:
1142///   - [2]
1143///   - [3]
1144/// ```
1145///
1146/// ```yaml,sandbox
1147/// title: "Single repeated mode still returns an array"
1148/// grid:
1149///   A1: 5
1150///   A2: 5
1151///   A3: 2
1152///   A4: 1
1153/// formula: "=MODE.MULT(A1:A4)"
1154/// expected:
1155///   - [5]
1156/// ```
1157///
1158/// ```yaml,docs
1159/// related:
1160///   - MODE.SNGL
1161///   - FREQUENCY
1162///   - MEDIAN
1163/// faq:
1164///   - q: "Why can MODE.MULT return an array result?"
1165///     a: "It emits every value tied for highest frequency as separate rows."
1166/// ```
1167#[derive(Debug)]
1168pub struct ModeMultiFn;
1169/// [formualizer-docgen:schema:start]
1170/// Name: MODE.MULT
1171/// Type: ModeMultiFn
1172/// Min args: 1
1173/// Max args: variadic
1174/// Variadic: true
1175/// Signature: MODE.MULT(arg1...: number@range)
1176/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1177/// Caps: PURE, REDUCTION, NUMERIC_ONLY
1178/// [formualizer-docgen:schema:end]
1179impl Function for ModeMultiFn {
1180    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
1181    fn name(&self) -> &'static str {
1182        "MODE.MULT"
1183    }
1184    fn min_args(&self) -> usize {
1185        1
1186    }
1187    fn variadic(&self) -> bool {
1188        true
1189    }
1190    fn arg_schema(&self) -> &'static [ArgSchema] {
1191        &ARG_RANGE_NUM_LENIENT_ONE[..]
1192    }
1193    fn eval<'a, 'b, 'c>(
1194        &self,
1195        args: &'c [ArgumentHandle<'a, 'b>],
1196        _ctx: &dyn FunctionContext<'b>,
1197    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1198        let mut nums = collect_numeric_stats(args)?;
1199        if nums.is_empty() {
1200            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1201                ExcelError::new_na(),
1202            )));
1203        }
1204        nums.sort_by(|a, b| a.partial_cmp(b).unwrap());
1205        let mut runs: Vec<(f64, usize)> = Vec::new();
1206        let mut cur_val = nums[0];
1207        let mut cur_cnt = 1usize;
1208        for &v in &nums[1..] {
1209            if (v - cur_val).abs() < 1e-12 {
1210                cur_cnt += 1;
1211            } else {
1212                runs.push((cur_val, cur_cnt));
1213                cur_val = v;
1214                cur_cnt = 1;
1215            }
1216        }
1217        runs.push((cur_val, cur_cnt));
1218        let max_freq = runs.iter().map(|r| r.1).max().unwrap_or(0);
1219        if max_freq <= 1 {
1220            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1221                ExcelError::new_na(),
1222            )));
1223        }
1224        let rows: Vec<Vec<LiteralValue>> = runs
1225            .into_iter()
1226            .filter(|&(_, c)| c == max_freq)
1227            .map(|(v, _)| vec![LiteralValue::Number(v)])
1228            .collect();
1229        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Array(rows)))
1230    }
1231}
1232
1233/// Returns the k-th percentile of a data set using inclusive interpolation.
1234///
1235/// `PERCENTILE.INC` accepts percentile values from `0` through `1` and interpolates between
1236/// sorted values as needed.
1237///
1238/// # Remarks
1239/// - `k` must be in the inclusive range `[0, 1]`.
1240/// - Returns `#NUM!` for empty numeric input or invalid percentile arguments.
1241/// - Alias `PERCENTILE` is supported.
1242///
1243/// # Examples
1244///
1245/// ```yaml,sandbox
1246/// title: "Inclusive 25th percentile from direct values"
1247/// formula: "=PERCENTILE.INC({1,2,3,4,5},0.25)"
1248/// expected: 2
1249/// ```
1250///
1251/// ```yaml,sandbox
1252/// title: "Inclusive median-style interpolation from a range"
1253/// grid:
1254///   A1: 10
1255///   A2: 20
1256///   A3: 30
1257///   A4: 40
1258/// formula: "=PERCENTILE.INC(A1:A4,0.5)"
1259/// expected: 25
1260/// ```
1261///
1262/// ```yaml,docs
1263/// related:
1264///   - PERCENTILE.EXC
1265///   - QUARTILE.INC
1266///   - PERCENTRANK.INC
1267/// faq:
1268///   - q: "What k range is valid for PERCENTILE.INC?"
1269///     a: "k must be between 0 and 1 inclusive; outside that range returns #NUM!."
1270/// ```
1271#[derive(Debug)]
1272pub struct PercentileInc; // inclusive
1273/// [formualizer-docgen:schema:start]
1274/// Name: PERCENTILE.INC
1275/// Type: PercentileInc
1276/// Min args: 2
1277/// Max args: variadic
1278/// Variadic: true
1279/// Signature: PERCENTILE.INC(arg1...: number@range)
1280/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1281/// Caps: PURE, NUMERIC_ONLY
1282/// [formualizer-docgen:schema:end]
1283impl Function for PercentileInc {
1284    func_caps!(PURE, NUMERIC_ONLY);
1285    fn name(&self) -> &'static str {
1286        "PERCENTILE.INC"
1287    }
1288    fn aliases(&self) -> &'static [&'static str] {
1289        &["PERCENTILE"]
1290    }
1291    fn min_args(&self) -> usize {
1292        2
1293    }
1294    fn variadic(&self) -> bool {
1295        true
1296    }
1297    fn arg_schema(&self) -> &'static [ArgSchema] {
1298        &ARG_RANGE_NUM_LENIENT_ONE[..]
1299    }
1300    fn eval<'a, 'b, 'c>(
1301        &self,
1302        args: &'c [ArgumentHandle<'a, 'b>],
1303        _ctx: &dyn FunctionContext<'b>,
1304    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1305        if args.len() < 2 {
1306            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1307                ExcelError::new_num(),
1308            )));
1309        }
1310        let pv = scalar_like_value(args.last().unwrap())?;
1311        let p = match coerce_num(&pv) {
1312            Ok(n) => n,
1313            Err(_) => {
1314                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1315                    ExcelError::new_num(),
1316                )));
1317            }
1318        };
1319        let mut nums = collect_numeric_stats(&args[..args.len() - 1])?;
1320        if nums.is_empty() {
1321            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1322                ExcelError::new_num(),
1323            )));
1324        }
1325        nums.sort_by(|a, b| a.partial_cmp(b).unwrap());
1326        match percentile_inc(&nums, p) {
1327            Ok(v) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(v))),
1328            Err(e) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
1329        }
1330    }
1331}
1332
1333/// Returns the k-th percentile of a data set using exclusive interpolation.
1334///
1335/// `PERCENTILE.EXC` uses the `n+1` rank basis and excludes the exact endpoints `0` and `1`.
1336///
1337/// # Remarks
1338/// - `k` must satisfy `0 < k < 1`.
1339/// - Returns `#NUM!` when the percentile falls outside the valid rank range for the data size.
1340/// - Returns `#NUM!` for empty numeric input.
1341///
1342/// # Examples
1343///
1344/// ```yaml,sandbox
1345/// title: "Exclusive 25th percentile from direct values"
1346/// formula: "=PERCENTILE.EXC({1,2,3,4,5},0.25)"
1347/// expected: 1.5
1348/// ```
1349///
1350/// ```yaml,sandbox
1351/// title: "Exclusive percentile from a range"
1352/// grid:
1353///   A1: 10
1354///   A2: 20
1355///   A3: 30
1356///   A4: 40
1357///   A5: 50
1358/// formula: "=PERCENTILE.EXC(A1:A5,0.6)"
1359/// expected: 36
1360/// ```
1361///
1362/// ```yaml,docs
1363/// related:
1364///   - PERCENTILE.INC
1365///   - QUARTILE.EXC
1366///   - PERCENTRANK.EXC
1367/// faq:
1368///   - q: "Why does PERCENTILE.EXC reject k=0 or k=1?"
1369///     a: "Exclusive percentile uses the n+1 basis and requires strictly 0 < k < 1."
1370/// ```
1371#[derive(Debug)]
1372pub struct PercentileExc; // exclusive
1373/// [formualizer-docgen:schema:start]
1374/// Name: PERCENTILE.EXC
1375/// Type: PercentileExc
1376/// Min args: 2
1377/// Max args: variadic
1378/// Variadic: true
1379/// Signature: PERCENTILE.EXC(arg1...: number@range)
1380/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1381/// Caps: PURE, NUMERIC_ONLY
1382/// [formualizer-docgen:schema:end]
1383impl Function for PercentileExc {
1384    func_caps!(PURE, NUMERIC_ONLY);
1385    fn name(&self) -> &'static str {
1386        "PERCENTILE.EXC"
1387    }
1388    fn min_args(&self) -> usize {
1389        2
1390    }
1391    fn variadic(&self) -> bool {
1392        true
1393    }
1394    fn arg_schema(&self) -> &'static [ArgSchema] {
1395        &ARG_RANGE_NUM_LENIENT_ONE[..]
1396    }
1397    fn eval<'a, 'b, 'c>(
1398        &self,
1399        args: &'c [ArgumentHandle<'a, 'b>],
1400        _ctx: &dyn FunctionContext<'b>,
1401    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1402        if args.len() < 2 {
1403            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1404                ExcelError::new_num(),
1405            )));
1406        }
1407        let pv = scalar_like_value(args.last().unwrap())?;
1408        let p = match coerce_num(&pv) {
1409            Ok(n) => n,
1410            Err(_) => {
1411                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1412                    ExcelError::new_num(),
1413                )));
1414            }
1415        };
1416        let mut nums = collect_numeric_stats(&args[..args.len() - 1])?;
1417        if nums.is_empty() {
1418            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1419                ExcelError::new_num(),
1420            )));
1421        }
1422        nums.sort_by(|a, b| a.partial_cmp(b).unwrap());
1423        match percentile_exc(&nums, p) {
1424            Ok(v) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(v))),
1425            Err(e) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
1426        }
1427    }
1428}
1429
1430/// Returns an inclusive quartile value for a data set.
1431///
1432/// `QUARTILE.INC` maps quartile index `0..4` onto minimum, quartiles, median, and maximum.
1433///
1434/// # Remarks
1435/// - Valid quartile index values are `0`, `1`, `2`, `3`, and `4`.
1436/// - Uses inclusive percentile logic for quartiles `1` through `3`.
1437/// - Returns `#NUM!` for invalid quartile index values or empty numeric input.
1438/// - Alias `QUARTILE` is supported.
1439///
1440/// # Examples
1441///
1442/// ```yaml,sandbox
1443/// title: "First quartile from direct values"
1444/// formula: "=QUARTILE.INC({1,2,3,4,5},1)"
1445/// expected: 2
1446/// ```
1447///
1448/// ```yaml,sandbox
1449/// title: "Third quartile from a range"
1450/// grid:
1451///   A1: 10
1452///   A2: 20
1453///   A3: 30
1454///   A4: 40
1455/// formula: "=QUARTILE.INC(A1:A4,3)"
1456/// expected: 32.5
1457/// ```
1458///
1459/// ```yaml,docs
1460/// related:
1461///   - QUARTILE.EXC
1462///   - PERCENTILE.INC
1463///   - MEDIAN
1464/// faq:
1465///   - q: "Which quartile numbers are valid for QUARTILE.INC?"
1466///     a: "Only 0 through 4 are valid; other quartile indices return #NUM!."
1467/// ```
1468#[derive(Debug)]
1469pub struct QuartileInc; // quartile inclusive
1470/// [formualizer-docgen:schema:start]
1471/// Name: QUARTILE.INC
1472/// Type: QuartileInc
1473/// Min args: 2
1474/// Max args: variadic
1475/// Variadic: true
1476/// Signature: QUARTILE.INC(arg1...: number@range)
1477/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1478/// Caps: PURE, NUMERIC_ONLY
1479/// [formualizer-docgen:schema:end]
1480impl Function for QuartileInc {
1481    func_caps!(PURE, NUMERIC_ONLY);
1482    fn name(&self) -> &'static str {
1483        "QUARTILE.INC"
1484    }
1485    fn aliases(&self) -> &'static [&'static str] {
1486        &["QUARTILE"]
1487    }
1488    fn min_args(&self) -> usize {
1489        2
1490    }
1491    fn variadic(&self) -> bool {
1492        true
1493    }
1494    fn arg_schema(&self) -> &'static [ArgSchema] {
1495        &ARG_RANGE_NUM_LENIENT_ONE[..]
1496    }
1497    fn eval<'a, 'b, 'c>(
1498        &self,
1499        args: &'c [ArgumentHandle<'a, 'b>],
1500        _ctx: &dyn FunctionContext<'b>,
1501    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1502        if args.len() < 2 {
1503            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1504                ExcelError::new_num(),
1505            )));
1506        }
1507        let qv = scalar_like_value(args.last().unwrap())?;
1508        let q = match coerce_num(&qv) {
1509            Ok(n) => n,
1510            Err(_) => {
1511                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1512                    ExcelError::new_num(),
1513                )));
1514            }
1515        };
1516        let q_i = q as i64;
1517        if !(0..=4).contains(&q_i) {
1518            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1519                ExcelError::new_num(),
1520            )));
1521        }
1522        let mut nums = collect_numeric_stats(&args[..args.len() - 1])?;
1523        if nums.is_empty() {
1524            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1525                ExcelError::new_num(),
1526            )));
1527        }
1528        nums.sort_by(|a, b| a.partial_cmp(b).unwrap());
1529        let p = match q_i {
1530            0 => {
1531                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
1532                    nums[0],
1533                )));
1534            }
1535            4 => {
1536                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
1537                    nums[nums.len() - 1],
1538                )));
1539            }
1540            1 => 0.25,
1541            2 => 0.5,
1542            3 => 0.75,
1543            _ => {
1544                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1545                    ExcelError::new_num(),
1546                )));
1547            }
1548        };
1549        match percentile_inc(&nums, p) {
1550            Ok(v) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(v))),
1551            Err(e) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
1552        }
1553    }
1554}
1555
1556/// Returns an exclusive quartile value for a data set.
1557///
1558/// `QUARTILE.EXC` applies exclusive percentile interpolation and supports quartiles `1` through
1559/// `3`.
1560///
1561/// # Remarks
1562/// - Valid quartile index values are `1`, `2`, and `3`.
1563/// - Returns `#NUM!` for invalid quartile index values.
1564/// - Returns `#NUM!` when the input is too small for exclusive quartile interpolation.
1565///
1566/// # Examples
1567///
1568/// ```yaml,sandbox
1569/// title: "First exclusive quartile from direct values"
1570/// formula: "=QUARTILE.EXC({1,2,3,4,5,6,7,8},1)"
1571/// expected: 2.25
1572/// ```
1573///
1574/// ```yaml,sandbox
1575/// title: "Third exclusive quartile from a range"
1576/// grid:
1577///   A1: 10
1578///   A2: 20
1579///   A3: 30
1580///   A4: 40
1581///   A5: 50
1582///   A6: 60
1583///   A7: 70
1584///   A8: 80
1585/// formula: "=QUARTILE.EXC(A1:A8,3)"
1586/// expected: 67.5
1587/// ```
1588///
1589/// ```yaml,docs
1590/// related:
1591///   - QUARTILE.INC
1592///   - PERCENTILE.EXC
1593///   - MEDIAN
1594/// faq:
1595///   - q: "Why can QUARTILE.EXC return #NUM! on small datasets?"
1596///     a: "Exclusive quartiles need enough data for valid interior rank interpolation."
1597/// ```
1598#[derive(Debug)]
1599pub struct QuartileExc; // quartile exclusive
1600/// [formualizer-docgen:schema:start]
1601/// Name: QUARTILE.EXC
1602/// Type: QuartileExc
1603/// Min args: 2
1604/// Max args: variadic
1605/// Variadic: true
1606/// Signature: QUARTILE.EXC(arg1...: number@range)
1607/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1608/// Caps: PURE, NUMERIC_ONLY
1609/// [formualizer-docgen:schema:end]
1610impl Function for QuartileExc {
1611    func_caps!(PURE, NUMERIC_ONLY);
1612    fn name(&self) -> &'static str {
1613        "QUARTILE.EXC"
1614    }
1615    fn min_args(&self) -> usize {
1616        2
1617    }
1618    fn variadic(&self) -> bool {
1619        true
1620    }
1621    fn arg_schema(&self) -> &'static [ArgSchema] {
1622        &ARG_RANGE_NUM_LENIENT_ONE[..]
1623    }
1624    fn eval<'a, 'b, 'c>(
1625        &self,
1626        args: &'c [ArgumentHandle<'a, 'b>],
1627        _ctx: &dyn FunctionContext<'b>,
1628    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1629        if args.len() < 2 {
1630            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1631                ExcelError::new_num(),
1632            )));
1633        }
1634        let qv = scalar_like_value(args.last().unwrap())?;
1635        let q = match coerce_num(&qv) {
1636            Ok(n) => n,
1637            Err(_) => {
1638                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1639                    ExcelError::new_num(),
1640                )));
1641            }
1642        };
1643        let q_i = q as i64;
1644        if !(1..=3).contains(&q_i) {
1645            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1646                ExcelError::new_num(),
1647            )));
1648        }
1649        let mut nums = collect_numeric_stats(&args[..args.len() - 1])?;
1650        if nums.len() < 2 {
1651            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1652                ExcelError::new_num(),
1653            )));
1654        }
1655        nums.sort_by(|a, b| a.partial_cmp(b).unwrap());
1656        let p = match q_i {
1657            1 => 0.25,
1658            2 => 0.5,
1659            3 => 0.75,
1660            _ => {
1661                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1662                    ExcelError::new_num(),
1663                )));
1664            }
1665        };
1666        match percentile_exc(&nums, p) {
1667            Ok(v) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(v))),
1668            Err(e) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
1669        }
1670    }
1671}
1672
1673/// Multiplies all numeric arguments and returns their product.
1674///
1675/// `PRODUCT` is useful for chained growth factors, scaling ratios, and compound multipliers.
1676///
1677/// # Remarks
1678/// - Non-numeric values in referenced ranges are ignored.
1679/// - Returns `0` when no numeric values are found.
1680/// - Direct scalar arguments still attempt numeric coercion.
1681///
1682/// # Examples
1683///
1684/// ```yaml,sandbox
1685/// title: "Product of scalar values"
1686/// formula: "=PRODUCT(2,3,4)"
1687/// expected: 24
1688/// ```
1689///
1690/// ```yaml,sandbox
1691/// title: "Product from a range"
1692/// grid:
1693///   A1: 1
1694///   A2: 5
1695///   A3: 10
1696/// formula: "=PRODUCT(A1:A3)"
1697/// expected: 50
1698/// ```
1699///
1700/// ```yaml,docs
1701/// related:
1702///   - SUM
1703///   - GEOMEAN
1704///   - SUMPRODUCT
1705/// faq:
1706///   - q: "Why does PRODUCT return 0 when no numeric inputs are found?"
1707///     a: "This implementation returns 0 for an empty numeric set after filtering."
1708/// ```
1709#[derive(Debug)]
1710pub struct ProductFn;
1711/// [formualizer-docgen:schema:start]
1712/// Name: PRODUCT
1713/// Type: ProductFn
1714/// Min args: 1
1715/// Max args: variadic
1716/// Variadic: true
1717/// Signature: PRODUCT(arg1...: number@range)
1718/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1719/// Caps: PURE, REDUCTION, NUMERIC_ONLY
1720/// [formualizer-docgen:schema:end]
1721impl Function for ProductFn {
1722    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
1723    fn name(&self) -> &'static str {
1724        "PRODUCT"
1725    }
1726    fn min_args(&self) -> usize {
1727        1
1728    }
1729    fn variadic(&self) -> bool {
1730        true
1731    }
1732    fn arg_schema(&self) -> &'static [ArgSchema] {
1733        &ARG_RANGE_NUM_LENIENT_ONE[..]
1734    }
1735    fn eval<'a, 'b, 'c>(
1736        &self,
1737        args: &'c [ArgumentHandle<'a, 'b>],
1738        _ctx: &dyn FunctionContext<'b>,
1739    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1740        let nums = collect_numeric_stats(args)?;
1741        if nums.is_empty() {
1742            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0)));
1743        }
1744        let result = nums.iter().product::<f64>();
1745        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
1746            result,
1747        )))
1748    }
1749}
1750
1751/// Returns the geometric mean of positive numeric values.
1752///
1753/// `GEOMEAN` is commonly used for rates of change and multiplicative growth comparisons.
1754///
1755/// # Remarks
1756/// - All numeric inputs must be strictly greater than `0`.
1757/// - Returns `#NUM!` if any value is `<= 0`, or if no numeric values are provided.
1758/// - Non-numeric values in referenced ranges are ignored.
1759///
1760/// # Examples
1761///
1762/// ```yaml,sandbox
1763/// title: "Geometric mean from scalar values"
1764/// formula: "=GEOMEAN(4,16)"
1765/// expected: 8
1766/// ```
1767///
1768/// ```yaml,sandbox
1769/// title: "Geometric mean from a range"
1770/// grid:
1771///   A1: 1
1772///   A2: 3
1773///   A3: 9
1774/// formula: "=GEOMEAN(A1:A3)"
1775/// expected: 3
1776/// ```
1777///
1778/// ```yaml,docs
1779/// related:
1780///   - HARMEAN
1781///   - PRODUCT
1782///   - AVERAGE
1783/// faq:
1784///   - q: "When does GEOMEAN return #NUM!?"
1785///     a: "GEOMEAN returns #NUM! if any numeric value is <= 0 or if no numeric values exist."
1786/// ```
1787#[derive(Debug)]
1788pub struct GeomeanFn;
1789/// [formualizer-docgen:schema:start]
1790/// Name: GEOMEAN
1791/// Type: GeomeanFn
1792/// Min args: 1
1793/// Max args: variadic
1794/// Variadic: true
1795/// Signature: GEOMEAN(arg1...: number@range)
1796/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1797/// Caps: PURE, REDUCTION, NUMERIC_ONLY
1798/// [formualizer-docgen:schema:end]
1799impl Function for GeomeanFn {
1800    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
1801    fn name(&self) -> &'static str {
1802        "GEOMEAN"
1803    }
1804    fn min_args(&self) -> usize {
1805        1
1806    }
1807    fn variadic(&self) -> bool {
1808        true
1809    }
1810    fn arg_schema(&self) -> &'static [ArgSchema] {
1811        &ARG_RANGE_NUM_LENIENT_ONE[..]
1812    }
1813    fn eval<'a, 'b, 'c>(
1814        &self,
1815        args: &'c [ArgumentHandle<'a, 'b>],
1816        _ctx: &dyn FunctionContext<'b>,
1817    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1818        let nums = collect_numeric_stats(args)?;
1819        if nums.is_empty() {
1820            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1821                ExcelError::new_num(),
1822            )));
1823        }
1824        // All values must be positive
1825        if nums.iter().any(|&n| n <= 0.0) {
1826            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1827                ExcelError::new_num(),
1828            )));
1829        }
1830        // Geometric mean = (x1 * x2 * ... * xn)^(1/n)
1831        // Use log to avoid overflow: exp(mean(ln(x)))
1832        let log_sum: f64 = nums.iter().map(|x| x.ln()).sum();
1833        let result = (log_sum / nums.len() as f64).exp();
1834        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
1835            result,
1836        )))
1837    }
1838}
1839
1840/// Returns the harmonic mean of positive numeric values.
1841///
1842/// `HARMEAN` emphasizes smaller values and is useful for averaging rates and ratios.
1843///
1844/// # Remarks
1845/// - All numeric inputs must be strictly greater than `0`.
1846/// - Returns `#NUM!` if any value is `<= 0`, or if no numeric values are provided.
1847/// - Non-numeric values in referenced ranges are ignored.
1848///
1849/// # Examples
1850///
1851/// ```yaml,sandbox
1852/// title: "Harmonic mean from scalar values"
1853/// formula: "=HARMEAN(1,2,4)"
1854/// expected: 1.7142857142857142
1855/// ```
1856///
1857/// ```yaml,sandbox
1858/// title: "Harmonic mean from a range"
1859/// grid:
1860///   A1: 2
1861///   A2: 3
1862///   A3: 6
1863/// formula: "=HARMEAN(A1:A3)"
1864/// expected: 3
1865/// ```
1866///
1867/// ```yaml,docs
1868/// related:
1869///   - GEOMEAN
1870///   - AVERAGE
1871///   - PRODUCT
1872/// faq:
1873///   - q: "Why does HARMEAN fail on zeros?"
1874///     a: "Harmonic mean uses reciprocals, so inputs must be strictly positive."
1875/// ```
1876#[derive(Debug)]
1877pub struct HarmeanFn;
1878/// [formualizer-docgen:schema:start]
1879/// Name: HARMEAN
1880/// Type: HarmeanFn
1881/// Min args: 1
1882/// Max args: variadic
1883/// Variadic: true
1884/// Signature: HARMEAN(arg1...: number@range)
1885/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1886/// Caps: PURE, REDUCTION, NUMERIC_ONLY
1887/// [formualizer-docgen:schema:end]
1888impl Function for HarmeanFn {
1889    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
1890    fn name(&self) -> &'static str {
1891        "HARMEAN"
1892    }
1893    fn min_args(&self) -> usize {
1894        1
1895    }
1896    fn variadic(&self) -> bool {
1897        true
1898    }
1899    fn arg_schema(&self) -> &'static [ArgSchema] {
1900        &ARG_RANGE_NUM_LENIENT_ONE[..]
1901    }
1902    fn eval<'a, 'b, 'c>(
1903        &self,
1904        args: &'c [ArgumentHandle<'a, 'b>],
1905        _ctx: &dyn FunctionContext<'b>,
1906    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1907        let nums = collect_numeric_stats(args)?;
1908        if nums.is_empty() {
1909            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1910                ExcelError::new_num(),
1911            )));
1912        }
1913        // All values must be positive
1914        if nums.iter().any(|&n| n <= 0.0) {
1915            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1916                ExcelError::new_num(),
1917            )));
1918        }
1919        // Harmonic mean = n / sum(1/x)
1920        let sum_reciprocals: f64 = nums.iter().map(|x| 1.0 / x).sum();
1921        let result = nums.len() as f64 / sum_reciprocals;
1922        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
1923            result,
1924        )))
1925    }
1926}
1927
1928/// Returns the average of absolute deviations from the mean.
1929///
1930/// `AVEDEV` provides a robust spread measure that is less sensitive to outliers than squared-error
1931/// metrics.
1932///
1933/// # Remarks
1934/// - Returns `#NUM!` when no numeric values are available.
1935/// - Non-numeric values in referenced ranges are ignored.
1936/// - Uses the arithmetic mean as the center point.
1937///
1938/// # Examples
1939///
1940/// ```yaml,sandbox
1941/// title: "Average absolute deviation from scalar values"
1942/// formula: "=AVEDEV(2,4,6)"
1943/// expected: 1.3333333333333333
1944/// ```
1945///
1946/// ```yaml,sandbox
1947/// title: "Average absolute deviation from a range"
1948/// grid:
1949///   A1: 1
1950///   A2: 1
1951///   A3: 3
1952///   A4: 5
1953/// formula: "=AVEDEV(A1:A4)"
1954/// expected: 1.5
1955/// ```
1956///
1957/// ```yaml,docs
1958/// related:
1959///   - DEVSQ
1960///   - STDEV.S
1961///   - VAR.S
1962/// faq:
1963///   - q: "What center does AVEDEV use for deviations?"
1964///     a: "It computes absolute deviations around the arithmetic mean of included values."
1965/// ```
1966#[derive(Debug)]
1967pub struct AvedevFn;
1968/// [formualizer-docgen:schema:start]
1969/// Name: AVEDEV
1970/// Type: AvedevFn
1971/// Min args: 1
1972/// Max args: variadic
1973/// Variadic: true
1974/// Signature: AVEDEV(arg1...: number@range)
1975/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1976/// Caps: PURE, REDUCTION, NUMERIC_ONLY
1977/// [formualizer-docgen:schema:end]
1978impl Function for AvedevFn {
1979    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
1980    fn name(&self) -> &'static str {
1981        "AVEDEV"
1982    }
1983    fn min_args(&self) -> usize {
1984        1
1985    }
1986    fn variadic(&self) -> bool {
1987        true
1988    }
1989    fn arg_schema(&self) -> &'static [ArgSchema] {
1990        &ARG_RANGE_NUM_LENIENT_ONE[..]
1991    }
1992    fn eval<'a, 'b, 'c>(
1993        &self,
1994        args: &'c [ArgumentHandle<'a, 'b>],
1995        _ctx: &dyn FunctionContext<'b>,
1996    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1997        let nums = collect_numeric_stats(args)?;
1998        if nums.is_empty() {
1999            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2000                ExcelError::new_num(),
2001            )));
2002        }
2003        let mean = nums.iter().sum::<f64>() / nums.len() as f64;
2004        let avedev = nums.iter().map(|x| (x - mean).abs()).sum::<f64>() / nums.len() as f64;
2005        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
2006            avedev,
2007        )))
2008    }
2009}
2010
2011/// Returns the sum of squared deviations from the mean.
2012///
2013/// `DEVSQ` is useful for variance-related calculations and diagnostics of spread.
2014///
2015/// # Remarks
2016/// - Returns `#NUM!` when no numeric values are available.
2017/// - Non-numeric values in referenced ranges are ignored.
2018/// - Uses the arithmetic mean of included values.
2019///
2020/// # Examples
2021///
2022/// ```yaml,sandbox
2023/// title: "Sum of squared deviations from scalar values"
2024/// formula: "=DEVSQ(2,4,6)"
2025/// expected: 8
2026/// ```
2027///
2028/// ```yaml,sandbox
2029/// title: "Sum of squared deviations from a range"
2030/// grid:
2031///   A1: 1
2032///   A2: 2
2033///   A3: 3
2034///   A4: 4
2035/// formula: "=DEVSQ(A1:A4)"
2036/// expected: 5
2037/// ```
2038#[derive(Debug)]
2039pub struct DevsqFn;
2040
2041/* ─────────────────────────── MAXIFS / MINIFS ──────────────────────────── */
2042
2043use super::utils::{ARG_ANY_ONE, criteria_match};
2044
2045/// Returns the maximum numeric value in a range that meets all criteria.
2046///
2047/// `MAXIFS` applies one or more `(criteria_range, criteria)` pairs and returns the largest
2048/// matching numeric value.
2049///
2050/// # Remarks
2051/// - Arguments must be `target_range` plus one or more criteria pairs.
2052/// - Criteria are combined with logical AND.
2053/// - Returns `0` when no cells satisfy all criteria.
2054/// - Non-numeric cells in `target_range` are ignored.
2055///
2056/// # Examples
2057///
2058/// ```yaml,sandbox
2059/// title: "Maximum value for one condition"
2060/// grid:
2061///   A1: 10
2062///   A2: 20
2063///   A3: 15
2064///   B1: "East"
2065///   B2: "West"
2066///   B3: "East"
2067/// formula: "=MAXIFS(A1:A3,B1:B3,\"East\")"
2068/// expected: 15
2069/// ```
2070///
2071/// ```yaml,sandbox
2072/// title: "Maximum value with two criteria"
2073/// grid:
2074///   A1: 100
2075///   A2: 80
2076///   A3: 90
2077///   A4: 70
2078///   B1: "A"
2079///   B2: "A"
2080///   B3: "B"
2081///   B4: "B"
2082///   C1: "Q1"
2083///   C2: "Q2"
2084///   C3: "Q1"
2085///   C4: "Q1"
2086/// formula: "=MAXIFS(A1:A4,B1:B4,\"B\",C1:C4,\"Q1\")"
2087/// expected: 90
2088/// ```
2089///
2090/// ```yaml,docs
2091/// related:
2092///   - MINIFS
2093///   - MAX
2094///   - SUMIFS
2095/// faq:
2096///   - q: "What does MAXIFS return when no rows match all criteria?"
2097///     a: "It returns 0 when no numeric target cells satisfy every criterion."
2098/// ```
2099#[derive(Debug)]
2100pub struct MaxIfsFn;
2101/// [formualizer-docgen:schema:start]
2102/// Name: MAXIFS
2103/// Type: MaxIfsFn
2104/// Min args: 3
2105/// Max args: variadic
2106/// Variadic: true
2107/// Signature: MAXIFS(arg1...: any@scalar)
2108/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
2109/// Caps: PURE, REDUCTION
2110/// [formualizer-docgen:schema:end]
2111impl Function for MaxIfsFn {
2112    func_caps!(PURE, REDUCTION);
2113    fn name(&self) -> &'static str {
2114        "MAXIFS"
2115    }
2116    fn min_args(&self) -> usize {
2117        3
2118    }
2119    fn variadic(&self) -> bool {
2120        true
2121    }
2122    fn arg_schema(&self) -> &'static [ArgSchema] {
2123        &ARG_ANY_ONE[..]
2124    }
2125    fn eval<'a, 'b, 'c>(
2126        &self,
2127        args: &'c [ArgumentHandle<'a, 'b>],
2128        _ctx: &dyn FunctionContext<'b>,
2129    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2130        eval_maxminifs(args, true)
2131    }
2132}
2133
2134/// Returns the minimum numeric value in a range that meets all criteria.
2135///
2136/// `MINIFS` evaluates one or more `(criteria_range, criteria)` pairs and returns the smallest
2137/// matching numeric value.
2138///
2139/// # Remarks
2140/// - Arguments must be `target_range` plus one or more criteria pairs.
2141/// - Criteria are combined with logical AND.
2142/// - Returns `0` when no cells satisfy all criteria.
2143/// - Non-numeric cells in `target_range` are ignored.
2144///
2145/// # Examples
2146///
2147/// ```yaml,sandbox
2148/// title: "Minimum value for one condition"
2149/// grid:
2150///   A1: 10
2151///   A2: 20
2152///   A3: 15
2153///   B1: "East"
2154///   B2: "West"
2155///   B3: "East"
2156/// formula: "=MINIFS(A1:A3,B1:B3,\"East\")"
2157/// expected: 10
2158/// ```
2159///
2160/// ```yaml,sandbox
2161/// title: "Minimum value with two criteria"
2162/// grid:
2163///   A1: 100
2164///   A2: 80
2165///   A3: 90
2166///   A4: 70
2167///   B1: "A"
2168///   B2: "A"
2169///   B3: "B"
2170///   B4: "B"
2171///   C1: "Q1"
2172///   C2: "Q2"
2173///   C3: "Q1"
2174///   C4: "Q1"
2175/// formula: "=MINIFS(A1:A4,B1:B4,\"B\",C1:C4,\"Q1\")"
2176/// expected: 70
2177/// ```
2178///
2179/// ```yaml,docs
2180/// related:
2181///   - MAXIFS
2182///   - MIN
2183///   - SUMIFS
2184/// faq:
2185///   - q: "How does MINIFS treat non-numeric target cells?"
2186///     a: "Non-numeric target cells are ignored; only numeric matches are eligible."
2187/// ```
2188#[derive(Debug)]
2189pub struct MinIfsFn;
2190/// [formualizer-docgen:schema:start]
2191/// Name: MINIFS
2192/// Type: MinIfsFn
2193/// Min args: 3
2194/// Max args: variadic
2195/// Variadic: true
2196/// Signature: MINIFS(arg1...: any@scalar)
2197/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
2198/// Caps: PURE, REDUCTION
2199/// [formualizer-docgen:schema:end]
2200impl Function for MinIfsFn {
2201    func_caps!(PURE, REDUCTION);
2202    fn name(&self) -> &'static str {
2203        "MINIFS"
2204    }
2205    fn min_args(&self) -> usize {
2206        3
2207    }
2208    fn variadic(&self) -> bool {
2209        true
2210    }
2211    fn arg_schema(&self) -> &'static [ArgSchema] {
2212        &ARG_ANY_ONE[..]
2213    }
2214    fn eval<'a, 'b, 'c>(
2215        &self,
2216        args: &'c [ArgumentHandle<'a, 'b>],
2217        _ctx: &dyn FunctionContext<'b>,
2218    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2219        eval_maxminifs(args, false)
2220    }
2221}
2222
2223/// Shared implementation for MAXIFS and MINIFS
2224fn eval_maxminifs<'a, 'b>(
2225    args: &[ArgumentHandle<'a, 'b>],
2226    is_max: bool,
2227) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2228    // Validate argument count: must be target_range + N pairs
2229    if args.len() < 3 || !(args.len() - 1).is_multiple_of(2) {
2230        return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2231            ExcelError::new_value().with_message(format!(
2232                "Function expects 1 target_range followed by N pairs (criteria_range, criteria); got {} args",
2233                args.len()
2234            )),
2235        )));
2236    }
2237
2238    // Get target range
2239    let target_view = match args[0].range_view() {
2240        Ok(v) => v,
2241        Err(_) => {
2242            // Single value case - if criteria match, return that value
2243            let target_val = args[0].value()?.into_literal();
2244            if let LiteralValue::Error(e) = target_val {
2245                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
2246            }
2247            // Check all criteria against empty/scalar
2248            let mut all_match = true;
2249            for i in (1..args.len()).step_by(2) {
2250                let crit_val = args[i].value()?.into_literal();
2251                let pred = crate::args::parse_criteria(&args[i + 1].value()?.into_literal())?;
2252                if !criteria_match(&pred, &crit_val) {
2253                    all_match = false;
2254                    break;
2255                }
2256            }
2257            if all_match {
2258                return match coerce_num(&target_val) {
2259                    Ok(n) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(n))),
2260                    Err(_) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0))),
2261                };
2262            }
2263            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0)));
2264        }
2265    };
2266
2267    let (rows, cols) = target_view.dims();
2268
2269    // Parse all criteria
2270    let mut criteria_ranges = Vec::new();
2271    let mut predicates = Vec::new();
2272    for i in (1..args.len()).step_by(2) {
2273        let crit_view = args[i].range_view().ok();
2274        let pred = crate::args::parse_criteria(&args[i + 1].value()?.into_literal())?;
2275        criteria_ranges.push(crit_view);
2276        predicates.push(pred);
2277    }
2278
2279    // Iterate through all cells and find max/min where all criteria match
2280    let mut result: Option<f64> = None;
2281
2282    for r in 0..rows {
2283        for c in 0..cols {
2284            // Check all criteria
2285            let mut all_match = true;
2286            for (crit_idx, pred) in predicates.iter().enumerate() {
2287                let crit_val = match &criteria_ranges[crit_idx] {
2288                    Some(view) => {
2289                        let (cr, cc) = view.dims();
2290                        if r < cr && c < cc {
2291                            view.get_cell(r, c)
2292                        } else {
2293                            LiteralValue::Empty
2294                        }
2295                    }
2296                    None => LiteralValue::Empty,
2297                };
2298                if !criteria_match(pred, &crit_val) {
2299                    all_match = false;
2300                    break;
2301                }
2302            }
2303
2304            if all_match {
2305                let target_val = target_view.get_cell(r, c);
2306                match target_val {
2307                    LiteralValue::Error(e) => {
2308                        return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
2309                    }
2310                    LiteralValue::Number(n) => {
2311                        result = Some(match result {
2312                            None => n,
2313                            Some(curr) => {
2314                                if is_max {
2315                                    curr.max(n)
2316                                } else {
2317                                    curr.min(n)
2318                                }
2319                            }
2320                        });
2321                    }
2322                    LiteralValue::Int(i) => {
2323                        let n = i as f64;
2324                        result = Some(match result {
2325                            None => n,
2326                            Some(curr) => {
2327                                if is_max {
2328                                    curr.max(n)
2329                                } else {
2330                                    curr.min(n)
2331                                }
2332                            }
2333                        });
2334                    }
2335                    _ => {} // Skip non-numeric
2336                }
2337            }
2338        }
2339    }
2340
2341    // Excel returns 0 if no matches found
2342    Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
2343        result.unwrap_or(0.0),
2344    )))
2345}
2346
2347/* ─────────────────────────── TRIMMEAN ──────────────────────────── */
2348
2349/// Returns the mean after trimming a percentage of values from both tails.
2350///
2351/// `TRIMMEAN` sorts numeric data, removes an equal count from low and high ends, then averages the
2352/// remaining interior values.
2353///
2354/// # Remarks
2355/// - `percent` must satisfy `0 <= percent < 1`.
2356/// - The trimmed count per side is `floor(n * percent / 2)`.
2357/// - Returns `#NUM!` for invalid percent values or when no numeric values are available.
2358///
2359/// # Examples
2360///
2361/// ```yaml,sandbox
2362/// title: "Trimmed mean from direct values"
2363/// formula: "=TRIMMEAN({1,2,3,4,5,6},0.3333333333333333)"
2364/// expected: 3.5
2365/// ```
2366///
2367/// ```yaml,sandbox
2368/// title: "Trimmed mean from a range"
2369/// grid:
2370///   A1: 10
2371///   A2: 12
2372///   A3: 13
2373///   A4: 20
2374///   A5: 21
2375///   A6: 30
2376/// formula: "=TRIMMEAN(A1:A6,0.4)"
2377/// expected: 16.5
2378/// ```
2379#[derive(Debug)]
2380pub struct TrimmeanFn;
2381/// [formualizer-docgen:schema:start]
2382/// Name: TRIMMEAN
2383/// Type: TrimmeanFn
2384/// Min args: 2
2385/// Max args: 1
2386/// Variadic: false
2387/// Signature: TRIMMEAN(arg1: number@range)
2388/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
2389/// Caps: PURE, REDUCTION, NUMERIC_ONLY
2390/// [formualizer-docgen:schema:end]
2391impl Function for TrimmeanFn {
2392    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
2393    fn name(&self) -> &'static str {
2394        "TRIMMEAN"
2395    }
2396    fn min_args(&self) -> usize {
2397        2
2398    }
2399    fn arg_schema(&self) -> &'static [ArgSchema] {
2400        &ARG_RANGE_NUM_LENIENT_ONE[..]
2401    }
2402    fn eval<'a, 'b, 'c>(
2403        &self,
2404        args: &'c [ArgumentHandle<'a, 'b>],
2405        _ctx: &dyn FunctionContext<'b>,
2406    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2407        let mut nums = collect_numeric_stats(&args[0..1])?;
2408        if nums.is_empty() {
2409            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2410                ExcelError::new_num(),
2411            )));
2412        }
2413
2414        let percent = match args[1].value()?.into_literal() {
2415            LiteralValue::Error(e) => {
2416                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
2417            }
2418            other => coerce_num(&other)?,
2419        };
2420
2421        // Percent must be between 0 and 1 (exclusive of 1)
2422        if !(0.0..1.0).contains(&percent) {
2423            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2424                ExcelError::new_num(),
2425            )));
2426        }
2427
2428        nums.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
2429
2430        let n = nums.len();
2431        // Number of values to exclude from each end
2432        let exclude = ((n as f64 * percent) / 2.0).floor() as usize;
2433
2434        if 2 * exclude >= n {
2435            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2436                ExcelError::new_num(),
2437            )));
2438        }
2439
2440        let trimmed = &nums[exclude..n - exclude];
2441        let sum: f64 = trimmed.iter().sum();
2442        let mean = sum / trimmed.len() as f64;
2443
2444        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(mean)))
2445    }
2446}
2447
2448/* ─────────────────────────── CORREL ──────────────────────────── */
2449
2450/// Helper to collect two paired arrays for regression/correlation functions
2451fn collect_paired_arrays(args: &[ArgumentHandle]) -> Result<(Vec<f64>, Vec<f64>), ExcelError> {
2452    let y_nums = collect_numeric_stats(&args[0..1])?;
2453    let x_nums = collect_numeric_stats(&args[1..2])?;
2454
2455    // Arrays must have same length
2456    if y_nums.len() != x_nums.len() {
2457        return Err(ExcelError::new_na());
2458    }
2459
2460    if y_nums.is_empty() {
2461        return Err(ExcelError::new_div());
2462    }
2463
2464    Ok((y_nums, x_nums))
2465}
2466
2467/// Returns the Pearson correlation coefficient between two numeric arrays.
2468///
2469/// `CORREL` measures linear relationship strength from `-1` (perfect inverse) to `1` (perfect
2470/// direct).
2471///
2472/// # Remarks
2473/// - Both arrays must produce the same number of numeric values.
2474/// - Returns `#N/A` when array lengths differ.
2475/// - Returns `#DIV/0!` when either series has zero variance.
2476///
2477/// # Examples
2478///
2479/// ```yaml,sandbox
2480/// title: "Perfect positive linear correlation"
2481/// formula: "=CORREL({2,4,6},{1,2,3})"
2482/// expected: 1
2483/// ```
2484///
2485/// ```yaml,sandbox
2486/// title: "Perfect negative linear correlation"
2487/// grid:
2488///   A1: 10
2489///   A2: 8
2490///   A3: 6
2491///   B1: 1
2492///   B2: 2
2493///   B3: 3
2494/// formula: "=CORREL(A1:A3,B1:B3)"
2495/// expected: -1
2496/// ```
2497#[derive(Debug)]
2498pub struct CorrelFn;
2499/// [formualizer-docgen:schema:start]
2500/// Name: CORREL
2501/// Type: CorrelFn
2502/// Min args: 2
2503/// Max args: 1
2504/// Variadic: false
2505/// Signature: CORREL(arg1: number@range)
2506/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
2507/// Caps: PURE, REDUCTION, NUMERIC_ONLY
2508/// [formualizer-docgen:schema:end]
2509impl Function for CorrelFn {
2510    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
2511    fn name(&self) -> &'static str {
2512        "CORREL"
2513    }
2514    fn min_args(&self) -> usize {
2515        2
2516    }
2517    fn arg_schema(&self) -> &'static [ArgSchema] {
2518        &ARG_RANGE_NUM_LENIENT_ONE[..]
2519    }
2520    fn eval<'a, 'b, 'c>(
2521        &self,
2522        args: &'c [ArgumentHandle<'a, 'b>],
2523        _ctx: &dyn FunctionContext<'b>,
2524    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2525        let (y, x) = match collect_paired_arrays(args) {
2526            Ok(v) => v,
2527            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
2528        };
2529
2530        let n = x.len() as f64;
2531        let mean_x = x.iter().sum::<f64>() / n;
2532        let mean_y = y.iter().sum::<f64>() / n;
2533
2534        let mut sum_xy = 0.0;
2535        let mut sum_x2 = 0.0;
2536        let mut sum_y2 = 0.0;
2537
2538        for i in 0..x.len() {
2539            let dx = x[i] - mean_x;
2540            let dy = y[i] - mean_y;
2541            sum_xy += dx * dy;
2542            sum_x2 += dx * dx;
2543            sum_y2 += dy * dy;
2544        }
2545
2546        let denom = (sum_x2 * sum_y2).sqrt();
2547        if denom == 0.0 {
2548            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2549                ExcelError::new_div(),
2550            )));
2551        }
2552
2553        let correl = sum_xy / denom;
2554        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
2555            correl,
2556        )))
2557    }
2558}
2559
2560/* ─────────────────────────── SLOPE ──────────────────────────── */
2561
2562/// Returns the slope of the linear regression line for paired data.
2563///
2564/// `SLOPE` fits `y = m*x + b` and returns `m`, the rate of change in `y` per unit of `x`.
2565///
2566/// # Remarks
2567/// - `known_y` and `known_x` must have the same numeric length.
2568/// - Returns `#N/A` for mismatched lengths.
2569/// - Returns `#DIV/0!` if all `x` values are identical.
2570///
2571/// # Examples
2572///
2573/// ```yaml,sandbox
2574/// title: "Positive slope from direct arrays"
2575/// formula: "=SLOPE({2,4,6},{1,2,3})"
2576/// expected: 2
2577/// ```
2578///
2579/// ```yaml,sandbox
2580/// title: "Negative slope from ranges"
2581/// grid:
2582///   A1: 10
2583///   A2: 8
2584///   A3: 6
2585///   B1: 1
2586///   B2: 2
2587///   B3: 3
2588/// formula: "=SLOPE(A1:A3,B1:B3)"
2589/// expected: -2
2590/// ```
2591#[derive(Debug)]
2592pub struct SlopeFn;
2593/// [formualizer-docgen:schema:start]
2594/// Name: SLOPE
2595/// Type: SlopeFn
2596/// Min args: 2
2597/// Max args: 1
2598/// Variadic: false
2599/// Signature: SLOPE(arg1: number@range)
2600/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
2601/// Caps: PURE, REDUCTION, NUMERIC_ONLY
2602/// [formualizer-docgen:schema:end]
2603impl Function for SlopeFn {
2604    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
2605    fn name(&self) -> &'static str {
2606        "SLOPE"
2607    }
2608    fn min_args(&self) -> usize {
2609        2
2610    }
2611    fn arg_schema(&self) -> &'static [ArgSchema] {
2612        &ARG_RANGE_NUM_LENIENT_ONE[..]
2613    }
2614    fn eval<'a, 'b, 'c>(
2615        &self,
2616        args: &'c [ArgumentHandle<'a, 'b>],
2617        _ctx: &dyn FunctionContext<'b>,
2618    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2619        let (y, x) = match collect_paired_arrays(args) {
2620            Ok(v) => v,
2621            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
2622        };
2623
2624        let n = x.len() as f64;
2625        let mean_x = x.iter().sum::<f64>() / n;
2626        let mean_y = y.iter().sum::<f64>() / n;
2627
2628        let mut sum_xy = 0.0;
2629        let mut sum_x2 = 0.0;
2630
2631        for i in 0..x.len() {
2632            let dx = x[i] - mean_x;
2633            let dy = y[i] - mean_y;
2634            sum_xy += dx * dy;
2635            sum_x2 += dx * dx;
2636        }
2637
2638        if sum_x2 == 0.0 {
2639            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2640                ExcelError::new_div(),
2641            )));
2642        }
2643
2644        let slope = sum_xy / sum_x2;
2645        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
2646            slope,
2647        )))
2648    }
2649}
2650
2651/* ─────────────────────────── INTERCEPT ──────────────────────────── */
2652
2653/// Returns the y-intercept of the linear regression line for paired data.
2654///
2655/// `INTERCEPT` fits `y = m*x + b` and returns `b`, the predicted `y` when `x = 0`.
2656///
2657/// # Remarks
2658/// - `known_y` and `known_x` must have the same numeric length.
2659/// - Returns `#N/A` for mismatched lengths.
2660/// - Returns `#DIV/0!` if all `x` values are identical.
2661///
2662/// # Examples
2663///
2664/// ```yaml,sandbox
2665/// title: "Positive intercept from direct arrays"
2666/// formula: "=INTERCEPT({3,5,7},{1,2,3})"
2667/// expected: 1
2668/// ```
2669///
2670/// ```yaml,sandbox
2671/// title: "Intercept from range-based linear trend"
2672/// grid:
2673///   A1: 10
2674///   A2: 8
2675///   A3: 6
2676///   B1: 1
2677///   B2: 2
2678///   B3: 3
2679/// formula: "=INTERCEPT(A1:A3,B1:B3)"
2680/// expected: 12
2681/// ```
2682#[derive(Debug)]
2683pub struct InterceptFn;
2684/// [formualizer-docgen:schema:start]
2685/// Name: INTERCEPT
2686/// Type: InterceptFn
2687/// Min args: 2
2688/// Max args: 1
2689/// Variadic: false
2690/// Signature: INTERCEPT(arg1: number@range)
2691/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
2692/// Caps: PURE, REDUCTION, NUMERIC_ONLY
2693/// [formualizer-docgen:schema:end]
2694impl Function for InterceptFn {
2695    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
2696    fn name(&self) -> &'static str {
2697        "INTERCEPT"
2698    }
2699    fn min_args(&self) -> usize {
2700        2
2701    }
2702    fn arg_schema(&self) -> &'static [ArgSchema] {
2703        &ARG_RANGE_NUM_LENIENT_ONE[..]
2704    }
2705    fn eval<'a, 'b, 'c>(
2706        &self,
2707        args: &'c [ArgumentHandle<'a, 'b>],
2708        _ctx: &dyn FunctionContext<'b>,
2709    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2710        let (y, x) = match collect_paired_arrays(args) {
2711            Ok(v) => v,
2712            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
2713        };
2714
2715        let n = x.len() as f64;
2716        let mean_x = x.iter().sum::<f64>() / n;
2717        let mean_y = y.iter().sum::<f64>() / n;
2718
2719        let mut sum_xy = 0.0;
2720        let mut sum_x2 = 0.0;
2721
2722        for i in 0..x.len() {
2723            let dx = x[i] - mean_x;
2724            let dy = y[i] - mean_y;
2725            sum_xy += dx * dy;
2726            sum_x2 += dx * dx;
2727        }
2728
2729        if sum_x2 == 0.0 {
2730            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2731                ExcelError::new_div(),
2732            )));
2733        }
2734
2735        let slope = sum_xy / sum_x2;
2736        let intercept = mean_y - slope * mean_x;
2737        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
2738            intercept,
2739        )))
2740    }
2741}
2742
2743/// [formualizer-docgen:schema:start]
2744/// Name: DEVSQ
2745/// Type: DevsqFn
2746/// Min args: 1
2747/// Max args: variadic
2748/// Variadic: true
2749/// Signature: DEVSQ(arg1...: number@range)
2750/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
2751/// Caps: PURE, REDUCTION, NUMERIC_ONLY
2752/// [formualizer-docgen:schema:end]
2753impl Function for DevsqFn {
2754    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
2755    fn name(&self) -> &'static str {
2756        "DEVSQ"
2757    }
2758    fn min_args(&self) -> usize {
2759        1
2760    }
2761    fn variadic(&self) -> bool {
2762        true
2763    }
2764    fn arg_schema(&self) -> &'static [ArgSchema] {
2765        &ARG_RANGE_NUM_LENIENT_ONE[..]
2766    }
2767    fn eval<'a, 'b, 'c>(
2768        &self,
2769        args: &'c [ArgumentHandle<'a, 'b>],
2770        _ctx: &dyn FunctionContext<'b>,
2771    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2772        let nums = collect_numeric_stats(args)?;
2773        if nums.is_empty() {
2774            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2775                ExcelError::new_num(),
2776            )));
2777        }
2778        let mean = nums.iter().sum::<f64>() / nums.len() as f64;
2779        let devsq = nums.iter().map(|x| (x - mean).powi(2)).sum::<f64>();
2780        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
2781            devsq,
2782        )))
2783    }
2784}
2785
2786/* ═══════════════════════════════════════════════════════════════════════════
2787STATISTICAL DISTRIBUTION FUNCTIONS
2788═══════════════════════════════════════════════════════════════════════════ */
2789
2790/// Helper: Standard normal CDF using error function approximation
2791fn std_norm_cdf(z: f64) -> f64 {
2792    // Use the complementary error function: Φ(z) = 0.5 * erfc(-z / sqrt(2))
2793    // Approximation using Abramowitz and Stegun formula 7.1.26
2794    let a1 = 0.254829592;
2795    let a2 = -0.284496736;
2796    let a3 = 1.421413741;
2797    let a4 = -1.453152027;
2798    let a5 = 1.061405429;
2799    let p = 0.3275911;
2800
2801    let sign = if z < 0.0 { -1.0 } else { 1.0 };
2802    let z_abs = z.abs() / std::f64::consts::SQRT_2;
2803
2804    let t = 1.0 / (1.0 + p * z_abs);
2805    let y = 1.0 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * (-z_abs * z_abs).exp();
2806
2807    0.5 * (1.0 + sign * y)
2808}
2809
2810/// Helper: Standard normal PDF
2811fn std_norm_pdf(z: f64) -> f64 {
2812    let inv_sqrt_2pi = 1.0 / (2.0 * std::f64::consts::PI).sqrt();
2813    inv_sqrt_2pi * (-0.5 * z * z).exp()
2814}
2815
2816/// Helper: Inverse standard normal CDF (probit function)
2817/// Uses Rational approximation from Abramowitz and Stegun
2818#[allow(clippy::excessive_precision)]
2819fn std_norm_inv(p: f64) -> Option<f64> {
2820    if p <= 0.0 || p >= 1.0 {
2821        return None;
2822    }
2823
2824    // Coefficients for rational approximation
2825    const A: [f64; 6] = [
2826        -3.969683028665376e+01,
2827        2.209460984245205e+02,
2828        -2.759285104469687e+02,
2829        1.383577518672690e+02,
2830        -3.066479806614716e+01,
2831        2.506628277459239e+00,
2832    ];
2833    const B: [f64; 5] = [
2834        -5.447609879822406e+01,
2835        1.615858368580409e+02,
2836        -1.556989798598866e+02,
2837        6.680131188771972e+01,
2838        -1.328068155288572e+01,
2839    ];
2840    const C: [f64; 6] = [
2841        -7.784894002430293e-03,
2842        -3.223964580411365e-01,
2843        -2.400758277161838e+00,
2844        -2.549732539343734e+00,
2845        4.374664141464968e+00,
2846        2.938163982698783e+00,
2847    ];
2848    const D: [f64; 4] = [
2849        7.784695709041462e-03,
2850        3.224671290700398e-01,
2851        2.445134137142996e+00,
2852        3.754408661907416e+00,
2853    ];
2854
2855    const P_LOW: f64 = 0.02425;
2856    const P_HIGH: f64 = 1.0 - P_LOW;
2857
2858    let q = p - 0.5;
2859
2860    if p < P_LOW {
2861        // Lower tail
2862        let r = (-2.0 * p.ln()).sqrt();
2863        let num = ((((C[0] * r + C[1]) * r + C[2]) * r + C[3]) * r + C[4]) * r + C[5];
2864        let den = (((D[0] * r + D[1]) * r + D[2]) * r + D[3]) * r + 1.0;
2865        Some(num / den)
2866    } else if p <= P_HIGH {
2867        // Central region
2868        let r = q * q;
2869        let num = ((((A[0] * r + A[1]) * r + A[2]) * r + A[3]) * r + A[4]) * r + A[5];
2870        let den = ((((B[0] * r + B[1]) * r + B[2]) * r + B[3]) * r + B[4]) * r + 1.0;
2871        Some(q * num / den)
2872    } else {
2873        // Upper tail
2874        let r = (-2.0 * (1.0 - p).ln()).sqrt();
2875        let num = ((((C[0] * r + C[1]) * r + C[2]) * r + C[3]) * r + C[4]) * r + C[5];
2876        let den = (((D[0] * r + D[1]) * r + D[2]) * r + D[3]) * r + 1.0;
2877        Some(-num / den)
2878    }
2879}
2880
2881/// Returns the standard normal probability for a z-score as either a CDF or PDF value.
2882///
2883/// Use `NORM.S.DIST` for z-based probability lookups when the distribution has mean `0` and
2884/// standard deviation `1`.
2885///
2886/// # Remarks
2887/// - Set `cumulative` to a non-zero value for the cumulative distribution `P(Z <= z)`.
2888/// - Set `cumulative` to `0` for the probability density at exactly `z`.
2889/// - Accepts any real-valued `z`; no domain clipping is applied.
2890/// - Invalid numeric coercions propagate as spreadsheet errors.
2891///
2892/// # Examples
2893///
2894/// ```yaml,sandbox
2895/// title: "Standard normal CDF at zero"
2896/// formula: "=NORM.S.DIST(0,TRUE)"
2897/// expected: 0.5
2898/// ```
2899///
2900/// ```yaml,sandbox
2901/// title: "Standard normal PDF at zero"
2902/// formula: "=NORM.S.DIST(0,FALSE)"
2903/// expected: 0.3989422804014327
2904/// ```
2905#[derive(Debug)]
2906pub struct NormSDistFn;
2907/// [formualizer-docgen:schema:start]
2908/// Name: NORM.S.DIST
2909/// Type: NormSDistFn
2910/// Min args: 2
2911/// Max args: 2
2912/// Variadic: false
2913/// Signature: NORM.S.DIST(arg1: number@scalar, arg2: number@scalar)
2914/// 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}
2915/// Caps: PURE
2916/// [formualizer-docgen:schema:end]
2917impl Function for NormSDistFn {
2918    func_caps!(PURE);
2919    fn name(&self) -> &'static str {
2920        "NORM.S.DIST"
2921    }
2922    fn min_args(&self) -> usize {
2923        2
2924    }
2925    fn arg_schema(&self) -> &'static [ArgSchema] {
2926        use std::sync::LazyLock;
2927        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
2928            vec![
2929                ArgSchema::number_lenient_scalar(),
2930                ArgSchema::number_lenient_scalar(),
2931            ]
2932        });
2933        &SCHEMA[..]
2934    }
2935    fn eval<'a, 'b, 'c>(
2936        &self,
2937        args: &'c [ArgumentHandle<'a, 'b>],
2938        _ctx: &dyn FunctionContext<'b>,
2939    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2940        let z = coerce_num(&scalar_like_value(&args[0])?)?;
2941        let cumulative = coerce_num(&scalar_like_value(&args[1])?)? != 0.0;
2942
2943        let result = if cumulative {
2944            std_norm_cdf(z)
2945        } else {
2946            std_norm_pdf(z)
2947        };
2948        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
2949            result,
2950        )))
2951    }
2952}
2953
2954/// Returns the z-score whose standard normal cumulative probability matches `probability`.
2955///
2956/// This is the inverse of `NORM.S.DIST(z, TRUE)` and is commonly used for critical-value
2957/// thresholds.
2958///
2959/// # Remarks
2960/// - `probability` must be strictly between `0` and `1`.
2961/// - Returns `#NUM!` when `probability <= 0` or `probability >= 1`.
2962/// - Output can be negative, zero, or positive depending on which side of `0.5` you query.
2963/// - Invalid numeric coercions propagate as spreadsheet errors.
2964///
2965/// # Examples
2966///
2967/// ```yaml,sandbox
2968/// title: "Median probability maps to zero"
2969/// formula: "=NORM.S.INV(0.5)"
2970/// expected: 0
2971/// ```
2972///
2973/// ```yaml,sandbox
2974/// title: "Upper-tail critical z-score"
2975/// formula: "=NORM.S.INV(0.975)"
2976/// expected: 1.959963986120195
2977/// ```
2978#[derive(Debug)]
2979pub struct NormSInvFn;
2980/// [formualizer-docgen:schema:start]
2981/// Name: NORM.S.INV
2982/// Type: NormSInvFn
2983/// Min args: 1
2984/// Max args: 1
2985/// Variadic: false
2986/// Signature: NORM.S.INV(arg1: number@scalar)
2987/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
2988/// Caps: PURE
2989/// [formualizer-docgen:schema:end]
2990impl Function for NormSInvFn {
2991    func_caps!(PURE);
2992    fn name(&self) -> &'static str {
2993        "NORM.S.INV"
2994    }
2995    fn min_args(&self) -> usize {
2996        1
2997    }
2998    fn arg_schema(&self) -> &'static [ArgSchema] {
2999        use std::sync::LazyLock;
3000        static SCHEMA: LazyLock<Vec<ArgSchema>> =
3001            LazyLock::new(|| vec![ArgSchema::number_lenient_scalar()]);
3002        &SCHEMA[..]
3003    }
3004    fn eval<'a, 'b, 'c>(
3005        &self,
3006        args: &'c [ArgumentHandle<'a, 'b>],
3007        _ctx: &dyn FunctionContext<'b>,
3008    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3009        let p = coerce_num(&scalar_like_value(&args[0])?)?;
3010
3011        match std_norm_inv(p) {
3012            Some(z) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(z))),
3013            None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3014                ExcelError::new_num(),
3015            ))),
3016        }
3017    }
3018}
3019
3020/// Returns the normal-distribution probability at `x` for a given mean and standard deviation.
3021///
3022/// Use `NORM.DIST` for either cumulative probabilities or point density under a non-standard
3023/// normal model.
3024///
3025/// # Remarks
3026/// - Set `cumulative` to non-zero for `P(X <= x)`; set it to `0` for density mode.
3027/// - `standard_dev` must be strictly greater than `0`.
3028/// - Returns `#NUM!` when `standard_dev <= 0`.
3029/// - Invalid numeric coercions propagate as spreadsheet errors.
3030///
3031/// # Examples
3032///
3033/// ```yaml,sandbox
3034/// title: "Normal CDF at the mean"
3035/// formula: "=NORM.DIST(50,50,10,TRUE)"
3036/// expected: 0.5
3037/// ```
3038///
3039/// ```yaml,sandbox
3040/// title: "Normal PDF at the mean"
3041/// formula: "=NORM.DIST(50,50,10,FALSE)"
3042/// expected: 0.03989422804014327
3043/// ```
3044#[derive(Debug)]
3045pub struct NormDistFn;
3046/// [formualizer-docgen:schema:start]
3047/// Name: NORM.DIST
3048/// Type: NormDistFn
3049/// Min args: 4
3050/// Max args: 4
3051/// Variadic: false
3052/// Signature: NORM.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar)
3053/// 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}
3054/// Caps: PURE
3055/// [formualizer-docgen:schema:end]
3056impl Function for NormDistFn {
3057    func_caps!(PURE);
3058    fn name(&self) -> &'static str {
3059        "NORM.DIST"
3060    }
3061    fn min_args(&self) -> usize {
3062        4
3063    }
3064    fn arg_schema(&self) -> &'static [ArgSchema] {
3065        use std::sync::LazyLock;
3066        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
3067            vec![
3068                ArgSchema::number_lenient_scalar(),
3069                ArgSchema::number_lenient_scalar(),
3070                ArgSchema::number_lenient_scalar(),
3071                ArgSchema::number_lenient_scalar(),
3072            ]
3073        });
3074        &SCHEMA[..]
3075    }
3076    fn eval<'a, 'b, 'c>(
3077        &self,
3078        args: &'c [ArgumentHandle<'a, 'b>],
3079        _ctx: &dyn FunctionContext<'b>,
3080    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3081        let x = coerce_num(&scalar_like_value(&args[0])?)?;
3082        let mean = coerce_num(&scalar_like_value(&args[1])?)?;
3083        let std_dev = coerce_num(&scalar_like_value(&args[2])?)?;
3084        let cumulative = coerce_num(&scalar_like_value(&args[3])?)? != 0.0;
3085
3086        if std_dev <= 0.0 {
3087            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3088                ExcelError::new_num(),
3089            )));
3090        }
3091
3092        let z = (x - mean) / std_dev;
3093
3094        let result = if cumulative {
3095            std_norm_cdf(z)
3096        } else {
3097            std_norm_pdf(z) / std_dev
3098        };
3099        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
3100            result,
3101        )))
3102    }
3103}
3104
3105/// Returns the value `x` whose normal cumulative probability equals `probability`.
3106///
3107/// This function is the inverse of `NORM.DIST(x, mean, standard_dev, TRUE)`.
3108///
3109/// # Remarks
3110/// - `probability` must be strictly between `0` and `1`.
3111/// - `standard_dev` must be strictly greater than `0`.
3112/// - Returns `#NUM!` for invalid probability bounds or non-positive standard deviation.
3113/// - Invalid numeric coercions propagate as spreadsheet errors.
3114///
3115/// # Examples
3116///
3117/// ```yaml,sandbox
3118/// title: "Median probability returns the mean"
3119/// formula: "=NORM.INV(0.5,10,2)"
3120/// expected: 10
3121/// ```
3122///
3123/// ```yaml,sandbox
3124/// title: "One-standard-deviation quantile"
3125/// formula: "=NORM.INV(0.841344746068543,0,1)"
3126/// expected: 1
3127/// ```
3128#[derive(Debug)]
3129pub struct NormInvFn;
3130/// [formualizer-docgen:schema:start]
3131/// Name: NORM.INV
3132/// Type: NormInvFn
3133/// Min args: 3
3134/// Max args: 3
3135/// Variadic: false
3136/// Signature: NORM.INV(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
3137/// 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}
3138/// Caps: PURE
3139/// [formualizer-docgen:schema:end]
3140impl Function for NormInvFn {
3141    func_caps!(PURE);
3142    fn name(&self) -> &'static str {
3143        "NORM.INV"
3144    }
3145    fn min_args(&self) -> usize {
3146        3
3147    }
3148    fn arg_schema(&self) -> &'static [ArgSchema] {
3149        use std::sync::LazyLock;
3150        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
3151            vec![
3152                ArgSchema::number_lenient_scalar(),
3153                ArgSchema::number_lenient_scalar(),
3154                ArgSchema::number_lenient_scalar(),
3155            ]
3156        });
3157        &SCHEMA[..]
3158    }
3159    fn eval<'a, 'b, 'c>(
3160        &self,
3161        args: &'c [ArgumentHandle<'a, 'b>],
3162        _ctx: &dyn FunctionContext<'b>,
3163    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3164        let p = coerce_num(&scalar_like_value(&args[0])?)?;
3165        let mean = coerce_num(&scalar_like_value(&args[1])?)?;
3166        let std_dev = coerce_num(&scalar_like_value(&args[2])?)?;
3167
3168        if std_dev <= 0.0 {
3169            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3170                ExcelError::new_num(),
3171            )));
3172        }
3173
3174        match std_norm_inv(p) {
3175            Some(z) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
3176                mean + z * std_dev,
3177            ))),
3178            None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3179                ExcelError::new_num(),
3180            ))),
3181        }
3182    }
3183}
3184
3185/// Returns the log-normal probability at `x` as either a cumulative value or density.
3186///
3187/// `LOGNORM.DIST` models positive-valued variables where `ln(X)` follows a normal distribution.
3188///
3189/// # Remarks
3190/// - Set `cumulative` to non-zero for CDF mode; set it to `0` for PDF mode.
3191/// - Requires `x > 0` and `standard_dev > 0`.
3192/// - Returns `#NUM!` when `x <= 0` or `standard_dev <= 0`.
3193/// - Invalid numeric coercions propagate as spreadsheet errors.
3194///
3195/// # Examples
3196///
3197/// ```yaml,sandbox
3198/// title: "Log-normal CDF at x = 1"
3199/// formula: "=LOGNORM.DIST(1,0,1,TRUE)"
3200/// expected: 0.5
3201/// ```
3202///
3203/// ```yaml,sandbox
3204/// title: "Log-normal PDF at x = 1"
3205/// formula: "=LOGNORM.DIST(1,0,1,FALSE)"
3206/// expected: 0.3989422804014327
3207/// ```
3208#[derive(Debug)]
3209pub struct LognormDistFn;
3210/// [formualizer-docgen:schema:start]
3211/// Name: LOGNORM.DIST
3212/// Type: LognormDistFn
3213/// Min args: 4
3214/// Max args: 4
3215/// Variadic: false
3216/// Signature: LOGNORM.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar)
3217/// 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}
3218/// Caps: PURE
3219/// [formualizer-docgen:schema:end]
3220impl Function for LognormDistFn {
3221    func_caps!(PURE);
3222    fn name(&self) -> &'static str {
3223        "LOGNORM.DIST"
3224    }
3225    fn min_args(&self) -> usize {
3226        4
3227    }
3228    fn arg_schema(&self) -> &'static [ArgSchema] {
3229        use std::sync::LazyLock;
3230        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
3231            vec![
3232                ArgSchema::number_lenient_scalar(),
3233                ArgSchema::number_lenient_scalar(),
3234                ArgSchema::number_lenient_scalar(),
3235                ArgSchema::number_lenient_scalar(),
3236            ]
3237        });
3238        &SCHEMA[..]
3239    }
3240    fn eval<'a, 'b, 'c>(
3241        &self,
3242        args: &'c [ArgumentHandle<'a, 'b>],
3243        _ctx: &dyn FunctionContext<'b>,
3244    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3245        let x = coerce_num(&scalar_like_value(&args[0])?)?;
3246        let mean = coerce_num(&scalar_like_value(&args[1])?)?;
3247        let std_dev = coerce_num(&scalar_like_value(&args[2])?)?;
3248        let cumulative = coerce_num(&scalar_like_value(&args[3])?)? != 0.0;
3249
3250        if x <= 0.0 || std_dev <= 0.0 {
3251            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3252                ExcelError::new_num(),
3253            )));
3254        }
3255
3256        let z = (x.ln() - mean) / std_dev;
3257
3258        let result = if cumulative {
3259            std_norm_cdf(z)
3260        } else {
3261            std_norm_pdf(z) / (x * std_dev)
3262        };
3263        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
3264            result,
3265        )))
3266    }
3267}
3268
3269/// Returns the positive value `x` whose log-normal cumulative probability is `probability`.
3270///
3271/// This function inverts `LOGNORM.DIST(x, mean, standard_dev, TRUE)`.
3272///
3273/// # Remarks
3274/// - `probability` must be strictly between `0` and `1`.
3275/// - `standard_dev` must be strictly greater than `0`.
3276/// - Returns `#NUM!` when inputs violate probability or scale constraints.
3277/// - Invalid numeric coercions propagate as spreadsheet errors.
3278///
3279/// # Examples
3280///
3281/// ```yaml,sandbox
3282/// title: "Median log-normal quantile"
3283/// formula: "=LOGNORM.INV(0.5,0,1)"
3284/// expected: 1
3285/// ```
3286///
3287/// ```yaml,sandbox
3288/// title: "Upper quantile for mean 0 and stdev 1"
3289/// formula: "=LOGNORM.INV(0.841344746068543,0,1)"
3290/// expected: 2.718281828459045
3291/// ```
3292#[derive(Debug)]
3293pub struct LognormInvFn;
3294/// [formualizer-docgen:schema:start]
3295/// Name: LOGNORM.INV
3296/// Type: LognormInvFn
3297/// Min args: 3
3298/// Max args: 3
3299/// Variadic: false
3300/// Signature: LOGNORM.INV(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
3301/// 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}
3302/// Caps: PURE
3303/// [formualizer-docgen:schema:end]
3304impl Function for LognormInvFn {
3305    func_caps!(PURE);
3306    fn name(&self) -> &'static str {
3307        "LOGNORM.INV"
3308    }
3309    fn min_args(&self) -> usize {
3310        3
3311    }
3312    fn arg_schema(&self) -> &'static [ArgSchema] {
3313        use std::sync::LazyLock;
3314        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
3315            vec![
3316                ArgSchema::number_lenient_scalar(),
3317                ArgSchema::number_lenient_scalar(),
3318                ArgSchema::number_lenient_scalar(),
3319            ]
3320        });
3321        &SCHEMA[..]
3322    }
3323    fn eval<'a, 'b, 'c>(
3324        &self,
3325        args: &'c [ArgumentHandle<'a, 'b>],
3326        _ctx: &dyn FunctionContext<'b>,
3327    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3328        let p = coerce_num(&scalar_like_value(&args[0])?)?;
3329        let mean = coerce_num(&scalar_like_value(&args[1])?)?;
3330        let std_dev = coerce_num(&scalar_like_value(&args[2])?)?;
3331
3332        if std_dev <= 0.0 {
3333            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3334                ExcelError::new_num(),
3335            )));
3336        }
3337
3338        match std_norm_inv(p) {
3339            Some(z) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
3340                (mean + z * std_dev).exp(),
3341            ))),
3342            None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3343                ExcelError::new_num(),
3344            ))),
3345        }
3346    }
3347}
3348
3349/// Returns the standard normal probability density at `x`.
3350///
3351/// `PHI` is equivalent to `NORM.S.DIST(x, FALSE)` and is useful in continuous-probability
3352/// calculations.
3353///
3354/// # Remarks
3355/// - Evaluates the density of a standard normal variable centered at `0`.
3356/// - The result is always non-negative and symmetric around `x = 0`.
3357/// - Works for any real input value.
3358/// - Invalid numeric coercions propagate as spreadsheet errors.
3359///
3360/// # Examples
3361///
3362/// ```yaml,sandbox
3363/// title: "Standard normal density at zero"
3364/// formula: "=PHI(0)"
3365/// expected: 0.3989422804014327
3366/// ```
3367///
3368/// ```yaml,sandbox
3369/// title: "Standard normal density at one"
3370/// formula: "=PHI(1)"
3371/// expected: 0.24197072451914337
3372/// ```
3373#[derive(Debug)]
3374pub struct PhiFn;
3375/// [formualizer-docgen:schema:start]
3376/// Name: PHI
3377/// Type: PhiFn
3378/// Min args: 1
3379/// Max args: 1
3380/// Variadic: false
3381/// Signature: PHI(arg1: number@scalar)
3382/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
3383/// Caps: PURE
3384/// [formualizer-docgen:schema:end]
3385impl Function for PhiFn {
3386    func_caps!(PURE);
3387    fn name(&self) -> &'static str {
3388        "PHI"
3389    }
3390    fn min_args(&self) -> usize {
3391        1
3392    }
3393    fn arg_schema(&self) -> &'static [ArgSchema] {
3394        use std::sync::LazyLock;
3395        static SCHEMA: LazyLock<Vec<ArgSchema>> =
3396            LazyLock::new(|| vec![ArgSchema::number_lenient_scalar()]);
3397        &SCHEMA[..]
3398    }
3399    fn eval<'a, 'b, 'c>(
3400        &self,
3401        args: &'c [ArgumentHandle<'a, 'b>],
3402        _ctx: &dyn FunctionContext<'b>,
3403    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3404        let z = coerce_num(&scalar_like_value(&args[0])?)?;
3405        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
3406            std_norm_pdf(z),
3407        )))
3408    }
3409}
3410
3411/// Returns the standard normal area between `0` and `z`.
3412///
3413/// `GAUSS` computes `NORM.S.DIST(z, TRUE) - 0.5`, preserving the sign of `z`.
3414///
3415/// # Remarks
3416/// - Positive `z` returns a positive area; negative `z` returns a negative area.
3417/// - `GAUSS(0)` returns `0`.
3418/// - Output magnitude is always less than `0.5`.
3419/// - Invalid numeric coercions propagate as spreadsheet errors.
3420///
3421/// # Examples
3422///
3423/// ```yaml,sandbox
3424/// title: "Area from mean to z = 1"
3425/// formula: "=GAUSS(1)"
3426/// expected: 0.3413447460685429
3427/// ```
3428///
3429/// ```yaml,sandbox
3430/// title: "Symmetric negative z-value"
3431/// formula: "=GAUSS(-1)"
3432/// expected: -0.3413447460685429
3433/// ```
3434#[derive(Debug)]
3435pub struct GaussFn;
3436/// [formualizer-docgen:schema:start]
3437/// Name: GAUSS
3438/// Type: GaussFn
3439/// Min args: 1
3440/// Max args: 1
3441/// Variadic: false
3442/// Signature: GAUSS(arg1: number@scalar)
3443/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
3444/// Caps: PURE
3445/// [formualizer-docgen:schema:end]
3446impl Function for GaussFn {
3447    func_caps!(PURE);
3448    fn name(&self) -> &'static str {
3449        "GAUSS"
3450    }
3451    fn min_args(&self) -> usize {
3452        1
3453    }
3454    fn arg_schema(&self) -> &'static [ArgSchema] {
3455        use std::sync::LazyLock;
3456        static SCHEMA: LazyLock<Vec<ArgSchema>> =
3457            LazyLock::new(|| vec![ArgSchema::number_lenient_scalar()]);
3458        &SCHEMA[..]
3459    }
3460    fn eval<'a, 'b, 'c>(
3461        &self,
3462        args: &'c [ArgumentHandle<'a, 'b>],
3463        _ctx: &dyn FunctionContext<'b>,
3464    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3465        let z = coerce_num(&scalar_like_value(&args[0])?)?;
3466        // GAUSS(z) = Φ(z) - 0.5
3467        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
3468            std_norm_cdf(z) - 0.5,
3469        )))
3470    }
3471}
3472
3473/// Helper: Log-gamma function
3474#[allow(clippy::excessive_precision)]
3475fn ln_gamma(x: f64) -> f64 {
3476    // Lanczos approximation
3477    const G: f64 = 7.0;
3478    const C: [f64; 9] = [
3479        0.99999999999980993,
3480        676.5203681218851,
3481        -1259.1392167224028,
3482        771.32342877765313,
3483        -176.61502916214059,
3484        12.507343278686905,
3485        -0.13857109526572012,
3486        9.9843695780195716e-6,
3487        1.5056327351493116e-7,
3488    ];
3489
3490    if x < 0.5 {
3491        // Reflection formula
3492        let pi = std::f64::consts::PI;
3493        pi.ln() - (pi * x).sin().ln() - ln_gamma(1.0 - x)
3494    } else {
3495        let x = x - 1.0;
3496        let mut ag = C[0];
3497        for (i, c) in C.iter().enumerate().skip(1) {
3498            ag += c / (x + i as f64);
3499        }
3500        let tmp = x + G + 0.5;
3501        0.5 * (2.0 * std::f64::consts::PI).ln() + (tmp).ln() * (x + 0.5) - tmp + ag.ln()
3502    }
3503}
3504
3505/// Helper: Regularized lower incomplete gamma function P(a, x)
3506fn gamma_p(a: f64, x: f64) -> f64 {
3507    if x < 0.0 || a <= 0.0 {
3508        return 0.0;
3509    }
3510    if x == 0.0 {
3511        return 0.0;
3512    }
3513
3514    // Use series expansion for x < a+1
3515    if x < a + 1.0 {
3516        gamma_series(a, x)
3517    } else {
3518        // Use continued fraction for x >= a+1
3519        1.0 - gamma_cf(a, x)
3520    }
3521}
3522
3523/// Helper: Series expansion for incomplete gamma
3524fn gamma_series(a: f64, x: f64) -> f64 {
3525    let ln_ga = ln_gamma(a);
3526    let mut sum = 1.0 / a;
3527    let mut term = sum;
3528    for n in 1..200 {
3529        term *= x / (a + n as f64);
3530        sum += term;
3531        if term.abs() < sum.abs() * 1e-15 {
3532            break;
3533        }
3534    }
3535    sum * (-x + a * x.ln() - ln_ga).exp()
3536}
3537
3538/// Helper: Continued fraction for upper incomplete gamma Q(a,x)
3539/// Using modified Lentz's algorithm (Numerical Recipes formulation)
3540fn gamma_cf(a: f64, x: f64) -> f64 {
3541    let ln_ga = ln_gamma(a);
3542    const TINY: f64 = 1e-30;
3543    const EPS: f64 = 1e-14;
3544
3545    // Set up for evaluating continued fraction by modified Lentz's method
3546    let mut b = x + 1.0 - a;
3547    let mut c = 1.0 / TINY;
3548    let mut d = 1.0 / b;
3549    let mut h = d;
3550
3551    for i in 1..=200 {
3552        let an = -(i as f64) * (i as f64 - a);
3553        b += 2.0;
3554        d = an * d + b;
3555        if d.abs() < TINY {
3556            d = TINY;
3557        }
3558        c = b + an / c;
3559        if c.abs() < TINY {
3560            c = TINY;
3561        }
3562        d = 1.0 / d;
3563        let delta = d * c;
3564        h *= delta;
3565        if (delta - 1.0).abs() <= EPS {
3566            break;
3567        }
3568    }
3569
3570    h * (-x + a * x.ln() - ln_ga).exp()
3571}
3572
3573/// Helper: Regularized incomplete beta function I_x(a,b)
3574/// Uses the continued fraction representation (NIST DLMF 8.17.22)
3575fn beta_i(x: f64, a: f64, b: f64) -> f64 {
3576    if x <= 0.0 {
3577        return 0.0;
3578    }
3579    if x >= 1.0 {
3580        return 1.0;
3581    }
3582    if a <= 0.0 || b <= 0.0 {
3583        return f64::NAN;
3584    }
3585
3586    // Use symmetry for better convergence: I_x(a,b) = 1 - I_{1-x}(b,a)
3587    if x > (a + 1.0) / (a + b + 2.0) {
3588        return 1.0 - beta_i(1.0 - x, b, a);
3589    }
3590
3591    // Compute the prefactor: x^a * (1-x)^b / (a * B(a,b))
3592    let ln_beta = ln_gamma(a) + ln_gamma(b) - ln_gamma(a + b);
3593    let ln_prefactor = a * x.ln() + b * (1.0 - x).ln() - ln_beta - a.ln();
3594    let prefactor = ln_prefactor.exp();
3595
3596    // Evaluate the continued fraction using modified Lentz algorithm
3597    // The CF is: 1 / (1 + d1/(1 + d2/(1 + ...)))
3598    // where d_{2m+1} = -(a+m)(a+b+m)x / ((a+2m)(a+2m+1))
3599    //       d_{2m}   = m(b-m)x / ((a+2m-1)(a+2m))
3600    const EPS: f64 = 1e-14;
3601    const TINY: f64 = 1e-30;
3602
3603    let qab = a + b;
3604    let qap = a + 1.0;
3605    let qam = a - 1.0;
3606    let mut c = 1.0;
3607    let mut d = 1.0 - qab * x / qap;
3608    if d.abs() < TINY {
3609        d = TINY;
3610    }
3611    d = 1.0 / d;
3612    let mut h = d;
3613
3614    for m in 1..=200 {
3615        let m_f64 = m as f64;
3616        let m2 = 2.0 * m_f64;
3617
3618        // Even step: d_{2m} = m(b-m)x / ((a+2m-1)(a+2m))
3619        let aa = m_f64 * (b - m_f64) * x / ((qam + m2) * (a + m2));
3620        d = 1.0 + aa * d;
3621        if d.abs() < TINY {
3622            d = TINY;
3623        }
3624        c = 1.0 + aa / c;
3625        if c.abs() < TINY {
3626            c = TINY;
3627        }
3628        d = 1.0 / d;
3629        h *= d * c;
3630
3631        // Odd step: d_{2m+1} = -(a+m)(a+b+m)x / ((a+2m)(a+2m+1))
3632        let aa = -((a + m_f64) * (qab + m_f64) * x) / ((a + m2) * (qap + m2));
3633        d = 1.0 + aa * d;
3634        if d.abs() < TINY {
3635            d = TINY;
3636        }
3637        c = 1.0 + aa / c;
3638        if c.abs() < TINY {
3639            c = TINY;
3640        }
3641        d = 1.0 / d;
3642        let delta = d * c;
3643        h *= delta;
3644
3645        if (delta - 1.0).abs() <= EPS {
3646            break;
3647        }
3648    }
3649
3650    prefactor * h
3651}
3652
3653/// Helper: T distribution CDF
3654fn t_cdf(t: f64, df: f64) -> f64 {
3655    let x = df / (df + t * t);
3656    0.5 * (1.0 + t.signum() * (1.0 - beta_i(x, df / 2.0, 0.5)))
3657}
3658
3659/// Helper: T distribution inverse CDF using Newton-Raphson
3660fn t_inv(p: f64, df: f64) -> Option<f64> {
3661    if p <= 0.0 || p >= 1.0 {
3662        return None;
3663    }
3664
3665    // Initial guess using normal approximation
3666    let mut t = std_norm_inv(p)?;
3667
3668    // Newton-Raphson iteration
3669    for _ in 0..50 {
3670        let cdf = t_cdf(t, df);
3671        let pdf = t_pdf(t, df);
3672        if pdf.abs() < 1e-30 {
3673            break;
3674        }
3675        let delta = (cdf - p) / pdf;
3676        t -= delta;
3677        if delta.abs() < 1e-12 {
3678            break;
3679        }
3680    }
3681
3682    Some(t)
3683}
3684
3685/// Helper: T distribution PDF
3686fn t_pdf(t: f64, df: f64) -> f64 {
3687    let coef =
3688        (ln_gamma((df + 1.0) / 2.0) - ln_gamma(df / 2.0) - 0.5 * (df * std::f64::consts::PI).ln())
3689            .exp();
3690    coef * (1.0 + t * t / df).powf(-(df + 1.0) / 2.0)
3691}
3692
3693/// Helper: Chi-square CDF
3694fn chisq_cdf(x: f64, df: f64) -> f64 {
3695    if x <= 0.0 {
3696        return 0.0;
3697    }
3698    gamma_p(df / 2.0, x / 2.0)
3699}
3700
3701/// Helper: Chi-square inverse CDF using Newton-Raphson
3702fn chisq_inv(p: f64, df: f64) -> Option<f64> {
3703    if p <= 0.0 || p >= 1.0 {
3704        return None;
3705    }
3706
3707    // Initial guess
3708    let mut x = df.max(1.0);
3709    if p < 0.5 {
3710        x = x.min(1.0);
3711    }
3712
3713    // Newton-Raphson iteration
3714    for _ in 0..100 {
3715        let cdf = chisq_cdf(x, df);
3716        let pdf = chisq_pdf(x, df);
3717        if pdf.abs() < 1e-30 {
3718            break;
3719        }
3720        let delta = (cdf - p) / pdf;
3721        let new_x = (x - delta).max(1e-15);
3722        if (new_x - x).abs() < 1e-12 * x {
3723            x = new_x;
3724            break;
3725        }
3726        x = new_x;
3727    }
3728
3729    Some(x)
3730}
3731
3732/// Helper: Chi-square PDF
3733fn chisq_pdf(x: f64, df: f64) -> f64 {
3734    if x <= 0.0 {
3735        return 0.0;
3736    }
3737    let k = df / 2.0;
3738    ((k - 1.0) * x.ln() - x / 2.0 - k * 2.0_f64.ln() - ln_gamma(k)).exp()
3739}
3740
3741/// Helper: F distribution CDF
3742fn f_cdf(f: f64, d1: f64, d2: f64) -> f64 {
3743    if f <= 0.0 {
3744        return 0.0;
3745    }
3746    let x = d1 * f / (d1 * f + d2);
3747    beta_i(x, d1 / 2.0, d2 / 2.0)
3748}
3749
3750/// Helper: F distribution inverse CDF using Newton-Raphson
3751fn f_inv(p: f64, d1: f64, d2: f64) -> Option<f64> {
3752    if p <= 0.0 || p >= 1.0 {
3753        return None;
3754    }
3755
3756    // Initial guess
3757    let mut f = 1.0;
3758
3759    // Newton-Raphson iteration
3760    for _ in 0..100 {
3761        let cdf = f_cdf(f, d1, d2);
3762        let pdf = f_pdf(f, d1, d2);
3763        if pdf.abs() < 1e-30 {
3764            break;
3765        }
3766        let delta = (cdf - p) / pdf;
3767        let new_f = (f - delta).max(1e-15);
3768        if (new_f - f).abs() < 1e-12 * f {
3769            f = new_f;
3770            break;
3771        }
3772        f = new_f;
3773    }
3774
3775    Some(f)
3776}
3777
3778/// Helper: F distribution PDF
3779fn f_pdf(f: f64, d1: f64, d2: f64) -> f64 {
3780    if f <= 0.0 {
3781        return 0.0;
3782    }
3783    let ln_beta = ln_gamma(d1 / 2.0) + ln_gamma(d2 / 2.0) - ln_gamma((d1 + d2) / 2.0);
3784    let coef = (d1 / 2.0) * (d1 / d2).ln() + (d1 / 2.0 - 1.0) * f.ln()
3785        - ((d1 + d2) / 2.0) * (1.0 + d1 * f / d2).ln()
3786        - ln_beta;
3787    coef.exp()
3788}
3789
3790/// Returns the Student's t probability for `x` and a given degrees-of-freedom value.
3791///
3792/// Use `T.DIST` in either cumulative mode (left-tail probability) or density mode.
3793///
3794/// # Remarks
3795/// - Set `cumulative` to non-zero for CDF mode, or `0` for PDF mode.
3796/// - `deg_freedom` must be at least `1`.
3797/// - Returns `#NUM!` when `deg_freedom < 1`.
3798/// - Invalid numeric coercions propagate as spreadsheet errors.
3799///
3800/// # Examples
3801///
3802/// ```yaml,sandbox
3803/// title: "t CDF at zero"
3804/// formula: "=T.DIST(0,10,TRUE)"
3805/// expected: 0.5
3806/// ```
3807///
3808/// ```yaml,sandbox
3809/// title: "t PDF at zero"
3810/// formula: "=T.DIST(0,10,FALSE)"
3811/// expected: 0.389108383966031
3812/// ```
3813#[derive(Debug)]
3814pub struct TDistFn;
3815/// [formualizer-docgen:schema:start]
3816/// Name: T.DIST
3817/// Type: TDistFn
3818/// Min args: 3
3819/// Max args: 3
3820/// Variadic: false
3821/// Signature: T.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
3822/// 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}
3823/// Caps: PURE
3824/// [formualizer-docgen:schema:end]
3825impl Function for TDistFn {
3826    func_caps!(PURE);
3827    fn name(&self) -> &'static str {
3828        "T.DIST"
3829    }
3830    fn min_args(&self) -> usize {
3831        3
3832    }
3833    fn arg_schema(&self) -> &'static [ArgSchema] {
3834        use std::sync::LazyLock;
3835        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
3836            vec![
3837                ArgSchema::number_lenient_scalar(),
3838                ArgSchema::number_lenient_scalar(),
3839                ArgSchema::number_lenient_scalar(),
3840            ]
3841        });
3842        &SCHEMA[..]
3843    }
3844    fn eval<'a, 'b, 'c>(
3845        &self,
3846        args: &'c [ArgumentHandle<'a, 'b>],
3847        _ctx: &dyn FunctionContext<'b>,
3848    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3849        let x = coerce_num(&scalar_like_value(&args[0])?)?;
3850        let df = coerce_num(&scalar_like_value(&args[1])?)?;
3851        let cumulative = coerce_num(&scalar_like_value(&args[2])?)? != 0.0;
3852
3853        if df < 1.0 {
3854            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3855                ExcelError::new_num(),
3856            )));
3857        }
3858
3859        let result = if cumulative {
3860            t_cdf(x, df)
3861        } else {
3862            t_pdf(x, df)
3863        };
3864        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
3865            result,
3866        )))
3867    }
3868}
3869
3870/// Returns the t-value whose left-tail probability equals `probability`.
3871///
3872/// `T.INV` is the inverse of `T.DIST(x, deg_freedom, TRUE)`.
3873///
3874/// # Remarks
3875/// - `probability` must be strictly between `0` and `1`.
3876/// - `deg_freedom` must be at least `1`.
3877/// - Returns `#NUM!` for out-of-range probability or invalid degrees of freedom.
3878/// - Invalid numeric coercions propagate as spreadsheet errors.
3879///
3880/// # Examples
3881///
3882/// ```yaml,sandbox
3883/// title: "Median t quantile"
3884/// formula: "=T.INV(0.5,10)"
3885/// expected: 0
3886/// ```
3887///
3888/// ```yaml,sandbox
3889/// title: "Upper-tail critical value"
3890/// formula: "=T.INV(0.975,10)"
3891/// expected: 2.228138851986273
3892/// ```
3893#[derive(Debug)]
3894pub struct TInvFn;
3895/// [formualizer-docgen:schema:start]
3896/// Name: T.INV
3897/// Type: TInvFn
3898/// Min args: 2
3899/// Max args: 2
3900/// Variadic: false
3901/// Signature: T.INV(arg1: number@scalar, arg2: number@scalar)
3902/// 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}
3903/// Caps: PURE
3904/// [formualizer-docgen:schema:end]
3905impl Function for TInvFn {
3906    func_caps!(PURE);
3907    fn name(&self) -> &'static str {
3908        "T.INV"
3909    }
3910    fn min_args(&self) -> usize {
3911        2
3912    }
3913    fn arg_schema(&self) -> &'static [ArgSchema] {
3914        use std::sync::LazyLock;
3915        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
3916            vec![
3917                ArgSchema::number_lenient_scalar(),
3918                ArgSchema::number_lenient_scalar(),
3919            ]
3920        });
3921        &SCHEMA[..]
3922    }
3923    fn eval<'a, 'b, 'c>(
3924        &self,
3925        args: &'c [ArgumentHandle<'a, 'b>],
3926        _ctx: &dyn FunctionContext<'b>,
3927    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3928        let p = coerce_num(&scalar_like_value(&args[0])?)?;
3929        let df = coerce_num(&scalar_like_value(&args[1])?)?;
3930
3931        if df < 1.0 {
3932            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3933                ExcelError::new_num(),
3934            )));
3935        }
3936
3937        match t_inv(p, df) {
3938            Some(result) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
3939                result,
3940            ))),
3941            None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3942                ExcelError::new_num(),
3943            ))),
3944        }
3945    }
3946}
3947
3948/// Returns the chi-square probability for `x` with the specified degrees of freedom.
3949///
3950/// Use `CHISQ.DIST` in cumulative mode for left-tail probability or density mode for the PDF.
3951///
3952/// # Remarks
3953/// - Set `cumulative` to non-zero for CDF mode, or `0` for PDF mode.
3954/// - Requires `x >= 0` and `deg_freedom >= 1`.
3955/// - Returns `#NUM!` for negative `x` or invalid degrees of freedom.
3956/// - Invalid numeric coercions propagate as spreadsheet errors.
3957///
3958/// # Examples
3959///
3960/// ```yaml,sandbox
3961/// title: "Chi-square CDF at zero"
3962/// formula: "=CHISQ.DIST(0,4,TRUE)"
3963/// expected: 0
3964/// ```
3965///
3966/// ```yaml,sandbox
3967/// title: "Chi-square PDF example"
3968/// formula: "=CHISQ.DIST(2,2,FALSE)"
3969/// expected: 0.18393972058572117
3970/// ```
3971#[derive(Debug)]
3972pub struct ChisqDistFn;
3973/// [formualizer-docgen:schema:start]
3974/// Name: CHISQ.DIST
3975/// Type: ChisqDistFn
3976/// Min args: 3
3977/// Max args: 3
3978/// Variadic: false
3979/// Signature: CHISQ.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
3980/// 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}
3981/// Caps: PURE
3982/// [formualizer-docgen:schema:end]
3983impl Function for ChisqDistFn {
3984    func_caps!(PURE);
3985    fn name(&self) -> &'static str {
3986        "CHISQ.DIST"
3987    }
3988    fn min_args(&self) -> usize {
3989        3
3990    }
3991    fn arg_schema(&self) -> &'static [ArgSchema] {
3992        use std::sync::LazyLock;
3993        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
3994            vec![
3995                ArgSchema::number_lenient_scalar(),
3996                ArgSchema::number_lenient_scalar(),
3997                ArgSchema::number_lenient_scalar(),
3998            ]
3999        });
4000        &SCHEMA[..]
4001    }
4002    fn eval<'a, 'b, 'c>(
4003        &self,
4004        args: &'c [ArgumentHandle<'a, 'b>],
4005        _ctx: &dyn FunctionContext<'b>,
4006    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4007        let x = coerce_num(&scalar_like_value(&args[0])?)?;
4008        let df = coerce_num(&scalar_like_value(&args[1])?)?;
4009        let cumulative = coerce_num(&scalar_like_value(&args[2])?)? != 0.0;
4010
4011        if df < 1.0 || x < 0.0 {
4012            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4013                ExcelError::new_num(),
4014            )));
4015        }
4016
4017        let result = if cumulative {
4018            chisq_cdf(x, df)
4019        } else {
4020            chisq_pdf(x, df)
4021        };
4022        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
4023            result,
4024        )))
4025    }
4026}
4027
4028/// Returns the chi-square value whose left-tail probability is `probability`.
4029///
4030/// `CHISQ.INV` inverts `CHISQ.DIST(x, deg_freedom, TRUE)`.
4031///
4032/// # Remarks
4033/// - `probability` must be strictly between `0` and `1`.
4034/// - `deg_freedom` must be at least `1`.
4035/// - Returns `#NUM!` when arguments are outside valid ranges.
4036/// - Invalid numeric coercions propagate as spreadsheet errors.
4037///
4038/// # Examples
4039///
4040/// ```yaml,sandbox
4041/// title: "Median chi-square quantile for df=2"
4042/// formula: "=CHISQ.INV(0.5,2)"
4043/// expected: 1.3862943611198906
4044/// ```
4045///
4046/// ```yaml,sandbox
4047/// title: "Upper quantile for df=10"
4048/// formula: "=CHISQ.INV(0.95,10)"
4049/// expected: 18.307038053275146
4050/// ```
4051#[derive(Debug)]
4052pub struct ChisqInvFn;
4053/// [formualizer-docgen:schema:start]
4054/// Name: CHISQ.INV
4055/// Type: ChisqInvFn
4056/// Min args: 2
4057/// Max args: 2
4058/// Variadic: false
4059/// Signature: CHISQ.INV(arg1: number@scalar, arg2: number@scalar)
4060/// 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}
4061/// Caps: PURE
4062/// [formualizer-docgen:schema:end]
4063impl Function for ChisqInvFn {
4064    func_caps!(PURE);
4065    fn name(&self) -> &'static str {
4066        "CHISQ.INV"
4067    }
4068    fn min_args(&self) -> usize {
4069        2
4070    }
4071    fn arg_schema(&self) -> &'static [ArgSchema] {
4072        use std::sync::LazyLock;
4073        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
4074            vec![
4075                ArgSchema::number_lenient_scalar(),
4076                ArgSchema::number_lenient_scalar(),
4077            ]
4078        });
4079        &SCHEMA[..]
4080    }
4081    fn eval<'a, 'b, 'c>(
4082        &self,
4083        args: &'c [ArgumentHandle<'a, 'b>],
4084        _ctx: &dyn FunctionContext<'b>,
4085    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4086        let p = coerce_num(&scalar_like_value(&args[0])?)?;
4087        let df = coerce_num(&scalar_like_value(&args[1])?)?;
4088
4089        if df < 1.0 {
4090            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4091                ExcelError::new_num(),
4092            )));
4093        }
4094
4095        match chisq_inv(p, df) {
4096            Some(result) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
4097                result,
4098            ))),
4099            None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4100                ExcelError::new_num(),
4101            ))),
4102        }
4103    }
4104}
4105
4106/// Returns the F-distribution probability for `x` with numerator and denominator degrees of freedom.
4107///
4108/// Use `F.DIST` for left-tail cumulative probabilities or density values in variance-ratio tests.
4109///
4110/// # Remarks
4111/// - Set `cumulative` to non-zero for CDF mode, or `0` for PDF mode.
4112/// - Requires `x >= 0`, `deg_freedom1 >= 1`, and `deg_freedom2 >= 1`.
4113/// - Returns `#NUM!` when any domain constraint is violated.
4114/// - Invalid numeric coercions propagate as spreadsheet errors.
4115///
4116/// # Examples
4117///
4118/// ```yaml,sandbox
4119/// title: "F CDF with symmetric 2 and 2 degrees of freedom"
4120/// formula: "=F.DIST(1,2,2,TRUE)"
4121/// expected: 0.5
4122/// ```
4123///
4124/// ```yaml,sandbox
4125/// title: "F PDF with symmetric 2 and 2 degrees of freedom"
4126/// formula: "=F.DIST(1,2,2,FALSE)"
4127/// expected: 0.25
4128/// ```
4129#[derive(Debug)]
4130pub struct FDistFn;
4131/// [formualizer-docgen:schema:start]
4132/// Name: F.DIST
4133/// Type: FDistFn
4134/// Min args: 4
4135/// Max args: 4
4136/// Variadic: false
4137/// Signature: F.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar)
4138/// 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}
4139/// Caps: PURE
4140/// [formualizer-docgen:schema:end]
4141impl Function for FDistFn {
4142    func_caps!(PURE);
4143    fn name(&self) -> &'static str {
4144        "F.DIST"
4145    }
4146    fn min_args(&self) -> usize {
4147        4
4148    }
4149    fn arg_schema(&self) -> &'static [ArgSchema] {
4150        use std::sync::LazyLock;
4151        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
4152            vec![
4153                ArgSchema::number_lenient_scalar(),
4154                ArgSchema::number_lenient_scalar(),
4155                ArgSchema::number_lenient_scalar(),
4156                ArgSchema::number_lenient_scalar(),
4157            ]
4158        });
4159        &SCHEMA[..]
4160    }
4161    fn eval<'a, 'b, 'c>(
4162        &self,
4163        args: &'c [ArgumentHandle<'a, 'b>],
4164        _ctx: &dyn FunctionContext<'b>,
4165    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4166        let x = coerce_num(&scalar_like_value(&args[0])?)?;
4167        let d1 = coerce_num(&scalar_like_value(&args[1])?)?;
4168        let d2 = coerce_num(&scalar_like_value(&args[2])?)?;
4169        let cumulative = coerce_num(&scalar_like_value(&args[3])?)? != 0.0;
4170
4171        if d1 < 1.0 || d2 < 1.0 || x < 0.0 {
4172            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4173                ExcelError::new_num(),
4174            )));
4175        }
4176
4177        let result = if cumulative {
4178            f_cdf(x, d1, d2)
4179        } else {
4180            f_pdf(x, d1, d2)
4181        };
4182        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
4183            result,
4184        )))
4185    }
4186}
4187
4188/// Returns the F value whose left-tail probability equals `probability`.
4189///
4190/// `F.INV` inverts `F.DIST(x, deg_freedom1, deg_freedom2, TRUE)`.
4191///
4192/// # Remarks
4193/// - `probability` must be strictly between `0` and `1`.
4194/// - `deg_freedom1` and `deg_freedom2` must each be at least `1`.
4195/// - Returns `#NUM!` for invalid probability or degree-of-freedom arguments.
4196/// - Invalid numeric coercions propagate as spreadsheet errors.
4197///
4198/// # Examples
4199///
4200/// ```yaml,sandbox
4201/// title: "Median F quantile with symmetric 2 and 2 degrees of freedom"
4202/// formula: "=F.INV(0.5,2,2)"
4203/// expected: 1
4204/// ```
4205///
4206/// ```yaml,sandbox
4207/// title: "Upper-tail F critical value"
4208/// formula: "=F.INV(0.95,5,10)"
4209/// expected: 3.3258345304130112
4210/// ```
4211#[derive(Debug)]
4212pub struct FInvFn;
4213/// [formualizer-docgen:schema:start]
4214/// Name: F.INV
4215/// Type: FInvFn
4216/// Min args: 3
4217/// Max args: 3
4218/// Variadic: false
4219/// Signature: F.INV(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
4220/// 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}
4221/// Caps: PURE
4222/// [formualizer-docgen:schema:end]
4223impl Function for FInvFn {
4224    func_caps!(PURE);
4225    fn name(&self) -> &'static str {
4226        "F.INV"
4227    }
4228    fn min_args(&self) -> usize {
4229        3
4230    }
4231    fn arg_schema(&self) -> &'static [ArgSchema] {
4232        use std::sync::LazyLock;
4233        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
4234            vec![
4235                ArgSchema::number_lenient_scalar(),
4236                ArgSchema::number_lenient_scalar(),
4237                ArgSchema::number_lenient_scalar(),
4238            ]
4239        });
4240        &SCHEMA[..]
4241    }
4242    fn eval<'a, 'b, 'c>(
4243        &self,
4244        args: &'c [ArgumentHandle<'a, 'b>],
4245        _ctx: &dyn FunctionContext<'b>,
4246    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4247        let p = coerce_num(&scalar_like_value(&args[0])?)?;
4248        let d1 = coerce_num(&scalar_like_value(&args[1])?)?;
4249        let d2 = coerce_num(&scalar_like_value(&args[2])?)?;
4250
4251        if d1 < 1.0 || d2 < 1.0 {
4252            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4253                ExcelError::new_num(),
4254            )));
4255        }
4256
4257        match f_inv(p, d1, d2) {
4258            Some(result) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
4259                result,
4260            ))),
4261            None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4262                ExcelError::new_num(),
4263            ))),
4264        }
4265    }
4266}
4267
4268/// Returns the z-score of `x` relative to a mean and standard deviation.
4269///
4270/// `STANDARDIZE` computes `(x - mean) / standard_dev`.
4271///
4272/// # Remarks
4273/// - `standard_dev` must be strictly greater than `0`.
4274/// - Returns `#NUM!` when `standard_dev <= 0`.
4275/// - Positive output means `x` is above the mean; negative output means below.
4276/// - Invalid numeric coercions propagate as spreadsheet errors.
4277///
4278/// # Examples
4279///
4280/// ```yaml,sandbox
4281/// title: "One standard deviation above the mean"
4282/// formula: "=STANDARDIZE(42,40,2)"
4283/// expected: 1
4284/// ```
4285///
4286/// ```yaml,sandbox
4287/// title: "Exactly at the mean"
4288/// formula: "=STANDARDIZE(100,100,10)"
4289/// expected: 0
4290/// ```
4291#[derive(Debug)]
4292pub struct StandardizeFn;
4293/// [formualizer-docgen:schema:start]
4294/// Name: STANDARDIZE
4295/// Type: StandardizeFn
4296/// Min args: 3
4297/// Max args: 3
4298/// Variadic: false
4299/// Signature: STANDARDIZE(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
4300/// 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}
4301/// Caps: PURE
4302/// [formualizer-docgen:schema:end]
4303impl Function for StandardizeFn {
4304    func_caps!(PURE);
4305    fn name(&self) -> &'static str {
4306        "STANDARDIZE"
4307    }
4308    fn min_args(&self) -> usize {
4309        3
4310    }
4311    fn arg_schema(&self) -> &'static [ArgSchema] {
4312        use std::sync::LazyLock;
4313        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
4314            vec![
4315                ArgSchema::number_lenient_scalar(),
4316                ArgSchema::number_lenient_scalar(),
4317                ArgSchema::number_lenient_scalar(),
4318            ]
4319        });
4320        &SCHEMA[..]
4321    }
4322    fn eval<'a, 'b, 'c>(
4323        &self,
4324        args: &'c [ArgumentHandle<'a, 'b>],
4325        _ctx: &dyn FunctionContext<'b>,
4326    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4327        let x = coerce_num(&scalar_like_value(&args[0])?)?;
4328        let mean = coerce_num(&scalar_like_value(&args[1])?)?;
4329        let std_dev = coerce_num(&scalar_like_value(&args[2])?)?;
4330
4331        if std_dev <= 0.0 {
4332            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4333                ExcelError::new_num(),
4334            )));
4335        }
4336
4337        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
4338            (x - mean) / std_dev,
4339        )))
4340    }
4341}
4342
4343/// Helper: Factorial function
4344fn factorial(n: i64) -> f64 {
4345    if n < 0 {
4346        return f64::NAN;
4347    }
4348    if n <= 1 {
4349        return 1.0;
4350    }
4351    // For large n, use gamma function: n! = Gamma(n+1)
4352    if n > 20 {
4353        return ln_gamma((n + 1) as f64).exp();
4354    }
4355    let mut result = 1.0;
4356    for i in 2..=n {
4357        result *= i as f64;
4358    }
4359    result
4360}
4361
4362/// Helper: Log of binomial coefficient (n choose k)
4363fn ln_binom(n: i64, k: i64) -> f64 {
4364    if k < 0 || k > n {
4365        return f64::NEG_INFINITY;
4366    }
4367    if k == 0 || k == n {
4368        return 0.0;
4369    }
4370    ln_gamma((n + 1) as f64) - ln_gamma((k + 1) as f64) - ln_gamma((n - k + 1) as f64)
4371}
4372
4373/// Returns the binomial probability for a count of successes across independent trials.
4374///
4375/// Use `BINOM.DIST` to evaluate either exact-success probability (PMF) or cumulative probability
4376/// up to a success count (CDF).
4377///
4378/// # Remarks
4379/// - `number_s` and `trials` are truncated to integers.
4380/// - Requires `0 <= number_s <= trials`, `trials >= 0`, and `0 <= probability_s <= 1`.
4381/// - Set `cumulative` to non-zero for CDF mode, or `0` for PMF mode.
4382/// - Returns `#NUM!` for invalid count or probability ranges.
4383///
4384/// # Examples
4385///
4386/// ```yaml,sandbox
4387/// title: "Binomial PMF for exactly 3 successes"
4388/// formula: "=BINOM.DIST(3,10,0.5,FALSE)"
4389/// expected: 0.1171875
4390/// ```
4391///
4392/// ```yaml,sandbox
4393/// title: "Binomial CDF for at most 3 successes"
4394/// formula: "=BINOM.DIST(3,10,0.5,TRUE)"
4395/// expected: 0.171875
4396/// ```
4397#[derive(Debug)]
4398pub struct BinomDistFn;
4399/// [formualizer-docgen:schema:start]
4400/// Name: BINOM.DIST
4401/// Type: BinomDistFn
4402/// Min args: 4
4403/// Max args: 4
4404/// Variadic: false
4405/// Signature: BINOM.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar)
4406/// 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}
4407/// Caps: PURE
4408/// [formualizer-docgen:schema:end]
4409impl Function for BinomDistFn {
4410    func_caps!(PURE);
4411    fn name(&self) -> &'static str {
4412        "BINOM.DIST"
4413    }
4414    fn min_args(&self) -> usize {
4415        4
4416    }
4417    fn arg_schema(&self) -> &'static [ArgSchema] {
4418        use std::sync::LazyLock;
4419        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
4420            vec![
4421                ArgSchema::number_lenient_scalar(),
4422                ArgSchema::number_lenient_scalar(),
4423                ArgSchema::number_lenient_scalar(),
4424                ArgSchema::number_lenient_scalar(),
4425            ]
4426        });
4427        &SCHEMA[..]
4428    }
4429    fn eval<'a, 'b, 'c>(
4430        &self,
4431        args: &'c [ArgumentHandle<'a, 'b>],
4432        _ctx: &dyn FunctionContext<'b>,
4433    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4434        let k = coerce_num(&scalar_like_value(&args[0])?)?.trunc() as i64;
4435        let n = coerce_num(&scalar_like_value(&args[1])?)?.trunc() as i64;
4436        let p = coerce_num(&scalar_like_value(&args[2])?)?;
4437        let cumulative = coerce_num(&scalar_like_value(&args[3])?)? != 0.0;
4438
4439        if n < 0 || k < 0 || k > n || !(0.0..=1.0).contains(&p) {
4440            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4441                ExcelError::new_num(),
4442            )));
4443        }
4444
4445        let result = if cumulative {
4446            // CDF: sum from i=0 to k of P(X=i)
4447            let mut sum = 0.0;
4448            for i in 0..=k {
4449                let ln_prob =
4450                    ln_binom(n, i) + (i as f64) * p.ln() + ((n - i) as f64) * (1.0 - p).ln();
4451                sum += ln_prob.exp();
4452            }
4453            sum
4454        } else {
4455            // PMF: P(X=k)
4456            let ln_prob = ln_binom(n, k) + (k as f64) * p.ln() + ((n - k) as f64) * (1.0 - p).ln();
4457            ln_prob.exp()
4458        };
4459
4460        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
4461            result,
4462        )))
4463    }
4464}
4465
4466/// Returns the Poisson probability for event count `x` at average rate `mean`.
4467///
4468/// `POISSON.DIST` supports exact-count mode (PMF) and cumulative mode (CDF).
4469///
4470/// # Remarks
4471/// - `x` is truncated to an integer and must be at least `0`.
4472/// - `mean` must be non-negative.
4473/// - Set `cumulative` to non-zero for CDF mode, or `0` for PMF mode.
4474/// - Returns `#NUM!` for negative counts or negative mean values.
4475///
4476/// # Examples
4477///
4478/// ```yaml,sandbox
4479/// title: "Poisson PMF for zero events"
4480/// formula: "=POISSON.DIST(0,2,FALSE)"
4481/// expected: 0.1353352832366127
4482/// ```
4483///
4484/// ```yaml,sandbox
4485/// title: "Poisson CDF up to two events"
4486/// formula: "=POISSON.DIST(2,2,TRUE)"
4487/// expected: 0.6766764161830634
4488/// ```
4489#[derive(Debug)]
4490pub struct PoissonDistFn;
4491/// [formualizer-docgen:schema:start]
4492/// Name: POISSON.DIST
4493/// Type: PoissonDistFn
4494/// Min args: 3
4495/// Max args: 3
4496/// Variadic: false
4497/// Signature: POISSON.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
4498/// 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}
4499/// Caps: PURE
4500/// [formualizer-docgen:schema:end]
4501impl Function for PoissonDistFn {
4502    func_caps!(PURE);
4503    fn name(&self) -> &'static str {
4504        "POISSON.DIST"
4505    }
4506    fn min_args(&self) -> usize {
4507        3
4508    }
4509    fn arg_schema(&self) -> &'static [ArgSchema] {
4510        use std::sync::LazyLock;
4511        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
4512            vec![
4513                ArgSchema::number_lenient_scalar(),
4514                ArgSchema::number_lenient_scalar(),
4515                ArgSchema::number_lenient_scalar(),
4516            ]
4517        });
4518        &SCHEMA[..]
4519    }
4520    fn eval<'a, 'b, 'c>(
4521        &self,
4522        args: &'c [ArgumentHandle<'a, 'b>],
4523        _ctx: &dyn FunctionContext<'b>,
4524    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4525        let k = coerce_num(&scalar_like_value(&args[0])?)?.trunc() as i64;
4526        let lambda = coerce_num(&scalar_like_value(&args[1])?)?;
4527        let cumulative = coerce_num(&scalar_like_value(&args[2])?)? != 0.0;
4528
4529        if k < 0 || lambda < 0.0 {
4530            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4531                ExcelError::new_num(),
4532            )));
4533        }
4534
4535        let result = if cumulative {
4536            // CDF: sum from i=0 to k of P(X=i) = 1 - Q(k+1, lambda)
4537            // Using the regularized incomplete gamma function
4538            1.0 - gamma_p((k + 1) as f64, lambda)
4539        } else {
4540            // PMF: P(X=k) = lambda^k * e^(-lambda) / k!
4541            // Use log to avoid overflow
4542            let ln_prob = (k as f64) * lambda.ln() - lambda - ln_gamma((k + 1) as f64);
4543            ln_prob.exp()
4544        };
4545
4546        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
4547            result,
4548        )))
4549    }
4550}
4551
4552/// Returns the exponential-distribution probability at `x` for rate `lambda`.
4553///
4554/// Use `EXPON.DIST` for waiting-time models where events occur with a constant hazard rate.
4555///
4556/// # Remarks
4557/// - Requires `x >= 0` and `lambda > 0`.
4558/// - Set `cumulative` to non-zero for CDF mode, or `0` for PDF mode.
4559/// - Returns `#NUM!` when inputs violate domain requirements.
4560/// - Invalid numeric coercions propagate as spreadsheet errors.
4561///
4562/// # Examples
4563///
4564/// ```yaml,sandbox
4565/// title: "Exponential CDF"
4566/// formula: "=EXPON.DIST(1,1,TRUE)"
4567/// expected: 0.6321205588285577
4568/// ```
4569///
4570/// ```yaml,sandbox
4571/// title: "Exponential PDF"
4572/// formula: "=EXPON.DIST(1,1,FALSE)"
4573/// expected: 0.36787944117144233
4574/// ```
4575#[derive(Debug)]
4576pub struct ExponDistFn;
4577/// [formualizer-docgen:schema:start]
4578/// Name: EXPON.DIST
4579/// Type: ExponDistFn
4580/// Min args: 3
4581/// Max args: 3
4582/// Variadic: false
4583/// Signature: EXPON.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
4584/// 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}
4585/// Caps: PURE
4586/// [formualizer-docgen:schema:end]
4587impl Function for ExponDistFn {
4588    func_caps!(PURE);
4589    fn name(&self) -> &'static str {
4590        "EXPON.DIST"
4591    }
4592    fn min_args(&self) -> usize {
4593        3
4594    }
4595    fn arg_schema(&self) -> &'static [ArgSchema] {
4596        use std::sync::LazyLock;
4597        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
4598            vec![
4599                ArgSchema::number_lenient_scalar(),
4600                ArgSchema::number_lenient_scalar(),
4601                ArgSchema::number_lenient_scalar(),
4602            ]
4603        });
4604        &SCHEMA[..]
4605    }
4606    fn eval<'a, 'b, 'c>(
4607        &self,
4608        args: &'c [ArgumentHandle<'a, 'b>],
4609        _ctx: &dyn FunctionContext<'b>,
4610    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4611        let x = coerce_num(&scalar_like_value(&args[0])?)?;
4612        let lambda = coerce_num(&scalar_like_value(&args[1])?)?;
4613        let cumulative = coerce_num(&scalar_like_value(&args[2])?)? != 0.0;
4614
4615        if x < 0.0 || lambda <= 0.0 {
4616            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4617                ExcelError::new_num(),
4618            )));
4619        }
4620
4621        let result = if cumulative {
4622            // CDF: 1 - e^(-lambda*x)
4623            1.0 - (-lambda * x).exp()
4624        } else {
4625            // PDF: lambda * e^(-lambda*x)
4626            lambda * (-lambda * x).exp()
4627        };
4628
4629        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
4630            result,
4631        )))
4632    }
4633}
4634
4635/// Returns the gamma-distribution probability at `x` for shape `alpha` and scale `beta`.
4636///
4637/// `GAMMA.DIST` supports cumulative and density modes for right-skewed waiting-time models.
4638///
4639/// # Remarks
4640/// - Requires `x >= 0`, `alpha > 0`, and `beta > 0`.
4641/// - Set `cumulative` to non-zero for CDF mode, or `0` for PDF mode.
4642/// - Returns `#NUM!` when any parameter is outside its valid range.
4643/// - Invalid numeric coercions propagate as spreadsheet errors.
4644///
4645/// # Examples
4646///
4647/// ```yaml,sandbox
4648/// title: "Gamma CDF with alpha=1 and beta=2"
4649/// formula: "=GAMMA.DIST(2,1,2,TRUE)"
4650/// expected: 0.6321205588285577
4651/// ```
4652///
4653/// ```yaml,sandbox
4654/// title: "Gamma PDF with alpha=1 and beta=2"
4655/// formula: "=GAMMA.DIST(2,1,2,FALSE)"
4656/// expected: 0.18393972058572117
4657/// ```
4658#[derive(Debug)]
4659pub struct GammaDistFn;
4660/// [formualizer-docgen:schema:start]
4661/// Name: GAMMA.DIST
4662/// Type: GammaDistFn
4663/// Min args: 4
4664/// Max args: 4
4665/// Variadic: false
4666/// Signature: GAMMA.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar)
4667/// 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}
4668/// Caps: PURE
4669/// [formualizer-docgen:schema:end]
4670impl Function for GammaDistFn {
4671    func_caps!(PURE);
4672    fn name(&self) -> &'static str {
4673        "GAMMA.DIST"
4674    }
4675    fn min_args(&self) -> usize {
4676        4
4677    }
4678    fn arg_schema(&self) -> &'static [ArgSchema] {
4679        use std::sync::LazyLock;
4680        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
4681            vec![
4682                ArgSchema::number_lenient_scalar(),
4683                ArgSchema::number_lenient_scalar(),
4684                ArgSchema::number_lenient_scalar(),
4685                ArgSchema::number_lenient_scalar(),
4686            ]
4687        });
4688        &SCHEMA[..]
4689    }
4690    fn eval<'a, 'b, 'c>(
4691        &self,
4692        args: &'c [ArgumentHandle<'a, 'b>],
4693        _ctx: &dyn FunctionContext<'b>,
4694    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4695        let x = coerce_num(&scalar_like_value(&args[0])?)?;
4696        let alpha = coerce_num(&scalar_like_value(&args[1])?)?; // shape
4697        let beta = coerce_num(&scalar_like_value(&args[2])?)?; // scale
4698        let cumulative = coerce_num(&scalar_like_value(&args[3])?)? != 0.0;
4699
4700        if x < 0.0 || alpha <= 0.0 || beta <= 0.0 {
4701            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4702                ExcelError::new_num(),
4703            )));
4704        }
4705
4706        let result = if cumulative {
4707            // CDF: P(alpha, x/beta) where P is the regularized lower incomplete gamma
4708            gamma_p(alpha, x / beta)
4709        } else {
4710            // PDF: x^(alpha-1) * e^(-x/beta) / (beta^alpha * Gamma(alpha))
4711            let ln_pdf = (alpha - 1.0) * x.ln() - x / beta - alpha * beta.ln() - ln_gamma(alpha);
4712            ln_pdf.exp()
4713        };
4714
4715        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
4716            result,
4717        )))
4718    }
4719}
4720
4721/// Returns the Weibull-distribution probability at `x` for shape `alpha` and scale `beta`.
4722///
4723/// `WEIBULL.DIST` is commonly used for reliability and time-to-failure analysis.
4724///
4725/// # Remarks
4726/// - Requires `x >= 0`, `alpha > 0`, and `beta > 0`.
4727/// - Set `cumulative` to non-zero for CDF mode, or `0` for PDF mode.
4728/// - Returns `#NUM!` when parameters fall outside valid ranges.
4729/// - In PDF mode at `x = 0`, behavior follows the Weibull shape-specific limit.
4730///
4731/// # Examples
4732///
4733/// ```yaml,sandbox
4734/// title: "Weibull CDF with alpha=1 and beta=2"
4735/// formula: "=WEIBULL.DIST(2,1,2,TRUE)"
4736/// expected: 0.6321205588285577
4737/// ```
4738///
4739/// ```yaml,sandbox
4740/// title: "Weibull PDF with alpha=1 and beta=2"
4741/// formula: "=WEIBULL.DIST(2,1,2,FALSE)"
4742/// expected: 0.18393972058572117
4743/// ```
4744#[derive(Debug)]
4745pub struct WeibullDistFn;
4746/// [formualizer-docgen:schema:start]
4747/// Name: WEIBULL.DIST
4748/// Type: WeibullDistFn
4749/// Min args: 4
4750/// Max args: 4
4751/// Variadic: false
4752/// Signature: WEIBULL.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar)
4753/// 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}
4754/// Caps: PURE
4755/// [formualizer-docgen:schema:end]
4756impl Function for WeibullDistFn {
4757    func_caps!(PURE);
4758    fn name(&self) -> &'static str {
4759        "WEIBULL.DIST"
4760    }
4761    fn min_args(&self) -> usize {
4762        4
4763    }
4764    fn arg_schema(&self) -> &'static [ArgSchema] {
4765        use std::sync::LazyLock;
4766        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
4767            vec![
4768                ArgSchema::number_lenient_scalar(),
4769                ArgSchema::number_lenient_scalar(),
4770                ArgSchema::number_lenient_scalar(),
4771                ArgSchema::number_lenient_scalar(),
4772            ]
4773        });
4774        &SCHEMA[..]
4775    }
4776    fn eval<'a, 'b, 'c>(
4777        &self,
4778        args: &'c [ArgumentHandle<'a, 'b>],
4779        _ctx: &dyn FunctionContext<'b>,
4780    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4781        let x = coerce_num(&scalar_like_value(&args[0])?)?;
4782        let alpha = coerce_num(&scalar_like_value(&args[1])?)?; // shape
4783        let beta = coerce_num(&scalar_like_value(&args[2])?)?; // scale
4784        let cumulative = coerce_num(&scalar_like_value(&args[3])?)? != 0.0;
4785
4786        if x < 0.0 || alpha <= 0.0 || beta <= 0.0 {
4787            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4788                ExcelError::new_num(),
4789            )));
4790        }
4791
4792        let result = if cumulative {
4793            // CDF: 1 - e^(-(x/beta)^alpha)
4794            1.0 - (-(x / beta).powf(alpha)).exp()
4795        } else {
4796            // PDF: (alpha/beta) * (x/beta)^(alpha-1) * e^(-(x/beta)^alpha)
4797            if x == 0.0 {
4798                if alpha < 1.0 {
4799                    f64::INFINITY
4800                } else if alpha == 1.0 {
4801                    alpha / beta
4802                } else {
4803                    0.0
4804                }
4805            } else {
4806                (alpha / beta) * (x / beta).powf(alpha - 1.0) * (-(x / beta).powf(alpha)).exp()
4807            }
4808        };
4809
4810        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
4811            result,
4812        )))
4813    }
4814}
4815
4816/// Returns the beta-distribution probability for `x`, with optional lower/upper bounds.
4817///
4818/// `BETA.DIST` can evaluate either the cumulative probability or density on `[A, B]` (default
4819/// `[0, 1]`).
4820///
4821/// # Remarks
4822/// - Requires `alpha > 0`, `beta > 0`, and `A < B`.
4823/// - `x` must lie within the inclusive interval `[A, B]`.
4824/// - Set `cumulative` to non-zero for CDF mode, or `0` for PDF mode.
4825/// - Returns `#NUM!` for invalid bounds, parameters, or out-of-range `x`.
4826///
4827/// # Examples
4828///
4829/// ```yaml,sandbox
4830/// title: "Uniform beta CDF on [0,1]"
4831/// formula: "=BETA.DIST(0.3,1,1,TRUE)"
4832/// expected: 0.3
4833/// ```
4834///
4835/// ```yaml,sandbox
4836/// title: "Uniform beta PDF on [0,1]"
4837/// formula: "=BETA.DIST(0.3,1,1,FALSE)"
4838/// expected: 1
4839/// ```
4840#[derive(Debug)]
4841pub struct BetaDistFn;
4842/// [formualizer-docgen:schema:start]
4843/// Name: BETA.DIST
4844/// Type: BetaDistFn
4845/// Min args: 4
4846/// Max args: variadic
4847/// Variadic: true
4848/// Signature: BETA.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar, arg5: number@scalar, arg6...: number@scalar)
4849/// 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}
4850/// Caps: PURE
4851/// [formualizer-docgen:schema:end]
4852impl Function for BetaDistFn {
4853    func_caps!(PURE);
4854    fn name(&self) -> &'static str {
4855        "BETA.DIST"
4856    }
4857    fn min_args(&self) -> usize {
4858        4
4859    }
4860    fn variadic(&self) -> bool {
4861        true
4862    }
4863    fn arg_schema(&self) -> &'static [ArgSchema] {
4864        use std::sync::LazyLock;
4865        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
4866            vec![
4867                ArgSchema::number_lenient_scalar(),
4868                ArgSchema::number_lenient_scalar(),
4869                ArgSchema::number_lenient_scalar(),
4870                ArgSchema::number_lenient_scalar(),
4871                ArgSchema::number_lenient_scalar(),
4872                ArgSchema::number_lenient_scalar(),
4873            ]
4874        });
4875        &SCHEMA[..]
4876    }
4877    fn eval<'a, 'b, 'c>(
4878        &self,
4879        args: &'c [ArgumentHandle<'a, 'b>],
4880        _ctx: &dyn FunctionContext<'b>,
4881    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4882        let x = coerce_num(&scalar_like_value(&args[0])?)?;
4883        let alpha = coerce_num(&scalar_like_value(&args[1])?)?;
4884        let beta_param = coerce_num(&scalar_like_value(&args[2])?)?;
4885        let cumulative = coerce_num(&scalar_like_value(&args[3])?)? != 0.0;
4886
4887        // Optional bounds A and B (default 0 and 1)
4888        let a = if args.len() > 4 {
4889            coerce_num(&scalar_like_value(&args[4])?)?
4890        } else {
4891            0.0
4892        };
4893        let b = if args.len() > 5 {
4894            coerce_num(&scalar_like_value(&args[5])?)?
4895        } else {
4896            1.0
4897        };
4898
4899        if alpha <= 0.0 || beta_param <= 0.0 || a >= b {
4900            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4901                ExcelError::new_num(),
4902            )));
4903        }
4904
4905        // x must be in [a, b]
4906        if x < a || x > b {
4907            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4908                ExcelError::new_num(),
4909            )));
4910        }
4911
4912        // Transform x to standard [0,1] interval
4913        let x_std = (x - a) / (b - a);
4914
4915        let result = if cumulative {
4916            // CDF: I_x(alpha, beta) - regularized incomplete beta function
4917            beta_i(x_std, alpha, beta_param)
4918        } else {
4919            // PDF: (x-A)^(alpha-1) * (B-x)^(beta-1) / ((B-A)^(alpha+beta-1) * B(alpha, beta))
4920            let ln_beta = ln_gamma(alpha) + ln_gamma(beta_param) - ln_gamma(alpha + beta_param);
4921            let scale = b - a;
4922            if (x_std == 0.0 && alpha < 1.0) || (x_std == 1.0 && beta_param < 1.0) {
4923                f64::INFINITY
4924            } else if x_std == 0.0 {
4925                if alpha == 1.0 {
4926                    (1.0 - x_std).powf(beta_param - 1.0) / (scale * ln_beta.exp())
4927                } else {
4928                    0.0
4929                }
4930            } else if x_std == 1.0 {
4931                if beta_param == 1.0 {
4932                    x_std.powf(alpha - 1.0) / (scale * ln_beta.exp())
4933                } else {
4934                    0.0
4935                }
4936            } else {
4937                let ln_pdf =
4938                    (alpha - 1.0) * x_std.ln() + (beta_param - 1.0) * (1.0 - x_std).ln() - ln_beta;
4939                ln_pdf.exp() / scale
4940            }
4941        };
4942
4943        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
4944            result,
4945        )))
4946    }
4947}
4948
4949/// Returns negative-binomial probabilities for failures observed before a target success count.
4950///
4951/// `NEGBINOM.DIST` supports exact-failure mode (PMF) and cumulative mode (CDF).
4952///
4953/// # Remarks
4954/// - `number_f` is truncated and must be `>= 0`.
4955/// - `number_s` is truncated and must be `>= 1`.
4956/// - `probability_s` must satisfy `0 < p < 1`.
4957/// - Returns `#NUM!` when counts or probability are outside valid ranges.
4958///
4959/// # Examples
4960///
4961/// ```yaml,sandbox
4962/// title: "Negative binomial PMF"
4963/// formula: "=NEGBINOM.DIST(2,1,0.5,FALSE)"
4964/// expected: 0.125
4965/// ```
4966///
4967/// ```yaml,sandbox
4968/// title: "Negative binomial CDF"
4969/// formula: "=NEGBINOM.DIST(2,1,0.5,TRUE)"
4970/// expected: 0.875
4971/// ```
4972#[derive(Debug)]
4973pub struct NegbinomDistFn;
4974/// [formualizer-docgen:schema:start]
4975/// Name: NEGBINOM.DIST
4976/// Type: NegbinomDistFn
4977/// Min args: 4
4978/// Max args: 4
4979/// Variadic: false
4980/// Signature: NEGBINOM.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar)
4981/// 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}
4982/// Caps: PURE
4983/// [formualizer-docgen:schema:end]
4984impl Function for NegbinomDistFn {
4985    func_caps!(PURE);
4986    fn name(&self) -> &'static str {
4987        "NEGBINOM.DIST"
4988    }
4989    fn min_args(&self) -> usize {
4990        4
4991    }
4992    fn arg_schema(&self) -> &'static [ArgSchema] {
4993        use std::sync::LazyLock;
4994        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
4995            vec![
4996                ArgSchema::number_lenient_scalar(),
4997                ArgSchema::number_lenient_scalar(),
4998                ArgSchema::number_lenient_scalar(),
4999                ArgSchema::number_lenient_scalar(),
5000            ]
5001        });
5002        &SCHEMA[..]
5003    }
5004    fn eval<'a, 'b, 'c>(
5005        &self,
5006        args: &'c [ArgumentHandle<'a, 'b>],
5007        _ctx: &dyn FunctionContext<'b>,
5008    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5009        let number_f = coerce_num(&scalar_like_value(&args[0])?)?.trunc() as i64; // number of failures
5010        let number_s = coerce_num(&scalar_like_value(&args[1])?)?.trunc() as i64; // number of successes
5011        let prob_s = coerce_num(&scalar_like_value(&args[2])?)?; // probability of success
5012        let cumulative = coerce_num(&scalar_like_value(&args[3])?)? != 0.0;
5013
5014        if number_f < 0 || number_s < 1 || prob_s <= 0.0 || prob_s >= 1.0 {
5015            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5016                ExcelError::new_num(),
5017            )));
5018        }
5019
5020        let result = if cumulative {
5021            // CDF: sum from i=0 to number_f of P(X=i)
5022            // This is equivalent to I_{prob_s}(number_s, number_f + 1) using regularized beta
5023            beta_i(prob_s, number_s as f64, (number_f + 1) as f64)
5024        } else {
5025            // PMF: C(number_f + number_s - 1, number_s - 1) * prob_s^number_s * (1-prob_s)^number_f
5026            // = C(k + r - 1, r - 1) * p^r * (1-p)^k where k = number_f, r = number_s
5027            let ln_prob = ln_binom(number_f + number_s - 1, number_s - 1)
5028                + (number_s as f64) * prob_s.ln()
5029                + (number_f as f64) * (1.0 - prob_s).ln();
5030            ln_prob.exp()
5031        };
5032
5033        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
5034            result,
5035        )))
5036    }
5037}
5038
5039/// Returns hypergeometric probabilities for successes drawn without replacement.
5040///
5041/// Use `HYPGEOM.DIST` for finite-population sampling where each draw changes remaining odds.
5042///
5043/// # Remarks
5044/// - Count inputs are truncated to integers.
5045/// - Requires valid population/sample bounds and feasible success counts.
5046/// - Set `cumulative` to non-zero for CDF mode, or `0` for PMF mode.
5047/// - Returns `#NUM!` for invalid population setup; out-of-support PMF values return `0`.
5048///
5049/// # Examples
5050///
5051/// ```yaml,sandbox
5052/// title: "Hypergeometric PMF"
5053/// formula: "=HYPGEOM.DIST(1,3,4,10,FALSE)"
5054/// expected: 0.5
5055/// ```
5056///
5057/// ```yaml,sandbox
5058/// title: "Hypergeometric CDF"
5059/// formula: "=HYPGEOM.DIST(1,3,4,10,TRUE)"
5060/// expected: 0.6666666666666666
5061/// ```
5062#[derive(Debug)]
5063pub struct HypgeomDistFn;
5064/// [formualizer-docgen:schema:start]
5065/// Name: HYPGEOM.DIST
5066/// Type: HypgeomDistFn
5067/// Min args: 5
5068/// Max args: 5
5069/// Variadic: false
5070/// Signature: HYPGEOM.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar, arg5: number@scalar)
5071/// 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}
5072/// Caps: PURE
5073/// [formualizer-docgen:schema:end]
5074impl Function for HypgeomDistFn {
5075    func_caps!(PURE);
5076    fn name(&self) -> &'static str {
5077        "HYPGEOM.DIST"
5078    }
5079    fn min_args(&self) -> usize {
5080        5
5081    }
5082    fn arg_schema(&self) -> &'static [ArgSchema] {
5083        use std::sync::LazyLock;
5084        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
5085            vec![
5086                ArgSchema::number_lenient_scalar(),
5087                ArgSchema::number_lenient_scalar(),
5088                ArgSchema::number_lenient_scalar(),
5089                ArgSchema::number_lenient_scalar(),
5090                ArgSchema::number_lenient_scalar(),
5091            ]
5092        });
5093        &SCHEMA[..]
5094    }
5095    fn eval<'a, 'b, 'c>(
5096        &self,
5097        args: &'c [ArgumentHandle<'a, 'b>],
5098        _ctx: &dyn FunctionContext<'b>,
5099    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5100        let sample_s = coerce_num(&scalar_like_value(&args[0])?)?.trunc() as i64; // successes in sample
5101        let number_sample = coerce_num(&scalar_like_value(&args[1])?)?.trunc() as i64; // sample size
5102        let population_s = coerce_num(&scalar_like_value(&args[2])?)?.trunc() as i64; // successes in population
5103        let number_pop = coerce_num(&scalar_like_value(&args[3])?)?.trunc() as i64; // population size
5104        let cumulative = coerce_num(&scalar_like_value(&args[4])?)? != 0.0;
5105
5106        // Validation
5107        if number_pop <= 0
5108            || population_s < 0
5109            || population_s > number_pop
5110            || number_sample < 0
5111            || number_sample > number_pop
5112            || sample_s < 0
5113        {
5114            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5115                ExcelError::new_num(),
5116            )));
5117        }
5118
5119        // sample_s must be at least max(0, number_sample - (number_pop - population_s))
5120        // and at most min(number_sample, population_s)
5121        let min_successes = 0.max(number_sample - (number_pop - population_s));
5122        let max_successes = number_sample.min(population_s);
5123
5124        if sample_s < min_successes || sample_s > max_successes {
5125            // Return 0 for PMF, or appropriate CDF value
5126            if cumulative {
5127                if sample_s < min_successes {
5128                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0)));
5129                } else {
5130                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(1.0)));
5131                }
5132            } else {
5133                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0)));
5134            }
5135        }
5136
5137        let result = if cumulative {
5138            // CDF: sum from i=min_successes to sample_s of P(X=i)
5139            let mut sum = 0.0;
5140            for i in min_successes..=sample_s {
5141                sum += hypgeom_pmf(i, number_sample, population_s, number_pop);
5142            }
5143            sum
5144        } else {
5145            // PMF: C(population_s, sample_s) * C(number_pop - population_s, number_sample - sample_s) / C(number_pop, number_sample)
5146            hypgeom_pmf(sample_s, number_sample, population_s, number_pop)
5147        };
5148
5149        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
5150            result,
5151        )))
5152    }
5153}
5154
5155/// Helper: Hypergeometric PMF
5156fn hypgeom_pmf(k: i64, n: i64, k_pop: i64, n_pop: i64) -> f64 {
5157    // P(X=k) = C(K, k) * C(N-K, n-k) / C(N, n)
5158    // Using logs to avoid overflow
5159    let ln_prob = ln_binom(k_pop, k) + ln_binom(n_pop - k_pop, n - k) - ln_binom(n_pop, n);
5160    ln_prob.exp()
5161}
5162
5163/* ═══════════════════════════════════════════════════════════════════════════
5164COVARIANCE AND CORRELATION FUNCTIONS
5165═══════════════════════════════════════════════════════════════════════════ */
5166
5167/// Returns population covariance for two paired numeric data sets.
5168///
5169/// `COVARIANCE.P` measures joint variability using `n` in the denominator.
5170///
5171/// # Remarks
5172/// - Arrays must resolve to the same number of numeric points.
5173/// - Uses population scaling (`/ n`) rather than sample scaling.
5174/// - Positive output indicates same-direction movement; negative output indicates opposite movement.
5175/// - Pairing and shape mismatches return spreadsheet errors from paired-array validation.
5176///
5177/// # Examples
5178///
5179/// ```yaml,sandbox
5180/// title: "Positive population covariance"
5181/// formula: "=COVARIANCE.P({1,3,5},{2,4,6})"
5182/// expected: 2.6666666666666665
5183/// ```
5184///
5185/// ```yaml,sandbox
5186/// title: "Negative population covariance"
5187/// formula: "=COVARIANCE.P({1,2,3},{3,2,1})"
5188/// expected: -0.6666666666666666
5189/// ```
5190#[derive(Debug)]
5191pub struct CovariancePFn;
5192/// [formualizer-docgen:schema:start]
5193/// Name: COVARIANCE.P
5194/// Type: CovariancePFn
5195/// Min args: 2
5196/// Max args: 1
5197/// Variadic: false
5198/// Signature: COVARIANCE.P(arg1: number@range)
5199/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
5200/// Caps: PURE, REDUCTION, NUMERIC_ONLY
5201/// [formualizer-docgen:schema:end]
5202impl Function for CovariancePFn {
5203    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
5204    fn name(&self) -> &'static str {
5205        "COVARIANCE.P"
5206    }
5207    fn aliases(&self) -> &'static [&'static str] {
5208        &["COVAR"]
5209    }
5210    fn min_args(&self) -> usize {
5211        2
5212    }
5213    fn arg_schema(&self) -> &'static [ArgSchema] {
5214        &ARG_RANGE_NUM_LENIENT_ONE[..]
5215    }
5216    fn eval<'a, 'b, 'c>(
5217        &self,
5218        args: &'c [ArgumentHandle<'a, 'b>],
5219        _ctx: &dyn FunctionContext<'b>,
5220    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5221        let (y, x) = match collect_paired_arrays(args) {
5222            Ok(v) => v,
5223            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
5224        };
5225
5226        let n = x.len() as f64;
5227        let mean_x = x.iter().sum::<f64>() / n;
5228        let mean_y = y.iter().sum::<f64>() / n;
5229
5230        let mut sum_xy = 0.0;
5231        for i in 0..x.len() {
5232            let dx = x[i] - mean_x;
5233            let dy = y[i] - mean_y;
5234            sum_xy += dx * dy;
5235        }
5236
5237        // Population covariance divides by n
5238        let covar = sum_xy / n;
5239        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
5240            covar,
5241        )))
5242    }
5243}
5244
5245/// Returns sample covariance for two paired numeric data sets.
5246///
5247/// `COVARIANCE.S` measures joint variability using `n - 1` in the denominator.
5248///
5249/// # Remarks
5250/// - Arrays must contain paired numeric values with matching lengths.
5251/// - Requires at least two paired points.
5252/// - Returns `#DIV/0!` when fewer than two numeric pairs are available.
5253/// - Pairing and shape mismatches return spreadsheet errors from paired-array validation.
5254///
5255/// # Examples
5256///
5257/// ```yaml,sandbox
5258/// title: "Positive sample covariance"
5259/// formula: "=COVARIANCE.S({1,3,5},{2,4,6})"
5260/// expected: 4
5261/// ```
5262///
5263/// ```yaml,sandbox
5264/// title: "Negative sample covariance"
5265/// formula: "=COVARIANCE.S({1,2,3},{3,2,1})"
5266/// expected: -1
5267/// ```
5268#[derive(Debug)]
5269pub struct CovarianceSFn;
5270/// [formualizer-docgen:schema:start]
5271/// Name: COVARIANCE.S
5272/// Type: CovarianceSFn
5273/// Min args: 2
5274/// Max args: 1
5275/// Variadic: false
5276/// Signature: COVARIANCE.S(arg1: number@range)
5277/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
5278/// Caps: PURE, REDUCTION, NUMERIC_ONLY
5279/// [formualizer-docgen:schema:end]
5280impl Function for CovarianceSFn {
5281    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
5282    fn name(&self) -> &'static str {
5283        "COVARIANCE.S"
5284    }
5285    fn min_args(&self) -> usize {
5286        2
5287    }
5288    fn arg_schema(&self) -> &'static [ArgSchema] {
5289        &ARG_RANGE_NUM_LENIENT_ONE[..]
5290    }
5291    fn eval<'a, 'b, 'c>(
5292        &self,
5293        args: &'c [ArgumentHandle<'a, 'b>],
5294        _ctx: &dyn FunctionContext<'b>,
5295    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5296        let (y, x) = match collect_paired_arrays(args) {
5297            Ok(v) => v,
5298            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
5299        };
5300
5301        let n = x.len();
5302        if n < 2 {
5303            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5304                ExcelError::new_div(),
5305            )));
5306        }
5307
5308        let mean_x = x.iter().sum::<f64>() / n as f64;
5309        let mean_y = y.iter().sum::<f64>() / n as f64;
5310
5311        let mut sum_xy = 0.0;
5312        for i in 0..n {
5313            let dx = x[i] - mean_x;
5314            let dy = y[i] - mean_y;
5315            sum_xy += dx * dy;
5316        }
5317
5318        // Sample covariance divides by (n - 1)
5319        let covar = sum_xy / (n - 1) as f64;
5320        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
5321            covar,
5322        )))
5323    }
5324}
5325
5326/// Returns the Pearson correlation coefficient between two paired numeric arrays.
5327///
5328/// `PEARSON` reports linear association on a normalized scale from `-1` to `1`.
5329///
5330/// # Remarks
5331/// - Arrays must contain the same number of numeric observations.
5332/// - Returns `#DIV/0!` when either array has zero variance.
5333/// - Positive values indicate positive linear association; negative values indicate inverse association.
5334/// - Pairing and shape mismatches return spreadsheet errors from paired-array validation.
5335///
5336/// # Examples
5337///
5338/// ```yaml,sandbox
5339/// title: "Perfect positive linear correlation"
5340/// formula: "=PEARSON({1,2,3},{2,4,6})"
5341/// expected: 1
5342/// ```
5343///
5344/// ```yaml,sandbox
5345/// title: "Perfect negative linear correlation"
5346/// formula: "=PEARSON({1,2,3},{3,2,1})"
5347/// expected: -1
5348/// ```
5349#[derive(Debug)]
5350pub struct PearsonFn;
5351/// [formualizer-docgen:schema:start]
5352/// Name: PEARSON
5353/// Type: PearsonFn
5354/// Min args: 2
5355/// Max args: 1
5356/// Variadic: false
5357/// Signature: PEARSON(arg1: number@range)
5358/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
5359/// Caps: PURE, REDUCTION, NUMERIC_ONLY
5360/// [formualizer-docgen:schema:end]
5361impl Function for PearsonFn {
5362    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
5363    fn name(&self) -> &'static str {
5364        "PEARSON"
5365    }
5366    fn min_args(&self) -> usize {
5367        2
5368    }
5369    fn arg_schema(&self) -> &'static [ArgSchema] {
5370        &ARG_RANGE_NUM_LENIENT_ONE[..]
5371    }
5372    fn eval<'a, 'b, 'c>(
5373        &self,
5374        args: &'c [ArgumentHandle<'a, 'b>],
5375        _ctx: &dyn FunctionContext<'b>,
5376    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5377        let (y, x) = match collect_paired_arrays(args) {
5378            Ok(v) => v,
5379            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
5380        };
5381
5382        let n = x.len() as f64;
5383        let mean_x = x.iter().sum::<f64>() / n;
5384        let mean_y = y.iter().sum::<f64>() / n;
5385
5386        let mut sum_xy = 0.0;
5387        let mut sum_x2 = 0.0;
5388        let mut sum_y2 = 0.0;
5389
5390        for i in 0..x.len() {
5391            let dx = x[i] - mean_x;
5392            let dy = y[i] - mean_y;
5393            sum_xy += dx * dy;
5394            sum_x2 += dx * dx;
5395            sum_y2 += dy * dy;
5396        }
5397
5398        let denom = (sum_x2 * sum_y2).sqrt();
5399        if denom == 0.0 {
5400            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5401                ExcelError::new_div(),
5402            )));
5403        }
5404
5405        let correl = sum_xy / denom;
5406        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
5407            correl,
5408        )))
5409    }
5410}
5411
5412/// Returns the coefficient of determination (`R^2`) for paired x/y data.
5413///
5414/// `RSQ` is the square of Pearson correlation and indicates explained linear variance.
5415///
5416/// # Remarks
5417/// - Arrays must contain the same number of numeric observations.
5418/// - Result is in `[0, 1]` for valid numeric inputs.
5419/// - Returns `#DIV/0!` when either input array has zero variance.
5420/// - Pairing and shape mismatches return spreadsheet errors from paired-array validation.
5421///
5422/// # Examples
5423///
5424/// ```yaml,sandbox
5425/// title: "Perfect linear fit"
5426/// formula: "=RSQ({1,2,3},{2,4,6})"
5427/// expected: 1
5428/// ```
5429///
5430/// ```yaml,sandbox
5431/// title: "Strong but imperfect linear relationship"
5432/// formula: "=RSQ({1,2,3},{1,2,4})"
5433/// expected: 0.9642857142857143
5434/// ```
5435#[derive(Debug)]
5436pub struct RsqFn;
5437/// [formualizer-docgen:schema:start]
5438/// Name: RSQ
5439/// Type: RsqFn
5440/// Min args: 2
5441/// Max args: 1
5442/// Variadic: false
5443/// Signature: RSQ(arg1: number@range)
5444/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
5445/// Caps: PURE, REDUCTION, NUMERIC_ONLY
5446/// [formualizer-docgen:schema:end]
5447impl Function for RsqFn {
5448    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
5449    fn name(&self) -> &'static str {
5450        "RSQ"
5451    }
5452    fn min_args(&self) -> usize {
5453        2
5454    }
5455    fn arg_schema(&self) -> &'static [ArgSchema] {
5456        &ARG_RANGE_NUM_LENIENT_ONE[..]
5457    }
5458    fn eval<'a, 'b, 'c>(
5459        &self,
5460        args: &'c [ArgumentHandle<'a, 'b>],
5461        _ctx: &dyn FunctionContext<'b>,
5462    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5463        let (y, x) = match collect_paired_arrays(args) {
5464            Ok(v) => v,
5465            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
5466        };
5467
5468        let n = x.len() as f64;
5469        let mean_x = x.iter().sum::<f64>() / n;
5470        let mean_y = y.iter().sum::<f64>() / n;
5471
5472        let mut sum_xy = 0.0;
5473        let mut sum_x2 = 0.0;
5474        let mut sum_y2 = 0.0;
5475
5476        for i in 0..x.len() {
5477            let dx = x[i] - mean_x;
5478            let dy = y[i] - mean_y;
5479            sum_xy += dx * dy;
5480            sum_x2 += dx * dx;
5481            sum_y2 += dy * dy;
5482        }
5483
5484        let denom = sum_x2 * sum_y2;
5485        if denom == 0.0 {
5486            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5487                ExcelError::new_div(),
5488            )));
5489        }
5490
5491        // R-squared = r^2 = (sum_xy)^2 / (sum_x2 * sum_y2)
5492        let rsq = (sum_xy * sum_xy) / denom;
5493        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(rsq)))
5494    }
5495}
5496
5497/// Returns the standard error of y-estimates from a simple linear regression.
5498///
5499/// `STEYX` measures the typical residual size around the fitted regression line.
5500///
5501/// # Remarks
5502/// - Requires paired x/y inputs with matching numeric lengths.
5503/// - Requires at least three paired points.
5504/// - Returns `#DIV/0!` when `n < 3` or x-values have zero variance.
5505/// - Pairing and shape mismatches return spreadsheet errors from paired-array validation.
5506///
5507/// # Examples
5508///
5509/// ```yaml,sandbox
5510/// title: "Perfect linear fit has zero standard error"
5511/// formula: "=STEYX({2,4,6},{1,2,3})"
5512/// expected: 0
5513/// ```
5514///
5515/// ```yaml,sandbox
5516/// title: "Non-zero regression standard error"
5517/// formula: "=STEYX({2,5,7},{1,2,3})"
5518/// expected: 0.408248290463863
5519/// ```
5520#[derive(Debug)]
5521pub struct SteyxFn;
5522/// [formualizer-docgen:schema:start]
5523/// Name: STEYX
5524/// Type: SteyxFn
5525/// Min args: 2
5526/// Max args: 1
5527/// Variadic: false
5528/// Signature: STEYX(arg1: number@range)
5529/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
5530/// Caps: PURE, REDUCTION, NUMERIC_ONLY
5531/// [formualizer-docgen:schema:end]
5532impl Function for SteyxFn {
5533    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
5534    fn name(&self) -> &'static str {
5535        "STEYX"
5536    }
5537    fn min_args(&self) -> usize {
5538        2
5539    }
5540    fn arg_schema(&self) -> &'static [ArgSchema] {
5541        &ARG_RANGE_NUM_LENIENT_ONE[..]
5542    }
5543    fn eval<'a, 'b, 'c>(
5544        &self,
5545        args: &'c [ArgumentHandle<'a, 'b>],
5546        _ctx: &dyn FunctionContext<'b>,
5547    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5548        let (y, x) = match collect_paired_arrays(args) {
5549            Ok(v) => v,
5550            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
5551        };
5552
5553        let n = x.len();
5554        if n < 3 {
5555            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5556                ExcelError::new_div(),
5557            )));
5558        }
5559
5560        let n_f = n as f64;
5561        let mean_x = x.iter().sum::<f64>() / n_f;
5562        let mean_y = y.iter().sum::<f64>() / n_f;
5563
5564        let mut sum_xy = 0.0;
5565        let mut sum_x2 = 0.0;
5566        let mut sum_y2 = 0.0;
5567
5568        for i in 0..n {
5569            let dx = x[i] - mean_x;
5570            let dy = y[i] - mean_y;
5571            sum_xy += dx * dy;
5572            sum_x2 += dx * dx;
5573            sum_y2 += dy * dy;
5574        }
5575
5576        if sum_x2 == 0.0 {
5577            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5578                ExcelError::new_div(),
5579            )));
5580        }
5581
5582        // STEYX = sqrt((sum_y2 - (sum_xy)^2 / sum_x2) / (n - 2))
5583        let sse = sum_y2 - (sum_xy * sum_xy) / sum_x2;
5584        if sse < 0.0 {
5585            // This can happen due to floating point errors; return 0 in such case
5586            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0)));
5587        }
5588        let steyx = (sse / (n_f - 2.0)).sqrt();
5589        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
5590            steyx,
5591        )))
5592    }
5593}
5594
5595/* ─────────────────────────── SKEW ──────────────────────────── */
5596
5597/// Returns the sample skewness of a numeric distribution.
5598///
5599/// `SKEW` quantifies asymmetry: positive values indicate a longer right tail, negative values a
5600/// longer left tail.
5601///
5602/// # Remarks
5603/// - Requires at least three numeric values.
5604/// - Returns `#DIV/0!` when there are fewer than three numbers or zero sample standard deviation.
5605/// - Non-numeric values in ranges are ignored by statistical-collection rules.
5606/// - Uses the Excel-style sample skewness correction factor.
5607///
5608/// # Examples
5609///
5610/// ```yaml,sandbox
5611/// title: "Symmetric sample"
5612/// formula: "=SKEW({1,2,3})"
5613/// expected: 0
5614/// ```
5615///
5616/// ```yaml,sandbox
5617/// title: "Right-skewed sample"
5618/// formula: "=SKEW({1,1,2,10})"
5619/// expected: 1.9683567600862015
5620/// ```
5621#[derive(Debug)]
5622pub struct SkewFn;
5623/// [formualizer-docgen:schema:start]
5624/// Name: SKEW
5625/// Type: SkewFn
5626/// Min args: 1
5627/// Max args: variadic
5628/// Variadic: true
5629/// Signature: SKEW(arg1...: number@range)
5630/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
5631/// Caps: PURE, REDUCTION, NUMERIC_ONLY
5632/// [formualizer-docgen:schema:end]
5633impl Function for SkewFn {
5634    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
5635    fn name(&self) -> &'static str {
5636        "SKEW"
5637    }
5638    fn min_args(&self) -> usize {
5639        1
5640    }
5641    fn variadic(&self) -> bool {
5642        true
5643    }
5644    fn arg_schema(&self) -> &'static [ArgSchema] {
5645        &ARG_RANGE_NUM_LENIENT_ONE[..]
5646    }
5647    fn eval<'a, 'b, 'c>(
5648        &self,
5649        args: &'c [ArgumentHandle<'a, 'b>],
5650        _ctx: &dyn FunctionContext<'b>,
5651    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5652        let nums = collect_numeric_stats(args)?;
5653        let n = nums.len();
5654
5655        // SKEW requires at least 3 data points
5656        if n < 3 {
5657            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5658                ExcelError::new_div(),
5659            )));
5660        }
5661
5662        let n_f = n as f64;
5663        let mean = nums.iter().sum::<f64>() / n_f;
5664
5665        // Calculate sample standard deviation
5666        let mut sum_sq = 0.0;
5667        for &v in &nums {
5668            let d = v - mean;
5669            sum_sq += d * d;
5670        }
5671        let stdev = (sum_sq / (n_f - 1.0)).sqrt();
5672
5673        if stdev == 0.0 {
5674            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5675                ExcelError::new_div(),
5676            )));
5677        }
5678
5679        // Calculate sum of cubed deviations normalized by stdev
5680        let mut sum_cubed = 0.0;
5681        for &v in &nums {
5682            let d = (v - mean) / stdev;
5683            sum_cubed += d * d * d;
5684        }
5685
5686        // Excel SKEW formula: n / ((n-1)*(n-2)) * sum((xi - mean)/stdev)^3
5687        let skew = (n_f / ((n_f - 1.0) * (n_f - 2.0))) * sum_cubed;
5688        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(skew)))
5689    }
5690}
5691
5692/* ─────────────────────────── KURT ──────────────────────────── */
5693
5694/// Returns the sample excess kurtosis of a numeric distribution.
5695///
5696/// `KURT` indicates tail heaviness relative to a normal distribution after Excel-style sample
5697/// correction.
5698///
5699/// # Remarks
5700/// - Requires at least four numeric values.
5701/// - Returns `#DIV/0!` when there are fewer than four numbers or zero sample standard deviation.
5702/// - Positive values suggest heavier tails; negative values suggest lighter tails.
5703/// - Non-numeric values in ranges are ignored by statistical-collection rules.
5704///
5705/// # Examples
5706///
5707/// ```yaml,sandbox
5708/// title: "Uniformly spaced values"
5709/// formula: "=KURT({1,2,3,4})"
5710/// expected: -1.2
5711/// ```
5712///
5713/// ```yaml,sandbox
5714/// title: "Heavier-tail sample"
5715/// formula: "=KURT({1,1,1,2,10,10,10,10})"
5716/// expected: -2.3069755007920767
5717/// ```
5718#[derive(Debug)]
5719pub struct KurtFn;
5720/// [formualizer-docgen:schema:start]
5721/// Name: KURT
5722/// Type: KurtFn
5723/// Min args: 1
5724/// Max args: variadic
5725/// Variadic: true
5726/// Signature: KURT(arg1...: number@range)
5727/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
5728/// Caps: PURE, REDUCTION, NUMERIC_ONLY
5729/// [formualizer-docgen:schema:end]
5730impl Function for KurtFn {
5731    func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
5732    fn name(&self) -> &'static str {
5733        "KURT"
5734    }
5735    fn min_args(&self) -> usize {
5736        1
5737    }
5738    fn variadic(&self) -> bool {
5739        true
5740    }
5741    fn arg_schema(&self) -> &'static [ArgSchema] {
5742        &ARG_RANGE_NUM_LENIENT_ONE[..]
5743    }
5744    fn eval<'a, 'b, 'c>(
5745        &self,
5746        args: &'c [ArgumentHandle<'a, 'b>],
5747        _ctx: &dyn FunctionContext<'b>,
5748    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5749        let nums = collect_numeric_stats(args)?;
5750        let n = nums.len();
5751
5752        // KURT requires at least 4 data points
5753        if n < 4 {
5754            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5755                ExcelError::new_div(),
5756            )));
5757        }
5758
5759        let n_f = n as f64;
5760        let mean = nums.iter().sum::<f64>() / n_f;
5761
5762        // Calculate sample standard deviation
5763        let mut sum_sq = 0.0;
5764        for &v in &nums {
5765            let d = v - mean;
5766            sum_sq += d * d;
5767        }
5768        let stdev = (sum_sq / (n_f - 1.0)).sqrt();
5769
5770        if stdev == 0.0 {
5771            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5772                ExcelError::new_div(),
5773            )));
5774        }
5775
5776        // Calculate sum of fourth powers of deviations normalized by stdev
5777        let mut sum_fourth = 0.0;
5778        for &v in &nums {
5779            let d = (v - mean) / stdev;
5780            sum_fourth += d * d * d * d;
5781        }
5782
5783        // Excel KURT formula (excess kurtosis):
5784        // n*(n+1) / ((n-1)*(n-2)*(n-3)) * sum((xi - mean)/stdev)^4 - 3*(n-1)^2 / ((n-2)*(n-3))
5785        let term1 = (n_f * (n_f + 1.0)) / ((n_f - 1.0) * (n_f - 2.0) * (n_f - 3.0)) * sum_fourth;
5786        let term2 = (3.0 * (n_f - 1.0) * (n_f - 1.0)) / ((n_f - 2.0) * (n_f - 3.0));
5787        let kurt = term1 - term2;
5788        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(kurt)))
5789    }
5790}
5791
5792/* ─────────────────────────── FISHER ──────────────────────────── */
5793
5794/// Returns the Fisher z-transformation of a correlation-like value `x`.
5795///
5796/// `FISHER` maps `(-1, 1)` into `(-inf, +inf)` and is commonly used in correlation inference.
5797///
5798/// # Remarks
5799/// - Input must satisfy `-1 < x < 1`.
5800/// - Returns `#NUM!` when `x <= -1` or `x >= 1`.
5801/// - The transformation is `0.5 * ln((1 + x) / (1 - x))`.
5802/// - Invalid numeric coercions propagate as spreadsheet errors.
5803///
5804/// # Examples
5805///
5806/// ```yaml,sandbox
5807/// title: "Fisher transform at zero"
5808/// formula: "=FISHER(0)"
5809/// expected: 0
5810/// ```
5811///
5812/// ```yaml,sandbox
5813/// title: "Fisher transform at x=0.5"
5814/// formula: "=FISHER(0.5)"
5815/// expected: 0.5493061443340549
5816/// ```
5817#[derive(Debug)]
5818pub struct FisherFn;
5819/// [formualizer-docgen:schema:start]
5820/// Name: FISHER
5821/// Type: FisherFn
5822/// Min args: 1
5823/// Max args: 1
5824/// Variadic: false
5825/// Signature: FISHER(arg1: number@range)
5826/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
5827/// Caps: PURE, NUMERIC_ONLY
5828/// [formualizer-docgen:schema:end]
5829impl Function for FisherFn {
5830    func_caps!(PURE, NUMERIC_ONLY);
5831    fn name(&self) -> &'static str {
5832        "FISHER"
5833    }
5834    fn min_args(&self) -> usize {
5835        1
5836    }
5837    fn arg_schema(&self) -> &'static [ArgSchema] {
5838        &ARG_RANGE_NUM_LENIENT_ONE[..]
5839    }
5840    fn eval<'a, 'b, 'c>(
5841        &self,
5842        args: &'c [ArgumentHandle<'a, 'b>],
5843        _ctx: &dyn FunctionContext<'b>,
5844    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5845        let x = coerce_num(&scalar_like_value(&args[0])?)?;
5846
5847        // FISHER requires -1 < x < 1
5848        if x <= -1.0 || x >= 1.0 {
5849            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5850                ExcelError::new_num(),
5851            )));
5852        }
5853
5854        // Fisher transformation: 0.5 * ln((1 + x) / (1 - x))
5855        let fisher = 0.5 * ((1.0 + x) / (1.0 - x)).ln();
5856        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
5857            fisher,
5858        )))
5859    }
5860}
5861
5862/* ─────────────────────────── FISHERINV ──────────────────────────── */
5863
5864/// Returns the inverse Fisher transformation of `y`.
5865///
5866/// `FISHERINV` maps Fisher z-values back to the open interval `(-1, 1)`.
5867///
5868/// # Remarks
5869/// - The inverse form is `(e^(2y) - 1) / (e^(2y) + 1)`.
5870/// - Output is always strictly between `-1` and `1` for finite inputs.
5871/// - This function is useful for converting transformed correlation estimates back to r-space.
5872/// - Invalid numeric coercions propagate as spreadsheet errors.
5873///
5874/// # Examples
5875///
5876/// ```yaml,sandbox
5877/// title: "Inverse Fisher at zero"
5878/// formula: "=FISHERINV(0)"
5879/// expected: 0
5880/// ```
5881///
5882/// ```yaml,sandbox
5883/// title: "Round-trip with FISHER(0.5)"
5884/// formula: "=FISHERINV(0.5493061443340549)"
5885/// expected: 0.5
5886/// ```
5887#[derive(Debug)]
5888pub struct FisherInvFn;
5889/// [formualizer-docgen:schema:start]
5890/// Name: FISHERINV
5891/// Type: FisherInvFn
5892/// Min args: 1
5893/// Max args: 1
5894/// Variadic: false
5895/// Signature: FISHERINV(arg1: number@range)
5896/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
5897/// Caps: PURE, NUMERIC_ONLY
5898/// [formualizer-docgen:schema:end]
5899impl Function for FisherInvFn {
5900    func_caps!(PURE, NUMERIC_ONLY);
5901    fn name(&self) -> &'static str {
5902        "FISHERINV"
5903    }
5904    fn min_args(&self) -> usize {
5905        1
5906    }
5907    fn arg_schema(&self) -> &'static [ArgSchema] {
5908        &ARG_RANGE_NUM_LENIENT_ONE[..]
5909    }
5910    fn eval<'a, 'b, 'c>(
5911        &self,
5912        args: &'c [ArgumentHandle<'a, 'b>],
5913        _ctx: &dyn FunctionContext<'b>,
5914    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5915        let y = coerce_num(&scalar_like_value(&args[0])?)?;
5916
5917        // Inverse Fisher transformation: (e^(2y) - 1) / (e^(2y) + 1)
5918        let e2y = (2.0 * y).exp();
5919        let fisherinv = (e2y - 1.0) / (e2y + 1.0);
5920        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
5921            fisherinv,
5922        )))
5923    }
5924}
5925
5926/* ─────────────────────────── FORECAST.LINEAR ──────────────────────────── */
5927
5928/// Returns a predicted y-value at `x` from simple linear regression over known data.
5929///
5930/// `FORECAST.LINEAR` fits `y = intercept + slope * x` and evaluates that line at the requested x.
5931///
5932/// # Remarks
5933/// - Requires `known_y` and `known_x` arrays with the same numeric length.
5934/// - Returns `#N/A` when arrays are empty or lengths do not match.
5935/// - Returns `#DIV/0!` when `known_x` has zero variance.
5936/// - Alias `FORECAST` is supported.
5937///
5938/// # Examples
5939///
5940/// ```yaml,sandbox
5941/// title: "Predict next point on a perfect line"
5942/// formula: "=FORECAST.LINEAR(4,{2,4,6},{1,2,3})"
5943/// expected: 8
5944/// ```
5945///
5946/// ```yaml,sandbox
5947/// title: "Forecast with non-zero intercept"
5948/// formula: "=FORECAST.LINEAR(5,{3,5,7},{1,2,3})"
5949/// expected: 11
5950/// ```
5951#[derive(Debug)]
5952pub struct ForecastLinearFn;
5953/// [formualizer-docgen:schema:start]
5954/// Name: FORECAST.LINEAR
5955/// Type: ForecastLinearFn
5956/// Min args: 3
5957/// Max args: 1
5958/// Variadic: false
5959/// Signature: FORECAST.LINEAR(arg1: number@range)
5960/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
5961/// Caps: PURE, NUMERIC_ONLY
5962/// [formualizer-docgen:schema:end]
5963impl Function for ForecastLinearFn {
5964    func_caps!(PURE, NUMERIC_ONLY);
5965    fn name(&self) -> &'static str {
5966        "FORECAST.LINEAR"
5967    }
5968    fn aliases(&self) -> &'static [&'static str] {
5969        &["FORECAST"]
5970    }
5971    fn min_args(&self) -> usize {
5972        3
5973    }
5974    fn arg_schema(&self) -> &'static [ArgSchema] {
5975        &ARG_RANGE_NUM_LENIENT_ONE[..]
5976    }
5977    fn eval<'a, 'b, 'c>(
5978        &self,
5979        args: &'c [ArgumentHandle<'a, 'b>],
5980        _ctx: &dyn FunctionContext<'b>,
5981    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5982        // args[0] = x value to forecast
5983        // args[1] = known_y's
5984        // args[2] = known_x's
5985        let x = match coerce_num(&scalar_like_value(&args[0])?) {
5986            Ok(n) => n,
5987            Err(_) => {
5988                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5989                    ExcelError::new_value(),
5990                )));
5991            }
5992        };
5993
5994        let y_vals = collect_numeric_stats(&args[1..2])?;
5995        let x_vals = collect_numeric_stats(&args[2..3])?;
5996
5997        // Arrays must have same length
5998        if y_vals.len() != x_vals.len() {
5999            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6000                ExcelError::new_na(),
6001            )));
6002        }
6003
6004        if y_vals.is_empty() {
6005            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6006                ExcelError::new_na(),
6007            )));
6008        }
6009
6010        let n = x_vals.len() as f64;
6011        let mean_x = x_vals.iter().sum::<f64>() / n;
6012        let mean_y = y_vals.iter().sum::<f64>() / n;
6013
6014        let mut sum_xy = 0.0;
6015        let mut sum_x2 = 0.0;
6016
6017        for i in 0..x_vals.len() {
6018            let dx = x_vals[i] - mean_x;
6019            let dy = y_vals[i] - mean_y;
6020            sum_xy += dx * dy;
6021            sum_x2 += dx * dx;
6022        }
6023
6024        if sum_x2 == 0.0 {
6025            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6026                ExcelError::new_div(),
6027            )));
6028        }
6029
6030        let slope = sum_xy / sum_x2;
6031        let intercept = mean_y - slope * mean_x;
6032        let forecast = intercept + slope * x;
6033
6034        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
6035            forecast,
6036        )))
6037    }
6038}
6039
6040/* ─────────────────────────── LINEST ──────────────────────────── */
6041
6042/// Returns linear-regression coefficients and optional fit statistics.
6043///
6044/// `LINEST` fits a straight line to known y/x pairs and returns either `[slope, intercept]` or a
6045/// larger statistics matrix.
6046///
6047/// # Remarks
6048/// - `known_y` is required; `known_x` defaults to `1..n` when omitted.
6049/// - `const` controls whether an intercept is fitted (`TRUE` by default).
6050/// - `stats=TRUE` returns a `5x2` result block; otherwise it returns `1x2`.
6051/// - Returns spreadsheet errors for mismatched lengths, empty data, or degenerate x-values.
6052///
6053/// # Examples
6054///
6055/// ```yaml,sandbox
6056/// title: "Slope and intercept only"
6057/// formula: "=LINEST({2,4,6},{1,2,3})"
6058/// expected:
6059///   - [2, 0]
6060/// ```
6061///
6062/// ```yaml,sandbox
6063/// title: "Linear fit with non-zero intercept"
6064/// formula: "=LINEST({3,5,7},{1,2,3})"
6065/// expected:
6066///   - [2, 1]
6067/// ```
6068#[derive(Debug)]
6069pub struct LinestFn;
6070/// [formualizer-docgen:schema:start]
6071/// Name: LINEST
6072/// Type: LinestFn
6073/// Min args: 1
6074/// Max args: variadic
6075/// Variadic: true
6076/// Signature: LINEST(arg1...: number@range)
6077/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
6078/// Caps: PURE, NUMERIC_ONLY
6079/// [formualizer-docgen:schema:end]
6080impl Function for LinestFn {
6081    func_caps!(PURE, NUMERIC_ONLY);
6082    fn name(&self) -> &'static str {
6083        "LINEST"
6084    }
6085    fn min_args(&self) -> usize {
6086        1
6087    }
6088    fn variadic(&self) -> bool {
6089        true
6090    }
6091    fn arg_schema(&self) -> &'static [ArgSchema] {
6092        &ARG_RANGE_NUM_LENIENT_ONE[..]
6093    }
6094    fn eval<'a, 'b, 'c>(
6095        &self,
6096        args: &'c [ArgumentHandle<'a, 'b>],
6097        _ctx: &dyn FunctionContext<'b>,
6098    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
6099        // args[0] = known_y's (required)
6100        // args[1] = known_x's (optional, defaults to {1,2,3,...})
6101        // args[2] = const (optional, default TRUE - whether to compute intercept)
6102        // args[3] = stats (optional, default FALSE - whether to return additional statistics)
6103
6104        let y_vals = collect_numeric_stats(&args[0..1])?;
6105
6106        if y_vals.is_empty() {
6107            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6108                ExcelError::new_na(),
6109            )));
6110        }
6111
6112        // Get known_x's or generate default {1, 2, 3, ...}
6113        let x_vals = if args.len() >= 2 {
6114            collect_numeric_stats(&args[1..2])?
6115        } else {
6116            (1..=y_vals.len()).map(|i| i as f64).collect()
6117        };
6118
6119        // Arrays must have same length
6120        if y_vals.len() != x_vals.len() {
6121            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6122                ExcelError::new_ref(),
6123            )));
6124        }
6125
6126        // Parse const argument (default TRUE)
6127        let use_const = if args.len() >= 3 {
6128            match scalar_like_value(&args[2])? {
6129                LiteralValue::Boolean(b) => b,
6130                LiteralValue::Number(n) => n != 0.0,
6131                LiteralValue::Int(i) => i != 0,
6132                _ => true,
6133            }
6134        } else {
6135            true
6136        };
6137
6138        // Parse stats argument (default FALSE)
6139        let return_stats = if args.len() >= 4 {
6140            match scalar_like_value(&args[3])? {
6141                LiteralValue::Boolean(b) => b,
6142                LiteralValue::Number(n) => n != 0.0,
6143                LiteralValue::Int(i) => i != 0,
6144                _ => false,
6145            }
6146        } else {
6147            false
6148        };
6149
6150        let n = x_vals.len() as f64;
6151
6152        // Calculate regression coefficients
6153        let (slope, intercept) = if use_const {
6154            // Normal linear regression with intercept
6155            let mean_x = x_vals.iter().sum::<f64>() / n;
6156            let mean_y = y_vals.iter().sum::<f64>() / n;
6157
6158            let mut sum_xy = 0.0;
6159            let mut sum_x2 = 0.0;
6160
6161            for i in 0..x_vals.len() {
6162                let dx = x_vals[i] - mean_x;
6163                let dy = y_vals[i] - mean_y;
6164                sum_xy += dx * dy;
6165                sum_x2 += dx * dx;
6166            }
6167
6168            if sum_x2 == 0.0 {
6169                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6170                    ExcelError::new_div(),
6171                )));
6172            }
6173
6174            let slope = sum_xy / sum_x2;
6175            let intercept = mean_y - slope * mean_x;
6176            (slope, intercept)
6177        } else {
6178            // Regression through origin (intercept = 0)
6179            let mut sum_xy = 0.0;
6180            let mut sum_x2 = 0.0;
6181
6182            for i in 0..x_vals.len() {
6183                sum_xy += x_vals[i] * y_vals[i];
6184                sum_x2 += x_vals[i] * x_vals[i];
6185            }
6186
6187            if sum_x2 == 0.0 {
6188                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6189                    ExcelError::new_div(),
6190                )));
6191            }
6192
6193            let slope = sum_xy / sum_x2;
6194            (slope, 0.0)
6195        };
6196
6197        if !return_stats {
6198            // Return just slope and intercept as 1x2 array: [[slope, intercept]]
6199            let row = vec![LiteralValue::Number(slope), LiteralValue::Number(intercept)];
6200            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Array(vec![
6201                row,
6202            ])));
6203        }
6204
6205        // Calculate additional statistics for stats=TRUE
6206        // Row 1: [slope, intercept]
6207        // Row 2: [se_slope, se_intercept]
6208        // Row 3: [r_squared, se_y]
6209        // Row 4: [F_statistic, df]
6210        // Row 5: [ss_reg, ss_resid]
6211
6212        let mean_y = y_vals.iter().sum::<f64>() / n;
6213
6214        // Calculate residuals and sums of squares
6215        let mut ss_resid = 0.0; // Sum of squared residuals
6216        let mut ss_tot = 0.0; // Total sum of squares
6217
6218        for i in 0..x_vals.len() {
6219            let y_pred = slope * x_vals[i] + intercept;
6220            let residual = y_vals[i] - y_pred;
6221            ss_resid += residual * residual;
6222            let dy_tot = y_vals[i] - mean_y;
6223            ss_tot += dy_tot * dy_tot;
6224        }
6225
6226        let ss_reg = ss_tot - ss_resid; // Regression sum of squares
6227
6228        // R-squared
6229        let r_squared = if ss_tot == 0.0 {
6230            1.0 // Perfect fit or all y values are the same
6231        } else {
6232            1.0 - (ss_resid / ss_tot)
6233        };
6234
6235        // Degrees of freedom
6236        let df = if use_const {
6237            (n as i64 - 2).max(1) as f64 // n - k - 1 where k=1 (one predictor)
6238        } else {
6239            (n as i64 - 1).max(1) as f64 // n - k when no intercept
6240        };
6241
6242        // Standard error of y estimate
6243        let se_y = if df > 0.0 {
6244            (ss_resid / df).sqrt()
6245        } else {
6246            0.0
6247        };
6248
6249        // Standard errors of coefficients
6250        let mean_x = x_vals.iter().sum::<f64>() / n;
6251        let mut sum_x2_centered = 0.0;
6252        let mut sum_x2_raw = 0.0;
6253        for &xi in &x_vals {
6254            sum_x2_centered += (xi - mean_x).powi(2);
6255            sum_x2_raw += xi * xi;
6256        }
6257
6258        let se_slope = if sum_x2_centered > 0.0 && df > 0.0 {
6259            se_y / sum_x2_centered.sqrt()
6260        } else {
6261            f64::NAN
6262        };
6263
6264        let se_intercept = if use_const && sum_x2_centered > 0.0 && df > 0.0 {
6265            se_y * (sum_x2_raw / (n * sum_x2_centered)).sqrt()
6266        } else {
6267            f64::NAN
6268        };
6269
6270        // F-statistic
6271        let f_stat = if ss_resid > 0.0 && df > 0.0 {
6272            (ss_reg / 1.0) / (ss_resid / df) // MSR / MSE
6273        } else if ss_resid == 0.0 {
6274            f64::INFINITY // Perfect fit
6275        } else {
6276            f64::NAN
6277        };
6278
6279        // Build 5x2 result array
6280        let rows = vec![
6281            vec![LiteralValue::Number(slope), LiteralValue::Number(intercept)],
6282            vec![
6283                LiteralValue::Number(se_slope),
6284                LiteralValue::Number(se_intercept),
6285            ],
6286            vec![LiteralValue::Number(r_squared), LiteralValue::Number(se_y)],
6287            vec![LiteralValue::Number(f_stat), LiteralValue::Number(df)],
6288            vec![LiteralValue::Number(ss_reg), LiteralValue::Number(ss_resid)],
6289        ];
6290
6291        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Array(rows)))
6292    }
6293}
6294
6295/* ─────────────────────────── CONFIDENCE.NORM ──────────────────────────── */
6296
6297/// Returns the half-width of a confidence interval using a normal critical value.
6298///
6299/// `CONFIDENCE.NORM` computes `z_crit * standard_dev / sqrt(size)` for two-sided intervals.
6300///
6301/// # Remarks
6302/// - `alpha` must satisfy `0 < alpha < 1`.
6303/// - `standard_dev` must be greater than `0`.
6304/// - `size` must be at least `1`.
6305/// - Returns `#NUM!` when any input is outside valid bounds.
6306///
6307/// # Examples
6308///
6309/// ```yaml,sandbox
6310/// title: "95% confidence half-width"
6311/// formula: "=CONFIDENCE.NORM(0.05,2,100)"
6312/// expected: 0.3919927977622559
6313/// ```
6314///
6315/// ```yaml,sandbox
6316/// title: "90% confidence half-width"
6317/// formula: "=CONFIDENCE.NORM(0.1,5,25)"
6318/// expected: 1.644853625133699
6319/// ```
6320#[derive(Debug)]
6321pub struct ConfidenceNormFn;
6322/// [formualizer-docgen:schema:start]
6323/// Name: CONFIDENCE.NORM
6324/// Type: ConfidenceNormFn
6325/// Min args: 3
6326/// Max args: 3
6327/// Variadic: false
6328/// Signature: CONFIDENCE.NORM(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
6329/// 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}
6330/// Caps: PURE
6331/// [formualizer-docgen:schema:end]
6332impl Function for ConfidenceNormFn {
6333    func_caps!(PURE);
6334    fn name(&self) -> &'static str {
6335        "CONFIDENCE.NORM"
6336    }
6337    fn aliases(&self) -> &'static [&'static str] {
6338        &["CONFIDENCE"]
6339    }
6340    fn min_args(&self) -> usize {
6341        3
6342    }
6343    fn arg_schema(&self) -> &'static [ArgSchema] {
6344        use std::sync::LazyLock;
6345        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
6346            vec![
6347                ArgSchema::number_lenient_scalar(),
6348                ArgSchema::number_lenient_scalar(),
6349                ArgSchema::number_lenient_scalar(),
6350            ]
6351        });
6352        &SCHEMA[..]
6353    }
6354    fn eval<'a, 'b, 'c>(
6355        &self,
6356        args: &'c [ArgumentHandle<'a, 'b>],
6357        _ctx: &dyn FunctionContext<'b>,
6358    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
6359        let alpha = coerce_num(&scalar_like_value(&args[0])?)?;
6360        let std_dev = coerce_num(&scalar_like_value(&args[1])?)?;
6361        let size = coerce_num(&scalar_like_value(&args[2])?)?;
6362
6363        // Validate inputs
6364        if alpha <= 0.0 || alpha >= 1.0 {
6365            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6366                ExcelError::new_num(),
6367            )));
6368        }
6369        if std_dev <= 0.0 {
6370            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6371                ExcelError::new_num(),
6372            )));
6373        }
6374        if size < 1.0 {
6375            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6376                ExcelError::new_num(),
6377            )));
6378        }
6379
6380        // z_crit = NORM.S.INV(1 - alpha/2)
6381        let z_crit = match std_norm_inv(1.0 - alpha / 2.0) {
6382            Some(z) => z,
6383            None => {
6384                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6385                    ExcelError::new_num(),
6386                )));
6387            }
6388        };
6389
6390        let result = z_crit * std_dev / size.sqrt();
6391        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
6392            result,
6393        )))
6394    }
6395}
6396
6397/* ─────────────────────────── CONFIDENCE.T ──────────────────────────── */
6398
6399/// Returns the half-width of a confidence interval using a t critical value.
6400///
6401/// `CONFIDENCE.T` is typically used when population standard deviation is unknown and sample size
6402/// is limited.
6403///
6404/// # Remarks
6405/// - `alpha` must satisfy `0 < alpha < 1`.
6406/// - `standard_dev` must be greater than `0`.
6407/// - `size` must be at least `2` so that `df = size - 1` is valid.
6408/// - Returns `#NUM!` when inputs are outside valid bounds.
6409///
6410/// # Examples
6411///
6412/// ```yaml,sandbox
6413/// title: "95% t-interval half-width"
6414/// formula: "=CONFIDENCE.T(0.05,2,25)"
6415/// expected: 0.8256636934020788
6416/// ```
6417///
6418/// ```yaml,sandbox
6419/// title: "90% t-interval half-width"
6420/// formula: "=CONFIDENCE.T(0.1,5,10)"
6421/// expected: 2.9158049866307585
6422/// ```
6423#[derive(Debug)]
6424pub struct ConfidenceTFn;
6425/// [formualizer-docgen:schema:start]
6426/// Name: CONFIDENCE.T
6427/// Type: ConfidenceTFn
6428/// Min args: 3
6429/// Max args: 3
6430/// Variadic: false
6431/// Signature: CONFIDENCE.T(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
6432/// 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}
6433/// Caps: PURE
6434/// [formualizer-docgen:schema:end]
6435impl Function for ConfidenceTFn {
6436    func_caps!(PURE);
6437    fn name(&self) -> &'static str {
6438        "CONFIDENCE.T"
6439    }
6440    fn min_args(&self) -> usize {
6441        3
6442    }
6443    fn arg_schema(&self) -> &'static [ArgSchema] {
6444        use std::sync::LazyLock;
6445        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
6446            vec![
6447                ArgSchema::number_lenient_scalar(),
6448                ArgSchema::number_lenient_scalar(),
6449                ArgSchema::number_lenient_scalar(),
6450            ]
6451        });
6452        &SCHEMA[..]
6453    }
6454    fn eval<'a, 'b, 'c>(
6455        &self,
6456        args: &'c [ArgumentHandle<'a, 'b>],
6457        _ctx: &dyn FunctionContext<'b>,
6458    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
6459        let alpha = coerce_num(&scalar_like_value(&args[0])?)?;
6460        let std_dev = coerce_num(&scalar_like_value(&args[1])?)?;
6461        let size = coerce_num(&scalar_like_value(&args[2])?)?;
6462
6463        // Validate inputs - size must be >= 2 for t-distribution (df = size - 1 >= 1)
6464        if alpha <= 0.0 || alpha >= 1.0 {
6465            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6466                ExcelError::new_num(),
6467            )));
6468        }
6469        if std_dev <= 0.0 {
6470            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6471                ExcelError::new_num(),
6472            )));
6473        }
6474        if size < 2.0 {
6475            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6476                ExcelError::new_num(),
6477            )));
6478        }
6479
6480        let df = size - 1.0;
6481
6482        // t_crit = T.INV(1 - alpha/2, df)
6483        let t_crit = match t_inv(1.0 - alpha / 2.0, df) {
6484            Some(t) => t,
6485            None => {
6486                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6487                    ExcelError::new_num(),
6488                )));
6489            }
6490        };
6491
6492        let result = t_crit * std_dev / size.sqrt();
6493        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
6494            result,
6495        )))
6496    }
6497}
6498
6499/* ─────────────────────────── Z.TEST ──────────────────────────── */
6500
6501/// Returns the one-tailed p-value of a z-test against hypothesized mean `x`.
6502///
6503/// `Z.TEST` evaluates whether the sample mean is significantly greater than the target value.
6504///
6505/// # Remarks
6506/// - Uses provided `sigma` when supplied; otherwise computes population standard deviation.
6507/// - Returns `#NUM!` when `sigma <= 0`.
6508/// - Returns `#DIV/0!` when implied standard deviation is zero.
6509/// - Returns `#N/A` when the data array has no numeric values.
6510///
6511/// # Examples
6512///
6513/// ```yaml,sandbox
6514/// title: "Z-test with provided sigma"
6515/// formula: "=Z.TEST({1,2,3,4,5},2,1)"
6516/// expected: 0.012673659338734137
6517/// ```
6518///
6519/// ```yaml,sandbox
6520/// title: "Z-test with sigma estimated from sample"
6521/// formula: "=Z.TEST({1,2,3,4,5},2)"
6522/// expected: 0.056923149003329065
6523/// ```
6524#[derive(Debug)]
6525pub struct ZTestFn;
6526/// [formualizer-docgen:schema:start]
6527/// Name: Z.TEST
6528/// Type: ZTestFn
6529/// Min args: 2
6530/// Max args: variadic
6531/// Variadic: true
6532/// Signature: Z.TEST(arg1: number@range, arg2: number@scalar, arg3...: number@scalar)
6533/// 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}
6534/// Caps: PURE
6535/// [formualizer-docgen:schema:end]
6536impl Function for ZTestFn {
6537    func_caps!(PURE);
6538    fn name(&self) -> &'static str {
6539        "Z.TEST"
6540    }
6541    fn aliases(&self) -> &'static [&'static str] {
6542        &["ZTEST"]
6543    }
6544    fn min_args(&self) -> usize {
6545        2
6546    }
6547    fn variadic(&self) -> bool {
6548        true
6549    }
6550    fn arg_schema(&self) -> &'static [ArgSchema] {
6551        use std::sync::LazyLock;
6552        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
6553            vec![
6554                {
6555                    let mut s = ArgSchema::number_lenient_scalar();
6556                    s.shape = crate::args::ShapeKind::Range;
6557                    s.coercion = formualizer_common::CoercionPolicy::NumberLenientText;
6558                    s
6559                },
6560                ArgSchema::number_lenient_scalar(),
6561                ArgSchema::number_lenient_scalar(), // optional sigma
6562            ]
6563        });
6564        &SCHEMA[..]
6565    }
6566    fn eval<'a, 'b, 'c>(
6567        &self,
6568        args: &'c [ArgumentHandle<'a, 'b>],
6569        _ctx: &dyn FunctionContext<'b>,
6570    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
6571        // Collect numeric values from the array argument
6572        let data = collect_numeric_stats(&args[0..1])?;
6573
6574        if data.is_empty() {
6575            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6576                ExcelError::new_na(),
6577            )));
6578        }
6579
6580        let x = coerce_num(&scalar_like_value(&args[1])?)?;
6581
6582        let n = data.len() as f64;
6583        let mean: f64 = data.iter().sum::<f64>() / n;
6584
6585        // Calculate sigma: use provided value or compute population std dev
6586        let sigma = if args.len() > 2 {
6587            let s = coerce_num(&scalar_like_value(&args[2])?)?;
6588            if s <= 0.0 {
6589                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6590                    ExcelError::new_num(),
6591                )));
6592            }
6593            s
6594        } else {
6595            // Population standard deviation
6596            let variance: f64 = data.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / n;
6597            let std_dev = variance.sqrt();
6598            if std_dev == 0.0 {
6599                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6600                    ExcelError::new_div(),
6601                )));
6602            }
6603            std_dev
6604        };
6605
6606        // z = (mean - x) / (sigma / sqrt(n))
6607        let z = (mean - x) / (sigma / n.sqrt());
6608
6609        // P-value = 1 - NORM.S.DIST(z, TRUE)
6610        let p_value = 1.0 - std_norm_cdf(z);
6611
6612        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
6613            p_value,
6614        )))
6615    }
6616}
6617
6618/* ─────────────────────────── TREND ──────────────────────────── */
6619
6620/// Returns fitted y-values along a linear trend derived from known data.
6621///
6622/// `TREND` performs simple linear regression and returns predictions for `new_x` (or defaults).
6623///
6624/// # Remarks
6625/// - `known_y` is required; `known_x` defaults to `1..n` when omitted.
6626/// - `new_x` defaults to `known_x` when omitted.
6627/// - `const` defaults to `TRUE`; set to `FALSE` to force a zero intercept.
6628/// - Returns spreadsheet errors for empty data, mismatched lengths, or degenerate x-variance.
6629///
6630/// # Examples
6631///
6632/// ```yaml,sandbox
6633/// title: "Predict two future points on a line"
6634/// formula: "=TREND({2,4,6},{1,2,3},{4,5})"
6635/// expected:
6636///   - [8, 10]
6637/// ```
6638///
6639/// ```yaml,sandbox
6640/// title: "Default x-values with fitted trend"
6641/// formula: "=TREND({3,5,7})"
6642/// expected:
6643///   - [3, 5, 7]
6644/// ```
6645#[derive(Debug)]
6646pub struct TrendFn;
6647/// [formualizer-docgen:schema:start]
6648/// Name: TREND
6649/// Type: TrendFn
6650/// Min args: 1
6651/// Max args: variadic
6652/// Variadic: true
6653/// Signature: TREND(arg1...: number@range)
6654/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
6655/// Caps: PURE, NUMERIC_ONLY
6656/// [formualizer-docgen:schema:end]
6657impl Function for TrendFn {
6658    func_caps!(PURE, NUMERIC_ONLY);
6659    fn name(&self) -> &'static str {
6660        "TREND"
6661    }
6662    fn min_args(&self) -> usize {
6663        1
6664    }
6665    fn variadic(&self) -> bool {
6666        true
6667    }
6668    fn arg_schema(&self) -> &'static [ArgSchema] {
6669        &ARG_RANGE_NUM_LENIENT_ONE[..]
6670    }
6671    fn eval<'a, 'b, 'c>(
6672        &self,
6673        args: &'c [ArgumentHandle<'a, 'b>],
6674        _ctx: &dyn FunctionContext<'b>,
6675    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
6676        // TREND: args[0] = known_y's (required)
6677        // args[1] = known_x's (optional, defaults to {1,2,3,...})
6678        // args[2] = new_x's (optional, defaults to known_x's)
6679        // args[3] = const (optional, default TRUE - whether to compute intercept)
6680
6681        let y_vals = collect_numeric_stats(&args[0..1])?;
6682
6683        if y_vals.is_empty() {
6684            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6685                ExcelError::new_na(),
6686            )));
6687        }
6688
6689        // Helper to check if argument is empty/omitted
6690        // Note: Empty arguments are represented as empty text strings by the parser
6691        fn is_arg_empty(arg: &ArgumentHandle) -> bool {
6692            match scalar_like_value(arg) {
6693                Ok(LiteralValue::Empty) => true,
6694                Ok(LiteralValue::Text(s)) if s.is_empty() => true,
6695                _ => false,
6696            }
6697        }
6698
6699        // Get known_x's or generate default {1, 2, 3, ...}
6700        let x_vals = if args.len() >= 2 && !is_arg_empty(&args[1]) {
6701            collect_numeric_stats(&args[1..2])?
6702        } else {
6703            (1..=y_vals.len()).map(|i| i as f64).collect()
6704        };
6705
6706        // Arrays must have same length
6707        if y_vals.len() != x_vals.len() {
6708            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6709                ExcelError::new_ref(),
6710            )));
6711        }
6712
6713        // Get new_x's or use known_x's - check if argument is empty/omitted
6714        let new_x_vals = if args.len() >= 3 && !is_arg_empty(&args[2]) {
6715            collect_numeric_stats(&args[2..3])?
6716        } else {
6717            x_vals.clone()
6718        };
6719
6720        if new_x_vals.is_empty() {
6721            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6722                ExcelError::new_na(),
6723            )));
6724        }
6725
6726        // Parse const argument (default TRUE)
6727        let use_const = if args.len() >= 4 {
6728            match scalar_like_value(&args[3])? {
6729                LiteralValue::Boolean(b) => b,
6730                LiteralValue::Number(n) => n != 0.0,
6731                LiteralValue::Int(i) => i != 0,
6732                LiteralValue::Empty => true, // empty defaults to TRUE
6733                _ => true,
6734            }
6735        } else {
6736            true
6737        };
6738
6739        let n = x_vals.len() as f64;
6740
6741        // Calculate regression coefficients
6742        let (slope, intercept) = if use_const {
6743            // Normal linear regression with intercept
6744            let mean_x = x_vals.iter().sum::<f64>() / n;
6745            let mean_y = y_vals.iter().sum::<f64>() / n;
6746
6747            let mut sum_xy = 0.0;
6748            let mut sum_x2 = 0.0;
6749
6750            for i in 0..x_vals.len() {
6751                let dx = x_vals[i] - mean_x;
6752                let dy = y_vals[i] - mean_y;
6753                sum_xy += dx * dy;
6754                sum_x2 += dx * dx;
6755            }
6756
6757            if sum_x2 == 0.0 {
6758                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6759                    ExcelError::new_div(),
6760                )));
6761            }
6762
6763            let slope = sum_xy / sum_x2;
6764            let intercept = mean_y - slope * mean_x;
6765            (slope, intercept)
6766        } else {
6767            // Regression through origin (intercept = 0)
6768            let mut sum_xy = 0.0;
6769            let mut sum_x2 = 0.0;
6770
6771            for i in 0..x_vals.len() {
6772                sum_xy += x_vals[i] * y_vals[i];
6773                sum_x2 += x_vals[i] * x_vals[i];
6774            }
6775
6776            if sum_x2 == 0.0 {
6777                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6778                    ExcelError::new_div(),
6779                )));
6780            }
6781
6782            let slope = sum_xy / sum_x2;
6783            (slope, 0.0)
6784        };
6785
6786        // Calculate predicted y values for new_x's
6787        let predicted: Vec<LiteralValue> = new_x_vals
6788            .iter()
6789            .map(|&x| LiteralValue::Number(slope * x + intercept))
6790            .collect();
6791
6792        // Return as 1xN array (row vector)
6793        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Array(vec![
6794            predicted,
6795        ])))
6796    }
6797}
6798
6799/* ─────────────────────────── GROWTH ──────────────────────────── */
6800
6801/// Returns fitted values from an exponential trend model.
6802///
6803/// `GROWTH` fits `y = b * m^x` by linearizing in log space, then returns predictions for `new_x`.
6804///
6805/// # Remarks
6806/// - All known y-values must be strictly greater than `0`.
6807/// - `known_x` defaults to `1..n`; `new_x` defaults to `known_x`.
6808/// - `const` defaults to `TRUE`; set to `FALSE` to force `b = 1`.
6809/// - Returns spreadsheet errors for invalid domains, mismatched lengths, or degenerate x-variance.
6810///
6811/// # Examples
6812///
6813/// ```yaml,sandbox
6814/// title: "Exponential growth forecast"
6815/// formula: "=GROWTH({2,4,8},{1,2,3},{4,5})"
6816/// expected:
6817///   - [16, 32]
6818/// ```
6819///
6820/// ```yaml,sandbox
6821/// title: "Default x-values with perfect doubling pattern"
6822/// formula: "=GROWTH({3,6,12})"
6823/// expected:
6824///   - [3, 6, 12]
6825/// ```
6826#[derive(Debug)]
6827pub struct GrowthFn;
6828/// [formualizer-docgen:schema:start]
6829/// Name: GROWTH
6830/// Type: GrowthFn
6831/// Min args: 1
6832/// Max args: variadic
6833/// Variadic: true
6834/// Signature: GROWTH(arg1...: number@range)
6835/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
6836/// Caps: PURE, NUMERIC_ONLY
6837/// [formualizer-docgen:schema:end]
6838impl Function for GrowthFn {
6839    func_caps!(PURE, NUMERIC_ONLY);
6840    fn name(&self) -> &'static str {
6841        "GROWTH"
6842    }
6843    fn min_args(&self) -> usize {
6844        1
6845    }
6846    fn variadic(&self) -> bool {
6847        true
6848    }
6849    fn arg_schema(&self) -> &'static [ArgSchema] {
6850        &ARG_RANGE_NUM_LENIENT_ONE[..]
6851    }
6852    fn eval<'a, 'b, 'c>(
6853        &self,
6854        args: &'c [ArgumentHandle<'a, 'b>],
6855        _ctx: &dyn FunctionContext<'b>,
6856    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
6857        // GROWTH: args[0] = known_y's (required)
6858        // args[1] = known_x's (optional, defaults to {1,2,3,...})
6859        // args[2] = new_x's (optional, defaults to known_x's)
6860        // args[3] = const (optional, default TRUE - whether to compute intercept)
6861
6862        let y_vals = collect_numeric_stats(&args[0..1])?;
6863
6864        if y_vals.is_empty() {
6865            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6866                ExcelError::new_na(),
6867            )));
6868        }
6869
6870        // Check that all y values are positive (required for log transformation)
6871        for &y in &y_vals {
6872            if y <= 0.0 {
6873                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6874                    ExcelError::new_num(),
6875                )));
6876            }
6877        }
6878
6879        // Helper to check if argument is empty/omitted
6880        // Note: Empty arguments are represented as empty text strings by the parser
6881        fn is_arg_empty(arg: &ArgumentHandle) -> bool {
6882            match scalar_like_value(arg) {
6883                Ok(LiteralValue::Empty) => true,
6884                Ok(LiteralValue::Text(s)) if s.is_empty() => true,
6885                _ => false,
6886            }
6887        }
6888
6889        // Get known_x's or generate default {1, 2, 3, ...}
6890        let x_vals = if args.len() >= 2 && !is_arg_empty(&args[1]) {
6891            collect_numeric_stats(&args[1..2])?
6892        } else {
6893            (1..=y_vals.len()).map(|i| i as f64).collect()
6894        };
6895
6896        // Arrays must have same length
6897        if y_vals.len() != x_vals.len() {
6898            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6899                ExcelError::new_ref(),
6900            )));
6901        }
6902
6903        // Get new_x's or use known_x's - check if argument is empty/omitted
6904        let new_x_vals = if args.len() >= 3 && !is_arg_empty(&args[2]) {
6905            collect_numeric_stats(&args[2..3])?
6906        } else {
6907            x_vals.clone()
6908        };
6909
6910        if new_x_vals.is_empty() {
6911            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6912                ExcelError::new_na(),
6913            )));
6914        }
6915
6916        // Parse const argument (default TRUE)
6917        let use_const = if args.len() >= 4 {
6918            match scalar_like_value(&args[3])? {
6919                LiteralValue::Boolean(b) => b,
6920                LiteralValue::Number(n) => n != 0.0,
6921                LiteralValue::Int(i) => i != 0,
6922                LiteralValue::Empty => true, // empty defaults to TRUE
6923                _ => true,
6924            }
6925        } else {
6926            true
6927        };
6928
6929        // Transform to log space: ln(y) = ln(b) + x*ln(m)
6930        // This is linear regression on log-transformed y values
6931        let ln_y_vals: Vec<f64> = y_vals.iter().map(|&y| y.ln()).collect();
6932
6933        let n = x_vals.len() as f64;
6934
6935        // Calculate regression coefficients in log space
6936        let (ln_m, ln_b) = if use_const {
6937            // Normal linear regression with intercept
6938            let mean_x = x_vals.iter().sum::<f64>() / n;
6939            let mean_ln_y = ln_y_vals.iter().sum::<f64>() / n;
6940
6941            let mut sum_xy = 0.0;
6942            let mut sum_x2 = 0.0;
6943
6944            for i in 0..x_vals.len() {
6945                let dx = x_vals[i] - mean_x;
6946                let dy = ln_y_vals[i] - mean_ln_y;
6947                sum_xy += dx * dy;
6948                sum_x2 += dx * dx;
6949            }
6950
6951            if sum_x2 == 0.0 {
6952                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6953                    ExcelError::new_div(),
6954                )));
6955            }
6956
6957            let ln_m = sum_xy / sum_x2;
6958            let ln_b = mean_ln_y - ln_m * mean_x;
6959            (ln_m, ln_b)
6960        } else {
6961            // Regression through origin in log space (ln_b = 0, so b = 1)
6962            let mut sum_xy = 0.0;
6963            let mut sum_x2 = 0.0;
6964
6965            for i in 0..x_vals.len() {
6966                sum_xy += x_vals[i] * ln_y_vals[i];
6967                sum_x2 += x_vals[i] * x_vals[i];
6968            }
6969
6970            if sum_x2 == 0.0 {
6971                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6972                    ExcelError::new_div(),
6973                )));
6974            }
6975
6976            let ln_m = sum_xy / sum_x2;
6977            (ln_m, 0.0)
6978        };
6979
6980        // Convert back from log space: m = e^ln_m, b = e^ln_b
6981        let m = ln_m.exp();
6982        let b = ln_b.exp();
6983
6984        // Calculate predicted y values: y = b * m^x
6985        let predicted: Vec<LiteralValue> = new_x_vals
6986            .iter()
6987            .map(|&x| LiteralValue::Number(b * m.powf(x)))
6988            .collect();
6989
6990        // Return as 1xN array (row vector)
6991        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Array(vec![
6992            predicted,
6993        ])))
6994    }
6995}
6996
6997/* ─────────────────────────── LOGEST ──────────────────────────── */
6998
6999/// Returns parameters for an exponential model fitted to known data.
7000///
7001/// `LOGEST` fits `y = b * m^x` and returns either `[m, b]` or an expanded statistics matrix.
7002///
7003/// # Remarks
7004/// - All known y-values must be strictly greater than `0`.
7005/// - `known_x` defaults to `1..n` when omitted.
7006/// - `const` controls whether `b` is fitted (`TRUE` by default).
7007/// - `stats=TRUE` returns a `5x2` statistics block; otherwise returns `1x2`.
7008///
7009/// # Examples
7010///
7011/// ```yaml,sandbox
7012/// title: "Exponential base and intercept"
7013/// formula: "=LOGEST({2,4,8},{1,2,3})"
7014/// expected:
7015///   - [2, 1]
7016/// ```
7017///
7018/// ```yaml,sandbox
7019/// title: "Alternative growth series"
7020/// formula: "=LOGEST({3,6,12},{1,2,3})"
7021/// expected:
7022///   - [2, 1.5]
7023/// ```
7024#[derive(Debug)]
7025pub struct LogestFn;
7026/// [formualizer-docgen:schema:start]
7027/// Name: LOGEST
7028/// Type: LogestFn
7029/// Min args: 1
7030/// Max args: variadic
7031/// Variadic: true
7032/// Signature: LOGEST(arg1...: number@range)
7033/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
7034/// Caps: PURE, NUMERIC_ONLY
7035/// [formualizer-docgen:schema:end]
7036impl Function for LogestFn {
7037    func_caps!(PURE, NUMERIC_ONLY);
7038    fn name(&self) -> &'static str {
7039        "LOGEST"
7040    }
7041    fn min_args(&self) -> usize {
7042        1
7043    }
7044    fn variadic(&self) -> bool {
7045        true
7046    }
7047    fn arg_schema(&self) -> &'static [ArgSchema] {
7048        &ARG_RANGE_NUM_LENIENT_ONE[..]
7049    }
7050    fn eval<'a, 'b, 'c>(
7051        &self,
7052        args: &'c [ArgumentHandle<'a, 'b>],
7053        _ctx: &dyn FunctionContext<'b>,
7054    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
7055        // args[0] = known_y's (required)
7056        // args[1] = known_x's (optional, defaults to {1,2,3,...})
7057        // args[2] = const (optional, default TRUE - whether to compute b)
7058        // args[3] = stats (optional, default FALSE - whether to return additional statistics)
7059
7060        let y_vals = collect_numeric_stats(&args[0..1])?;
7061
7062        if y_vals.is_empty() {
7063            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7064                ExcelError::new_na(),
7065            )));
7066        }
7067
7068        // Check that all y values are positive (required for log transformation)
7069        for &y in &y_vals {
7070            if y <= 0.0 {
7071                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7072                    ExcelError::new_num(),
7073                )));
7074            }
7075        }
7076
7077        // Get known_x's or generate default {1, 2, 3, ...}
7078        let x_vals = if args.len() >= 2 {
7079            collect_numeric_stats(&args[1..2])?
7080        } else {
7081            (1..=y_vals.len()).map(|i| i as f64).collect()
7082        };
7083
7084        // Arrays must have same length
7085        if y_vals.len() != x_vals.len() {
7086            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7087                ExcelError::new_ref(),
7088            )));
7089        }
7090
7091        // Parse const argument (default TRUE)
7092        let use_const = if args.len() >= 3 {
7093            match scalar_like_value(&args[2])? {
7094                LiteralValue::Boolean(b) => b,
7095                LiteralValue::Number(n) => n != 0.0,
7096                LiteralValue::Int(i) => i != 0,
7097                _ => true,
7098            }
7099        } else {
7100            true
7101        };
7102
7103        // Parse stats argument (default FALSE)
7104        let return_stats = if args.len() >= 4 {
7105            match scalar_like_value(&args[3])? {
7106                LiteralValue::Boolean(b) => b,
7107                LiteralValue::Number(n) => n != 0.0,
7108                LiteralValue::Int(i) => i != 0,
7109                _ => false,
7110            }
7111        } else {
7112            false
7113        };
7114
7115        // Transform to log space: ln(y) = ln(b) + x*ln(m)
7116        let ln_y_vals: Vec<f64> = y_vals.iter().map(|&y| y.ln()).collect();
7117
7118        let n = x_vals.len() as f64;
7119
7120        // Calculate regression coefficients in log space
7121        let (ln_m, ln_b) = if use_const {
7122            // Normal linear regression with intercept
7123            let mean_x = x_vals.iter().sum::<f64>() / n;
7124            let mean_ln_y = ln_y_vals.iter().sum::<f64>() / n;
7125
7126            let mut sum_xy = 0.0;
7127            let mut sum_x2 = 0.0;
7128
7129            for i in 0..x_vals.len() {
7130                let dx = x_vals[i] - mean_x;
7131                let dy = ln_y_vals[i] - mean_ln_y;
7132                sum_xy += dx * dy;
7133                sum_x2 += dx * dx;
7134            }
7135
7136            if sum_x2 == 0.0 {
7137                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7138                    ExcelError::new_div(),
7139                )));
7140            }
7141
7142            let ln_m = sum_xy / sum_x2;
7143            let ln_b = mean_ln_y - ln_m * mean_x;
7144            (ln_m, ln_b)
7145        } else {
7146            // Regression through origin in log space (ln_b = 0, so b = 1)
7147            let mut sum_xy = 0.0;
7148            let mut sum_x2 = 0.0;
7149
7150            for i in 0..x_vals.len() {
7151                sum_xy += x_vals[i] * ln_y_vals[i];
7152                sum_x2 += x_vals[i] * x_vals[i];
7153            }
7154
7155            if sum_x2 == 0.0 {
7156                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7157                    ExcelError::new_div(),
7158                )));
7159            }
7160
7161            let ln_m = sum_xy / sum_x2;
7162            (ln_m, 0.0)
7163        };
7164
7165        // Convert from log space to get m and b
7166        let m = ln_m.exp();
7167        let b = ln_b.exp();
7168
7169        if !return_stats {
7170            // Return just m and b as 1x2 array: [[m, b]]
7171            let row = vec![LiteralValue::Number(m), LiteralValue::Number(b)];
7172            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Array(vec![
7173                row,
7174            ])));
7175        }
7176
7177        // Calculate additional statistics for stats=TRUE
7178        // Statistics are computed in log space, then converted
7179        // Row 1: [m, b]
7180        // Row 2: [se_m, se_b] - standard errors (converted from log space)
7181        // Row 3: [r_squared, se_y] - R-squared and standard error of y estimate
7182        // Row 4: [F_statistic, df] - F-statistic and degrees of freedom
7183        // Row 5: [ss_reg, ss_resid] - regression sum of squares and residual sum of squares
7184
7185        let mean_ln_y = ln_y_vals.iter().sum::<f64>() / n;
7186
7187        // Calculate residuals and sums of squares in log space
7188        let mut ss_resid = 0.0;
7189        let mut ss_tot = 0.0;
7190
7191        for i in 0..x_vals.len() {
7192            let ln_y_pred = ln_m * x_vals[i] + ln_b;
7193            let residual = ln_y_vals[i] - ln_y_pred;
7194            ss_resid += residual * residual;
7195            let dy_tot = ln_y_vals[i] - mean_ln_y;
7196            ss_tot += dy_tot * dy_tot;
7197        }
7198
7199        let ss_reg = ss_tot - ss_resid;
7200
7201        // R-squared (same in both spaces for transformed regression)
7202        let r_squared = if ss_tot == 0.0 {
7203            1.0
7204        } else {
7205            1.0 - (ss_resid / ss_tot)
7206        };
7207
7208        // Degrees of freedom
7209        let df = if use_const {
7210            (n as i64 - 2).max(1) as f64
7211        } else {
7212            (n as i64 - 1).max(1) as f64
7213        };
7214
7215        // Standard error of y estimate (in log space)
7216        let se_ln_y = if df > 0.0 {
7217            (ss_resid / df).sqrt()
7218        } else {
7219            0.0
7220        };
7221
7222        // Standard errors of coefficients in log space
7223        let mean_x = x_vals.iter().sum::<f64>() / n;
7224        let mut sum_x2_centered = 0.0;
7225        let mut sum_x2_raw = 0.0;
7226        for &xi in &x_vals {
7227            sum_x2_centered += (xi - mean_x).powi(2);
7228            sum_x2_raw += xi * xi;
7229        }
7230
7231        let se_ln_m = if sum_x2_centered > 0.0 && df > 0.0 {
7232            se_ln_y / sum_x2_centered.sqrt()
7233        } else {
7234            f64::NAN
7235        };
7236
7237        let se_ln_b = if use_const && sum_x2_centered > 0.0 && df > 0.0 {
7238            se_ln_y * (sum_x2_raw / (n * sum_x2_centered)).sqrt()
7239        } else {
7240            f64::NAN
7241        };
7242
7243        // Convert standard errors: se_m = m * se_ln_m (delta method approximation)
7244        let se_m = m * se_ln_m;
7245        let se_b = b * se_ln_b;
7246
7247        // Standard error of y estimate - convert from log space
7248        // This is an approximation; for exponential models, se_y in original space varies with x
7249        let se_y = se_ln_y;
7250
7251        // F-statistic
7252        let f_stat = if ss_resid > 0.0 && df > 0.0 {
7253            (ss_reg / 1.0) / (ss_resid / df)
7254        } else if ss_resid == 0.0 {
7255            f64::INFINITY
7256        } else {
7257            f64::NAN
7258        };
7259
7260        // Build 5x2 result array
7261        let rows = vec![
7262            vec![LiteralValue::Number(m), LiteralValue::Number(b)],
7263            vec![LiteralValue::Number(se_m), LiteralValue::Number(se_b)],
7264            vec![LiteralValue::Number(r_squared), LiteralValue::Number(se_y)],
7265            vec![LiteralValue::Number(f_stat), LiteralValue::Number(df)],
7266            vec![LiteralValue::Number(ss_reg), LiteralValue::Number(ss_resid)],
7267        ];
7268
7269        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Array(rows)))
7270    }
7271}
7272
7273/* ─────────────────────────── PERCENTRANK ──────────────────────────── */
7274
7275/// Returns the inclusive percentile rank of `x` within a numeric data array.
7276///
7277/// `PERCENTRANK.INC` maps values to `[0, 1]` and interpolates linearly between data points.
7278///
7279/// # Remarks
7280/// - `x` must be within the observed min/max range; otherwise returns `#N/A`.
7281/// - Optional `significance` controls decimal truncation and defaults to `3`.
7282/// - `significance` must be at least `1`.
7283/// - Returns `#NUM!` for invalid setup such as empty numeric input.
7284///
7285/// # Examples
7286///
7287/// ```yaml,sandbox
7288/// title: "Exact inclusive percentile rank"
7289/// formula: "=PERCENTRANK.INC({1,2,3,4,5},3)"
7290/// expected: 0.5
7291/// ```
7292///
7293/// ```yaml,sandbox
7294/// title: "Interpolated inclusive percentile rank"
7295/// formula: "=PERCENTRANK.INC({1,2,3,4,5},2.5)"
7296/// expected: 0.375
7297/// ```
7298#[derive(Debug)]
7299pub struct PercentRankIncFn;
7300/// [formualizer-docgen:schema:start]
7301/// Name: PERCENTRANK.INC
7302/// Type: PercentRankIncFn
7303/// Min args: 2
7304/// Max args: variadic
7305/// Variadic: true
7306/// Signature: PERCENTRANK.INC(arg1...: number@range)
7307/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
7308/// Caps: PURE, NUMERIC_ONLY
7309/// [formualizer-docgen:schema:end]
7310impl Function for PercentRankIncFn {
7311    func_caps!(PURE, NUMERIC_ONLY);
7312    fn name(&self) -> &'static str {
7313        "PERCENTRANK.INC"
7314    }
7315    fn aliases(&self) -> &'static [&'static str] {
7316        &["PERCENTRANK"]
7317    }
7318    fn min_args(&self) -> usize {
7319        2
7320    }
7321    fn variadic(&self) -> bool {
7322        true
7323    }
7324    fn arg_schema(&self) -> &'static [ArgSchema] {
7325        &ARG_RANGE_NUM_LENIENT_ONE[..]
7326    }
7327    fn eval<'a, 'b, 'c>(
7328        &self,
7329        args: &'c [ArgumentHandle<'a, 'b>],
7330        _ctx: &dyn FunctionContext<'b>,
7331    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
7332        if args.len() < 2 {
7333            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7334                ExcelError::new_num(),
7335            )));
7336        }
7337
7338        // Get x value (the value to find the rank of)
7339        let x = match coerce_num(&scalar_like_value(&args[1])?) {
7340            Ok(n) => n,
7341            Err(_) => {
7342                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7343                    ExcelError::new_num(),
7344                )));
7345            }
7346        };
7347
7348        // Get optional significance (default 3)
7349        let significance = if args.len() > 2 {
7350            match coerce_num(&scalar_like_value(&args[2])?) {
7351                Ok(n) => {
7352                    let s = n as i32;
7353                    if s < 1 {
7354                        return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7355                            ExcelError::new_num(),
7356                        )));
7357                    }
7358                    s as u32
7359                }
7360                Err(_) => 3,
7361            }
7362        } else {
7363            3
7364        };
7365
7366        // Collect and sort the data array
7367        let mut nums = collect_numeric_stats(&args[0..1])?;
7368        if nums.is_empty() {
7369            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7370                ExcelError::new_num(),
7371            )));
7372        }
7373        nums.sort_by(|a, b| a.partial_cmp(b).unwrap());
7374
7375        let n = nums.len();
7376
7377        // Check if x is outside the range
7378        if x < nums[0] || x > nums[n - 1] {
7379            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7380                ExcelError::new_na(),
7381            )));
7382        }
7383
7384        // Find the rank using linear interpolation
7385        // For PERCENTRANK.INC, the formula is: rank = (position) / (n-1)
7386        // where position is 0-based and uses linear interpolation
7387        let rank = if n == 1 {
7388            // Single element - rank is 0 (or 1.0 if we want, but Excel returns 0)
7389            0.0
7390        } else {
7391            let mut rank_val = 0.0;
7392            for i in 0..n - 1 {
7393                if (nums[i] - x).abs() < 1e-12 {
7394                    // Exact match at position i
7395                    rank_val = (i as f64) / ((n - 1) as f64);
7396                    break;
7397                } else if nums[i] < x && x < nums[i + 1] {
7398                    // Interpolate between positions i and i+1
7399                    let frac = (x - nums[i]) / (nums[i + 1] - nums[i]);
7400                    rank_val = ((i as f64) + frac) / ((n - 1) as f64);
7401                    break;
7402                } else if i == n - 2 && (nums[n - 1] - x).abs() < 1e-12 {
7403                    // Exact match at last position
7404                    rank_val = 1.0;
7405                }
7406            }
7407            rank_val
7408        };
7409
7410        // Truncate to significance decimal places
7411        let multiplier = 10_f64.powi(significance as i32);
7412        let truncated = (rank * multiplier).trunc() / multiplier;
7413
7414        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
7415            truncated,
7416        )))
7417    }
7418}
7419
7420/// Returns the exclusive percentile rank of `x` within a numeric data array.
7421///
7422/// `PERCENTRANK.EXC` uses an open ranking scale that excludes exact `0` and `1` endpoints.
7423///
7424/// # Remarks
7425/// - `x` must lie within the observed min/max range; otherwise returns `#N/A`.
7426/// - Output is based on position divided by `n + 1`, with interpolation between points.
7427/// - Optional `significance` defaults to `3` and must be at least `1`.
7428/// - Returns `#NUM!` for invalid setup such as empty numeric input.
7429///
7430/// # Examples
7431///
7432/// ```yaml,sandbox
7433/// title: "Exact exclusive percentile rank"
7434/// formula: "=PERCENTRANK.EXC({1,2,3,4,5},3)"
7435/// expected: 0.5
7436/// ```
7437///
7438/// ```yaml,sandbox
7439/// title: "Interpolated exclusive percentile rank"
7440/// formula: "=PERCENTRANK.EXC({1,2,3,4,5},2.5)"
7441/// expected: 0.416
7442/// ```
7443#[derive(Debug)]
7444pub struct PercentRankExcFn;
7445/// [formualizer-docgen:schema:start]
7446/// Name: PERCENTRANK.EXC
7447/// Type: PercentRankExcFn
7448/// Min args: 2
7449/// Max args: variadic
7450/// Variadic: true
7451/// Signature: PERCENTRANK.EXC(arg1...: number@range)
7452/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
7453/// Caps: PURE, NUMERIC_ONLY
7454/// [formualizer-docgen:schema:end]
7455impl Function for PercentRankExcFn {
7456    func_caps!(PURE, NUMERIC_ONLY);
7457    fn name(&self) -> &'static str {
7458        "PERCENTRANK.EXC"
7459    }
7460    fn min_args(&self) -> usize {
7461        2
7462    }
7463    fn variadic(&self) -> bool {
7464        true
7465    }
7466    fn arg_schema(&self) -> &'static [ArgSchema] {
7467        &ARG_RANGE_NUM_LENIENT_ONE[..]
7468    }
7469    fn eval<'a, 'b, 'c>(
7470        &self,
7471        args: &'c [ArgumentHandle<'a, 'b>],
7472        _ctx: &dyn FunctionContext<'b>,
7473    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
7474        if args.len() < 2 {
7475            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7476                ExcelError::new_num(),
7477            )));
7478        }
7479
7480        // Get x value (the value to find the rank of)
7481        let x = match coerce_num(&scalar_like_value(&args[1])?) {
7482            Ok(n) => n,
7483            Err(_) => {
7484                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7485                    ExcelError::new_num(),
7486                )));
7487            }
7488        };
7489
7490        // Get optional significance (default 3)
7491        let significance = if args.len() > 2 {
7492            match coerce_num(&scalar_like_value(&args[2])?) {
7493                Ok(n) => {
7494                    let s = n as i32;
7495                    if s < 1 {
7496                        return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7497                            ExcelError::new_num(),
7498                        )));
7499                    }
7500                    s as u32
7501                }
7502                Err(_) => 3,
7503            }
7504        } else {
7505            3
7506        };
7507
7508        // Collect and sort the data array
7509        let mut nums = collect_numeric_stats(&args[0..1])?;
7510        if nums.is_empty() {
7511            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7512                ExcelError::new_num(),
7513            )));
7514        }
7515        nums.sort_by(|a, b| a.partial_cmp(b).unwrap());
7516
7517        let n = nums.len();
7518
7519        // Check if x is outside the range
7520        if x < nums[0] || x > nums[n - 1] {
7521            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7522                ExcelError::new_na(),
7523            )));
7524        }
7525
7526        // For PERCENTRANK.EXC, the formula is: rank = position / (n+1)
7527        // where position is 1-based and uses linear interpolation
7528        let rank = {
7529            let mut rank_val = 0.0;
7530            for i in 0..n {
7531                if (nums[i] - x).abs() < 1e-12 {
7532                    // Exact match at position i (1-based: i+1)
7533                    rank_val = ((i + 1) as f64) / ((n + 1) as f64);
7534                    break;
7535                } else if i < n - 1 && nums[i] < x && x < nums[i + 1] {
7536                    // Interpolate between positions i and i+1 (1-based: i+1 and i+2)
7537                    let frac = (x - nums[i]) / (nums[i + 1] - nums[i]);
7538                    let position = ((i + 1) as f64) + frac;
7539                    rank_val = position / ((n + 1) as f64);
7540                    break;
7541                }
7542            }
7543            rank_val
7544        };
7545
7546        // Truncate to significance decimal places
7547        let multiplier = 10_f64.powi(significance as i32);
7548        let truncated = (rank * multiplier).trunc() / multiplier;
7549
7550        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
7551            truncated,
7552        )))
7553    }
7554}
7555
7556/* ─────────────────────────── FREQUENCY ──────────────────────────── */
7557
7558/// Returns a vertical frequency distribution for numeric data across bin cutoffs.
7559///
7560/// `FREQUENCY` counts values into `<= first bin`, intermediate right-closed bins, and an overflow
7561/// bucket above the final bin.
7562///
7563/// # Remarks
7564/// - Returns an array with `bins + 1` rows.
7565/// - Bins are sorted before counting.
7566/// - If `bins_array` has no numeric values, result is a single count of all data points.
7567/// - Non-numeric values in input ranges are ignored by statistical-collection rules.
7568///
7569/// # Examples
7570///
7571/// ```yaml,sandbox
7572/// title: "Frequency buckets with two bins"
7573/// formula: "=FREQUENCY({1,2,3,4,5},{2,4})"
7574/// expected:
7575///   - [2]
7576///   - [2]
7577///   - [1]
7578/// ```
7579///
7580/// ```yaml,sandbox
7581/// title: "Frequency with repeated values"
7582/// formula: "=FREQUENCY({1,1,2,2,3},{1,2})"
7583/// expected:
7584///   - [2]
7585///   - [2]
7586///   - [1]
7587/// ```
7588#[derive(Debug)]
7589pub struct FrequencyFn;
7590/// [formualizer-docgen:schema:start]
7591/// Name: FREQUENCY
7592/// Type: FrequencyFn
7593/// Min args: 2
7594/// Max args: 1
7595/// Variadic: false
7596/// Signature: FREQUENCY(arg1: number@range)
7597/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
7598/// Caps: PURE, NUMERIC_ONLY
7599/// [formualizer-docgen:schema:end]
7600impl Function for FrequencyFn {
7601    func_caps!(PURE, NUMERIC_ONLY);
7602    fn name(&self) -> &'static str {
7603        "FREQUENCY"
7604    }
7605    fn min_args(&self) -> usize {
7606        2
7607    }
7608    fn variadic(&self) -> bool {
7609        false
7610    }
7611    fn arg_schema(&self) -> &'static [ArgSchema] {
7612        &ARG_RANGE_NUM_LENIENT_ONE[..]
7613    }
7614    fn eval<'a, 'b, 'c>(
7615        &self,
7616        args: &'c [ArgumentHandle<'a, 'b>],
7617        _ctx: &dyn FunctionContext<'b>,
7618    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
7619        if args.len() < 2 {
7620            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7621                ExcelError::new_num(),
7622            )));
7623        }
7624
7625        // Collect data array
7626        let data = collect_numeric_stats(&args[0..1])?;
7627
7628        // Collect bins array
7629        let mut bins = collect_numeric_stats(&args[1..2])?;
7630
7631        // Handle empty bins - return single count of all data
7632        if bins.is_empty() {
7633            let rows = vec![vec![LiteralValue::Number(data.len() as f64)]];
7634            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Array(rows)));
7635        }
7636
7637        // Sort bins
7638        bins.sort_by(|a, b| a.partial_cmp(b).unwrap());
7639
7640        // Calculate frequencies
7641        // Result has bins.len() + 1 elements
7642        let mut frequencies = vec![0usize; bins.len() + 1];
7643
7644        for &value in &data {
7645            // Find which bin the value belongs to
7646            let mut found = false;
7647            for (i, &bin) in bins.iter().enumerate() {
7648                if i == 0 {
7649                    // First bin: count values <= bins[0]
7650                    if value <= bin {
7651                        frequencies[0] += 1;
7652                        found = true;
7653                        break;
7654                    }
7655                } else {
7656                    // Intermediate bins: count values > bins[i-1] AND <= bins[i]
7657                    if value <= bin {
7658                        frequencies[i] += 1;
7659                        found = true;
7660                        break;
7661                    }
7662                }
7663            }
7664            // Last bin: values > bins[last]
7665            if !found {
7666                frequencies[bins.len()] += 1;
7667            }
7668        }
7669
7670        // Return as vertical array (column vector)
7671        let rows: Vec<Vec<LiteralValue>> = frequencies
7672            .into_iter()
7673            .map(|f| vec![LiteralValue::Number(f as f64)])
7674            .collect();
7675
7676        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Array(rows)))
7677    }
7678}
7679
7680/* ─────────────────────────── T.DIST.2T ──────────────────────────── */
7681
7682/// Returns the two-tailed Student's t probability beyond `x`.
7683///
7684/// `T.DIST.2T` computes `P(|T| > x)` for the specified degrees of freedom.
7685///
7686/// # Remarks
7687/// - Requires `x >= 0` and `deg_freedom >= 1`.
7688/// - Represents a two-sided tail area.
7689/// - Returns `#NUM!` when arguments are outside valid ranges.
7690/// - Invalid numeric coercions propagate as spreadsheet errors.
7691///
7692/// # Examples
7693///
7694/// ```yaml,sandbox
7695/// title: "Two-tailed t probability at zero"
7696/// formula: "=T.DIST.2T(0,10)"
7697/// expected: 1
7698/// ```
7699///
7700/// ```yaml,sandbox
7701/// title: "Two-tailed t probability at x=2"
7702/// formula: "=T.DIST.2T(2,10)"
7703/// expected: 0.0733880342639167
7704/// ```
7705#[derive(Debug)]
7706pub struct TDist2TFn;
7707/// [formualizer-docgen:schema:start]
7708/// Name: T.DIST.2T
7709/// Type: TDist2TFn
7710/// Min args: 2
7711/// Max args: 2
7712/// Variadic: false
7713/// Signature: T.DIST.2T(arg1: number@scalar, arg2: number@scalar)
7714/// 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}
7715/// Caps: PURE
7716/// [formualizer-docgen:schema:end]
7717impl Function for TDist2TFn {
7718    func_caps!(PURE);
7719    fn name(&self) -> &'static str {
7720        "T.DIST.2T"
7721    }
7722    fn min_args(&self) -> usize {
7723        2
7724    }
7725    fn arg_schema(&self) -> &'static [ArgSchema] {
7726        use std::sync::LazyLock;
7727        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
7728            vec![
7729                ArgSchema::number_lenient_scalar(),
7730                ArgSchema::number_lenient_scalar(),
7731            ]
7732        });
7733        &SCHEMA[..]
7734    }
7735    fn eval<'a, 'b, 'c>(
7736        &self,
7737        args: &'c [ArgumentHandle<'a, 'b>],
7738        _ctx: &dyn FunctionContext<'b>,
7739    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
7740        let x = coerce_num(&scalar_like_value(&args[0])?)?;
7741        let df = coerce_num(&scalar_like_value(&args[1])?)?;
7742
7743        // x must be non-negative for T.DIST.2T, df must be >= 1
7744        if x < 0.0 || df < 1.0 {
7745            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7746                ExcelError::new_num(),
7747            )));
7748        }
7749
7750        // Two-tailed: P(|T| > x) = 2 * (1 - t_cdf(x, df))
7751        let p = 2.0 * (1.0 - t_cdf(x, df));
7752        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(p)))
7753    }
7754}
7755
7756/* ─────────────────────────── T.INV.2T ──────────────────────────── */
7757
7758/// Returns the positive t critical value for a two-tailed probability.
7759///
7760/// `T.INV.2T` solves for `t` such that `P(|T| > t) = probability`.
7761///
7762/// # Remarks
7763/// - `probability` must satisfy `0 < probability <= 1`.
7764/// - `deg_freedom` must be at least `1`.
7765/// - Returns `#NUM!` for invalid probability or degree-of-freedom arguments.
7766/// - Alias `TINV` is supported.
7767///
7768/// # Examples
7769///
7770/// ```yaml,sandbox
7771/// title: "Maximum two-tailed probability"
7772/// formula: "=T.INV.2T(1,10)"
7773/// expected: 0
7774/// ```
7775///
7776/// ```yaml,sandbox
7777/// title: "95% two-sided critical value"
7778/// formula: "=T.INV.2T(0.05,10)"
7779/// expected: 2.228138851986273
7780/// ```
7781#[derive(Debug)]
7782pub struct TInv2TFn;
7783/// [formualizer-docgen:schema:start]
7784/// Name: T.INV.2T
7785/// Type: TInv2TFn
7786/// Min args: 2
7787/// Max args: 2
7788/// Variadic: false
7789/// Signature: T.INV.2T(arg1: number@scalar, arg2: number@scalar)
7790/// 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}
7791/// Caps: PURE
7792/// [formualizer-docgen:schema:end]
7793impl Function for TInv2TFn {
7794    func_caps!(PURE);
7795    fn name(&self) -> &'static str {
7796        "T.INV.2T"
7797    }
7798    fn aliases(&self) -> &'static [&'static str] {
7799        &["TINV"]
7800    }
7801    fn min_args(&self) -> usize {
7802        2
7803    }
7804    fn arg_schema(&self) -> &'static [ArgSchema] {
7805        use std::sync::LazyLock;
7806        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
7807            vec![
7808                ArgSchema::number_lenient_scalar(),
7809                ArgSchema::number_lenient_scalar(),
7810            ]
7811        });
7812        &SCHEMA[..]
7813    }
7814    fn eval<'a, 'b, 'c>(
7815        &self,
7816        args: &'c [ArgumentHandle<'a, 'b>],
7817        _ctx: &dyn FunctionContext<'b>,
7818    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
7819        let p = coerce_num(&scalar_like_value(&args[0])?)?;
7820        let df = coerce_num(&scalar_like_value(&args[1])?)?;
7821
7822        // probability must be in (0, 1], df >= 1
7823        if p <= 0.0 || p > 1.0 || df < 1.0 {
7824            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7825                ExcelError::new_num(),
7826            )));
7827        }
7828
7829        // For two-tailed: we want t such that P(|T| > t) = p
7830        // P(|T| > t) = 2 * (1 - F(t)) where F is CDF
7831        // So 1 - F(t) = p/2, meaning F(t) = 1 - p/2
7832        // Thus t = t_inv(1 - p/2, df)
7833        match t_inv(1.0 - p / 2.0, df) {
7834            Some(result) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
7835                result,
7836            ))),
7837            None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7838                ExcelError::new_num(),
7839            ))),
7840        }
7841    }
7842}
7843
7844/* ─────────────────────────── T.TEST ──────────────────────────── */
7845
7846/// Returns the p-value from a Student t-test comparing two numeric samples.
7847///
7848/// `T.TEST` supports paired, equal-variance two-sample, and unequal-variance (Welch) modes.
7849///
7850/// # Remarks
7851/// - `tails` must be `1` (one-tailed) or `2` (two-tailed).
7852/// - `type` must be `1` (paired), `2` (two-sample equal variance), or `3` (Welch).
7853/// - Returns `#N/A` when paired mode arrays have different lengths.
7854/// - Returns `#NUM!` or `#DIV/0!` for invalid setup or degenerate variance conditions.
7855///
7856/// # Examples
7857///
7858/// ```yaml,sandbox
7859/// title: "Two-tailed equal-variance test with identical samples"
7860/// formula: "=T.TEST({1,2,3},{1,2,3},2,2)"
7861/// expected: 1
7862/// ```
7863///
7864/// ```yaml,sandbox
7865/// title: "One-tailed Welch test with identical samples"
7866/// formula: "=T.TEST({1,2,3},{1,2,3},1,3)"
7867/// expected: 0.5
7868/// ```
7869#[derive(Debug)]
7870pub struct TTestFn;
7871/// [formualizer-docgen:schema:start]
7872/// Name: T.TEST
7873/// Type: TTestFn
7874/// Min args: 4
7875/// Max args: 4
7876/// Variadic: false
7877/// Signature: T.TEST(arg1: number@range, arg2: number@range, arg3: number@scalar, arg4: number@scalar)
7878/// 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}
7879/// Caps: PURE
7880/// [formualizer-docgen:schema:end]
7881impl Function for TTestFn {
7882    func_caps!(PURE);
7883    fn name(&self) -> &'static str {
7884        "T.TEST"
7885    }
7886    fn aliases(&self) -> &'static [&'static str] {
7887        &["TTEST"]
7888    }
7889    fn min_args(&self) -> usize {
7890        4
7891    }
7892    fn arg_schema(&self) -> &'static [ArgSchema] {
7893        use std::sync::LazyLock;
7894        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
7895            vec![
7896                {
7897                    let mut s = ArgSchema::number_lenient_scalar();
7898                    s.shape = crate::args::ShapeKind::Range;
7899                    s.coercion = formualizer_common::CoercionPolicy::NumberLenientText;
7900                    s
7901                },
7902                {
7903                    let mut s = ArgSchema::number_lenient_scalar();
7904                    s.shape = crate::args::ShapeKind::Range;
7905                    s.coercion = formualizer_common::CoercionPolicy::NumberLenientText;
7906                    s
7907                },
7908                ArgSchema::number_lenient_scalar(), // tails
7909                ArgSchema::number_lenient_scalar(), // type
7910            ]
7911        });
7912        &SCHEMA[..]
7913    }
7914    fn eval<'a, 'b, 'c>(
7915        &self,
7916        args: &'c [ArgumentHandle<'a, 'b>],
7917        _ctx: &dyn FunctionContext<'b>,
7918    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
7919        let array1 = collect_numeric_stats(&args[0..1])?;
7920        let array2 = collect_numeric_stats(&args[1..2])?;
7921        let tails = coerce_num(&scalar_like_value(&args[2])?)? as i32;
7922        let test_type = coerce_num(&scalar_like_value(&args[3])?)? as i32;
7923
7924        // Validate tails (1 or 2) and type (1, 2, or 3)
7925        if !(1..=2).contains(&tails) || !(1..=3).contains(&test_type) {
7926            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7927                ExcelError::new_num(),
7928            )));
7929        }
7930
7931        let n1 = array1.len();
7932        let n2 = array2.len();
7933
7934        // For paired test, arrays must have same length
7935        if test_type == 1 && n1 != n2 {
7936            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7937                ExcelError::new_na(),
7938            )));
7939        }
7940
7941        // Need at least 2 data points for meaningful t-test
7942        if n1 < 2 || n2 < 2 {
7943            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7944                ExcelError::new_num(),
7945            )));
7946        }
7947
7948        let (t_stat, df) = match test_type {
7949            1 => {
7950                // Paired t-test
7951                let n = n1 as f64;
7952                let diffs: Vec<f64> = array1
7953                    .iter()
7954                    .zip(array2.iter())
7955                    .map(|(a, b)| a - b)
7956                    .collect();
7957                let mean_diff = diffs.iter().sum::<f64>() / n;
7958                let var_diff =
7959                    diffs.iter().map(|d| (d - mean_diff).powi(2)).sum::<f64>() / (n - 1.0);
7960                if var_diff == 0.0 {
7961                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7962                        ExcelError::new_div(),
7963                    )));
7964                }
7965                let se = (var_diff / n).sqrt();
7966                (mean_diff / se, n - 1.0)
7967            }
7968            2 => {
7969                // Two-sample equal variance (pooled)
7970                let n1f = n1 as f64;
7971                let n2f = n2 as f64;
7972                let mean1 = array1.iter().sum::<f64>() / n1f;
7973                let mean2 = array2.iter().sum::<f64>() / n2f;
7974                let var1 = array1.iter().map(|x| (x - mean1).powi(2)).sum::<f64>() / (n1f - 1.0);
7975                let var2 = array2.iter().map(|x| (x - mean2).powi(2)).sum::<f64>() / (n2f - 1.0);
7976
7977                // Pooled variance
7978                let sp2 = ((n1f - 1.0) * var1 + (n2f - 1.0) * var2) / (n1f + n2f - 2.0);
7979                if sp2 == 0.0 {
7980                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7981                        ExcelError::new_div(),
7982                    )));
7983                }
7984                let se = (sp2 * (1.0 / n1f + 1.0 / n2f)).sqrt();
7985                ((mean1 - mean2) / se, n1f + n2f - 2.0)
7986            }
7987            3 => {
7988                // Welch's t-test (unequal variance)
7989                let n1f = n1 as f64;
7990                let n2f = n2 as f64;
7991                let mean1 = array1.iter().sum::<f64>() / n1f;
7992                let mean2 = array2.iter().sum::<f64>() / n2f;
7993                let var1 = array1.iter().map(|x| (x - mean1).powi(2)).sum::<f64>() / (n1f - 1.0);
7994                let var2 = array2.iter().map(|x| (x - mean2).powi(2)).sum::<f64>() / (n2f - 1.0);
7995
7996                let s1_n = var1 / n1f;
7997                let s2_n = var2 / n2f;
7998                let se = (s1_n + s2_n).sqrt();
7999                if se == 0.0 {
8000                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8001                        ExcelError::new_div(),
8002                    )));
8003                }
8004
8005                // Welch-Satterthwaite degrees of freedom
8006                let df_num = (s1_n + s2_n).powi(2);
8007                let df_denom = s1_n.powi(2) / (n1f - 1.0) + s2_n.powi(2) / (n2f - 1.0);
8008                let df = if df_denom == 0.0 {
8009                    1.0
8010                } else {
8011                    df_num / df_denom
8012                };
8013                ((mean1 - mean2) / se, df)
8014            }
8015            _ => unreachable!(),
8016        };
8017
8018        // Calculate p-value based on tails
8019        let t_abs = t_stat.abs();
8020        let p = if tails == 1 {
8021            1.0 - t_cdf(t_abs, df)
8022        } else {
8023            2.0 * (1.0 - t_cdf(t_abs, df))
8024        };
8025
8026        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(p)))
8027    }
8028}
8029
8030/* ─────────────────────────── F.TEST ──────────────────────────── */
8031
8032/// Returns the two-tailed p-value from an F-test comparing sample variances.
8033///
8034/// `F.TEST` evaluates whether two samples have significantly different variances.
8035///
8036/// # Remarks
8037/// - Each array must contain at least two numeric values.
8038/// - Uses sample variances and computes a two-tailed probability.
8039/// - Returns `#DIV/0!` when either sample variance is zero.
8040/// - Alias `FTEST` is supported.
8041///
8042/// # Examples
8043///
8044/// ```yaml,sandbox
8045/// title: "Identical samples yield p-value 1"
8046/// formula: "=F.TEST({1,2,3,4},{1,2,3,4})"
8047/// expected: 1
8048/// ```
8049///
8050/// ```yaml,sandbox
8051/// title: "Different variances example"
8052/// formula: "=F.TEST({1,2,3,4},{1,1,1,5})"
8053/// expected: 0.5466810975407987
8054/// ```
8055#[derive(Debug)]
8056pub struct FTestFn;
8057/// [formualizer-docgen:schema:start]
8058/// Name: F.TEST
8059/// Type: FTestFn
8060/// Min args: 2
8061/// Max args: 2
8062/// Variadic: false
8063/// Signature: F.TEST(arg1: number@range, arg2: number@range)
8064/// 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}
8065/// Caps: PURE
8066/// [formualizer-docgen:schema:end]
8067impl Function for FTestFn {
8068    func_caps!(PURE);
8069    fn name(&self) -> &'static str {
8070        "F.TEST"
8071    }
8072    fn aliases(&self) -> &'static [&'static str] {
8073        &["FTEST"]
8074    }
8075    fn min_args(&self) -> usize {
8076        2
8077    }
8078    fn arg_schema(&self) -> &'static [ArgSchema] {
8079        use std::sync::LazyLock;
8080        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
8081            vec![
8082                {
8083                    let mut s = ArgSchema::number_lenient_scalar();
8084                    s.shape = crate::args::ShapeKind::Range;
8085                    s.coercion = formualizer_common::CoercionPolicy::NumberLenientText;
8086                    s
8087                },
8088                {
8089                    let mut s = ArgSchema::number_lenient_scalar();
8090                    s.shape = crate::args::ShapeKind::Range;
8091                    s.coercion = formualizer_common::CoercionPolicy::NumberLenientText;
8092                    s
8093                },
8094            ]
8095        });
8096        &SCHEMA[..]
8097    }
8098    fn eval<'a, 'b, 'c>(
8099        &self,
8100        args: &'c [ArgumentHandle<'a, 'b>],
8101        _ctx: &dyn FunctionContext<'b>,
8102    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
8103        let array1 = collect_numeric_stats(&args[0..1])?;
8104        let array2 = collect_numeric_stats(&args[1..2])?;
8105
8106        let n1 = array1.len();
8107        let n2 = array2.len();
8108
8109        // Need at least 2 points in each array
8110        if n1 < 2 || n2 < 2 {
8111            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8112                ExcelError::new_div(),
8113            )));
8114        }
8115
8116        let n1f = n1 as f64;
8117        let n2f = n2 as f64;
8118
8119        let mean1 = array1.iter().sum::<f64>() / n1f;
8120        let mean2 = array2.iter().sum::<f64>() / n2f;
8121
8122        let var1 = array1.iter().map(|x| (x - mean1).powi(2)).sum::<f64>() / (n1f - 1.0);
8123        let var2 = array2.iter().map(|x| (x - mean2).powi(2)).sum::<f64>() / (n2f - 1.0);
8124
8125        // Handle zero variance
8126        if var1 == 0.0 || var2 == 0.0 {
8127            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8128                ExcelError::new_div(),
8129            )));
8130        }
8131
8132        // F-statistic: Excel's F.TEST uses var1/var2 (order matters for degrees of freedom)
8133        // and returns two-tailed p-value
8134        let f = var1 / var2;
8135        let df1 = n1f - 1.0;
8136        let df2 = n2f - 1.0;
8137
8138        // Two-tailed p-value: min(F.DIST(f), 1-F.DIST(f)) * 2
8139        let p_lower = f_cdf(f, df1, df2);
8140        let p_upper = 1.0 - p_lower;
8141        let p = 2.0 * p_lower.min(p_upper);
8142
8143        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(p)))
8144    }
8145}
8146
8147/* ─────────────────────────── CHISQ.TEST ──────────────────────────── */
8148
8149/// Returns the right-tail p-value from a chi-square goodness-of-fit style comparison.
8150///
8151/// `CHISQ.TEST` compares observed and expected values and computes `1 - CHISQ.DIST(...)`.
8152///
8153/// # Remarks
8154/// - `actual_range` and `expected_range` must contain the same number of numeric points.
8155/// - Expected values must be strictly greater than `0`.
8156/// - Requires at least two categories (`df >= 1`).
8157/// - Returns `#N/A` for length mismatches or empty inputs, and `#NUM!` for invalid expected values.
8158///
8159/// # Examples
8160///
8161/// ```yaml,sandbox
8162/// title: "Perfect match between observed and expected"
8163/// formula: "=CHISQ.TEST({20,30,50},{20,30,50})"
8164/// expected: 1
8165/// ```
8166///
8167/// ```yaml,sandbox
8168/// title: "Two-category chi-square test"
8169/// formula: "=CHISQ.TEST({18,22},{20,20})"
8170/// expected: 0.5270892568655381
8171/// ```
8172#[derive(Debug)]
8173pub struct ChisqTestFn;
8174/// [formualizer-docgen:schema:start]
8175/// Name: CHISQ.TEST
8176/// Type: ChisqTestFn
8177/// Min args: 2
8178/// Max args: 2
8179/// Variadic: false
8180/// Signature: CHISQ.TEST(arg1: number@range, arg2: number@range)
8181/// 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}
8182/// Caps: PURE
8183/// [formualizer-docgen:schema:end]
8184impl Function for ChisqTestFn {
8185    func_caps!(PURE);
8186    fn name(&self) -> &'static str {
8187        "CHISQ.TEST"
8188    }
8189    fn aliases(&self) -> &'static [&'static str] {
8190        &["CHITEST"]
8191    }
8192    fn min_args(&self) -> usize {
8193        2
8194    }
8195    fn arg_schema(&self) -> &'static [ArgSchema] {
8196        use std::sync::LazyLock;
8197        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
8198            vec![
8199                {
8200                    let mut s = ArgSchema::number_lenient_scalar();
8201                    s.shape = crate::args::ShapeKind::Range;
8202                    s.coercion = formualizer_common::CoercionPolicy::NumberLenientText;
8203                    s
8204                },
8205                {
8206                    let mut s = ArgSchema::number_lenient_scalar();
8207                    s.shape = crate::args::ShapeKind::Range;
8208                    s.coercion = formualizer_common::CoercionPolicy::NumberLenientText;
8209                    s
8210                },
8211            ]
8212        });
8213        &SCHEMA[..]
8214    }
8215    fn eval<'a, 'b, 'c>(
8216        &self,
8217        args: &'c [ArgumentHandle<'a, 'b>],
8218        _ctx: &dyn FunctionContext<'b>,
8219    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
8220        let actual = collect_numeric_stats(&args[0..1])?;
8221        let expected = collect_numeric_stats(&args[1..2])?;
8222
8223        // Arrays must have same length
8224        if actual.len() != expected.len() {
8225            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8226                ExcelError::new_na(),
8227            )));
8228        }
8229
8230        if actual.is_empty() {
8231            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8232                ExcelError::new_na(),
8233            )));
8234        }
8235
8236        // Calculate chi-squared statistic: sum((observed - expected)^2 / expected)
8237        let mut chi_sq = 0.0;
8238        for (obs, exp) in actual.iter().zip(expected.iter()) {
8239            if *exp <= 0.0 {
8240                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8241                    ExcelError::new_num(),
8242                )));
8243            }
8244            chi_sq += (obs - exp).powi(2) / exp;
8245        }
8246
8247        // Degrees of freedom = number of categories - 1
8248        let df = (actual.len() - 1) as f64;
8249
8250        if df < 1.0 {
8251            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8252                ExcelError::new_num(),
8253            )));
8254        }
8255
8256        // P-value = 1 - CHISQ.DIST(chi_sq, df, TRUE) = right-tail probability
8257        let p = 1.0 - chisq_cdf(chi_sq, df);
8258
8259        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(p)))
8260    }
8261}
8262
8263pub fn register_builtins() {
8264    use std::sync::Arc;
8265    crate::function_registry::register_function(Arc::new(ForecastLinearFn));
8266    crate::function_registry::register_function(Arc::new(LinestFn));
8267    crate::function_registry::register_function(Arc::new(LARGE));
8268    crate::function_registry::register_function(Arc::new(SMALL));
8269    crate::function_registry::register_function(Arc::new(MEDIAN));
8270    crate::function_registry::register_function(Arc::new(StdevSample));
8271    crate::function_registry::register_function(Arc::new(StdevPop));
8272    crate::function_registry::register_function(Arc::new(VarSample));
8273    crate::function_registry::register_function(Arc::new(VarPop));
8274    crate::function_registry::register_function(Arc::new(PercentileInc));
8275    crate::function_registry::register_function(Arc::new(PercentileExc));
8276    crate::function_registry::register_function(Arc::new(QuartileInc));
8277    crate::function_registry::register_function(Arc::new(QuartileExc));
8278    crate::function_registry::register_function(Arc::new(RankEqFn));
8279    crate::function_registry::register_function(Arc::new(RankAvgFn));
8280    crate::function_registry::register_function(Arc::new(ModeSingleFn));
8281    crate::function_registry::register_function(Arc::new(ModeMultiFn));
8282    crate::function_registry::register_function(Arc::new(ProductFn));
8283    crate::function_registry::register_function(Arc::new(GeomeanFn));
8284    crate::function_registry::register_function(Arc::new(HarmeanFn));
8285    crate::function_registry::register_function(Arc::new(AvedevFn));
8286    crate::function_registry::register_function(Arc::new(DevsqFn));
8287    crate::function_registry::register_function(Arc::new(MaxIfsFn));
8288    crate::function_registry::register_function(Arc::new(MinIfsFn));
8289    crate::function_registry::register_function(Arc::new(TrimmeanFn));
8290    crate::function_registry::register_function(Arc::new(CorrelFn));
8291    crate::function_registry::register_function(Arc::new(SlopeFn));
8292    crate::function_registry::register_function(Arc::new(InterceptFn));
8293    // Covariance and correlation functions
8294    crate::function_registry::register_function(Arc::new(CovariancePFn));
8295    crate::function_registry::register_function(Arc::new(CovarianceSFn));
8296    crate::function_registry::register_function(Arc::new(PearsonFn));
8297    crate::function_registry::register_function(Arc::new(RsqFn));
8298    crate::function_registry::register_function(Arc::new(SteyxFn));
8299    crate::function_registry::register_function(Arc::new(SkewFn));
8300    crate::function_registry::register_function(Arc::new(KurtFn));
8301    crate::function_registry::register_function(Arc::new(FisherFn));
8302    crate::function_registry::register_function(Arc::new(FisherInvFn));
8303    // Statistical distributions
8304    crate::function_registry::register_function(Arc::new(NormSDistFn));
8305    crate::function_registry::register_function(Arc::new(NormSInvFn));
8306    crate::function_registry::register_function(Arc::new(NormDistFn));
8307    crate::function_registry::register_function(Arc::new(NormInvFn));
8308    crate::function_registry::register_function(Arc::new(LognormDistFn));
8309    crate::function_registry::register_function(Arc::new(LognormInvFn));
8310    crate::function_registry::register_function(Arc::new(PhiFn));
8311    crate::function_registry::register_function(Arc::new(GaussFn));
8312    crate::function_registry::register_function(Arc::new(StandardizeFn));
8313    crate::function_registry::register_function(Arc::new(TDistFn));
8314    crate::function_registry::register_function(Arc::new(TInvFn));
8315    crate::function_registry::register_function(Arc::new(ChisqDistFn));
8316    crate::function_registry::register_function(Arc::new(ChisqInvFn));
8317    crate::function_registry::register_function(Arc::new(FDistFn));
8318    crate::function_registry::register_function(Arc::new(FInvFn));
8319    // Discrete distributions
8320    crate::function_registry::register_function(Arc::new(BinomDistFn));
8321    crate::function_registry::register_function(Arc::new(PoissonDistFn));
8322    crate::function_registry::register_function(Arc::new(ExponDistFn));
8323    crate::function_registry::register_function(Arc::new(GammaDistFn));
8324    // Additional distributions
8325    crate::function_registry::register_function(Arc::new(WeibullDistFn));
8326    crate::function_registry::register_function(Arc::new(BetaDistFn));
8327    crate::function_registry::register_function(Arc::new(NegbinomDistFn));
8328    crate::function_registry::register_function(Arc::new(HypgeomDistFn));
8329    // Confidence intervals and hypothesis testing
8330    crate::function_registry::register_function(Arc::new(ConfidenceNormFn));
8331    crate::function_registry::register_function(Arc::new(ConfidenceTFn));
8332    crate::function_registry::register_function(Arc::new(ZTestFn));
8333    // Regression and trend functions
8334    crate::function_registry::register_function(Arc::new(TrendFn));
8335    crate::function_registry::register_function(Arc::new(GrowthFn));
8336    crate::function_registry::register_function(Arc::new(LogestFn));
8337    // Percent rank and frequency functions
8338    crate::function_registry::register_function(Arc::new(PercentRankIncFn));
8339    crate::function_registry::register_function(Arc::new(PercentRankExcFn));
8340    crate::function_registry::register_function(Arc::new(FrequencyFn));
8341    // Hypothesis testing functions
8342    crate::function_registry::register_function(Arc::new(TDist2TFn));
8343    crate::function_registry::register_function(Arc::new(TInv2TFn));
8344    crate::function_registry::register_function(Arc::new(TTestFn));
8345    crate::function_registry::register_function(Arc::new(FTestFn));
8346    crate::function_registry::register_function(Arc::new(ChisqTestFn));
8347}
8348
8349#[cfg(test)]
8350mod tests_basic_stats {
8351    use super::*;
8352    use crate::test_workbook::TestWorkbook;
8353    use crate::traits::ArgumentHandle;
8354    use formualizer_common::LiteralValue;
8355    use formualizer_parse::parser::{ASTNode, ASTNodeType};
8356    fn interp(wb: &TestWorkbook) -> crate::interpreter::Interpreter<'_> {
8357        wb.interpreter()
8358    }
8359    fn arr(vals: Vec<f64>) -> ASTNode {
8360        ASTNode::new(
8361            ASTNodeType::Literal(LiteralValue::Array(vec![
8362                vals.into_iter().map(LiteralValue::Number).collect(),
8363            ])),
8364            None,
8365        )
8366    }
8367    #[test]
8368    fn median_even() {
8369        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(MEDIAN));
8370        let ctx = interp(&wb);
8371        let node = arr(vec![1.0, 3.0, 5.0, 7.0]);
8372        let f = ctx.context.get_function("", "MEDIAN").unwrap();
8373        let out = f
8374            .dispatch(
8375                &[ArgumentHandle::new(&node, &ctx)],
8376                &ctx.function_context(None),
8377            )
8378            .unwrap();
8379        assert_eq!(out, LiteralValue::Number(4.0));
8380    }
8381    #[test]
8382    fn median_odd() {
8383        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(MEDIAN));
8384        let ctx = interp(&wb);
8385        let node = arr(vec![1.0, 9.0, 5.0]);
8386        let f = ctx.context.get_function("", "MEDIAN").unwrap();
8387        let out = f
8388            .dispatch(
8389                &[ArgumentHandle::new(&node, &ctx)],
8390                &ctx.function_context(None),
8391            )
8392            .unwrap();
8393        assert_eq!(out, LiteralValue::Number(5.0));
8394    }
8395    #[test]
8396    fn large_basic() {
8397        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(LARGE));
8398        let ctx = interp(&wb);
8399        let nums = arr(vec![10.0, 20.0, 30.0]);
8400        let k = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(2.0)), None);
8401        let f = ctx.context.get_function("", "LARGE").unwrap();
8402        let out = f
8403            .dispatch(
8404                &[
8405                    ArgumentHandle::new(&nums, &ctx),
8406                    ArgumentHandle::new(&k, &ctx),
8407                ],
8408                &ctx.function_context(None),
8409            )
8410            .unwrap();
8411        assert_eq!(out, LiteralValue::Number(20.0));
8412    }
8413    #[test]
8414    fn small_basic() {
8415        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(SMALL));
8416        let ctx = interp(&wb);
8417        let nums = arr(vec![10.0, 20.0, 30.0]);
8418        let k = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(2.0)), None);
8419        let f = ctx.context.get_function("", "SMALL").unwrap();
8420        let out = f
8421            .dispatch(
8422                &[
8423                    ArgumentHandle::new(&nums, &ctx),
8424                    ArgumentHandle::new(&k, &ctx),
8425                ],
8426                &ctx.function_context(None),
8427            )
8428            .unwrap();
8429        assert_eq!(out, LiteralValue::Number(20.0));
8430    }
8431    #[test]
8432    fn percentile_inc_quarter() {
8433        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(PercentileInc));
8434        let ctx = interp(&wb);
8435        let nums = arr(vec![1.0, 2.0, 3.0, 4.0]);
8436        let p = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(0.25)), None);
8437        let f = ctx.context.get_function("", "PERCENTILE.INC").unwrap();
8438        match f
8439            .dispatch(
8440                &[
8441                    ArgumentHandle::new(&nums, &ctx),
8442                    ArgumentHandle::new(&p, &ctx),
8443                ],
8444                &ctx.function_context(None),
8445            )
8446            .unwrap()
8447            .into_literal()
8448        {
8449            LiteralValue::Number(v) => assert!((v - 1.75).abs() < 1e-9),
8450            other => panic!("unexpected {other:?}"),
8451        }
8452    }
8453    #[test]
8454    fn rank_eq_descending() {
8455        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(RankEqFn));
8456        let ctx = interp(&wb);
8457        // target 5 among {10,5,1} descending => ranks 1,2,3 => expect 2
8458        let target = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(5.0)), None);
8459        let arr_node = arr(vec![10.0, 5.0, 1.0]);
8460        let f = ctx.context.get_function("", "RANK.EQ").unwrap();
8461        let out = f
8462            .dispatch(
8463                &[
8464                    ArgumentHandle::new(&target, &ctx),
8465                    ArgumentHandle::new(&arr_node, &ctx),
8466                ],
8467                &ctx.function_context(None),
8468            )
8469            .unwrap();
8470        assert_eq!(out, LiteralValue::Number(2.0));
8471    }
8472    #[test]
8473    fn rank_eq_ascending_order_arg() {
8474        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(RankEqFn));
8475        let ctx = interp(&wb);
8476        // ascending order=1: array {1,5,10}; target 5 => rank 2
8477        let target = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(5.0)), None);
8478        let arr_node = arr(vec![1.0, 5.0, 10.0]);
8479        let order = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(1.0)), None);
8480        let f = ctx.context.get_function("", "RANK.EQ").unwrap();
8481        let out = f
8482            .dispatch(
8483                &[
8484                    ArgumentHandle::new(&target, &ctx),
8485                    ArgumentHandle::new(&arr_node, &ctx),
8486                    ArgumentHandle::new(&order, &ctx),
8487                ],
8488                &ctx.function_context(None),
8489            )
8490            .unwrap();
8491        assert_eq!(out, LiteralValue::Number(2.0));
8492    }
8493    #[test]
8494    fn rank_avg_ties() {
8495        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(RankAvgFn));
8496        let ctx = interp(&wb);
8497        // descending array {5,5,1} target 5 positions 1 and 2 avg -> 1.5
8498        let target = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(5.0)), None);
8499        let arr_node = arr(vec![5.0, 5.0, 1.0]);
8500        let f = ctx.context.get_function("", "RANK.AVG").unwrap();
8501        let out = f
8502            .dispatch(
8503                &[
8504                    ArgumentHandle::new(&target, &ctx),
8505                    ArgumentHandle::new(&arr_node, &ctx),
8506                ],
8507                &ctx.function_context(None),
8508            )
8509            .unwrap()
8510            .into_literal();
8511        match out {
8512            LiteralValue::Number(v) => assert!((v - 1.5).abs() < 1e-12),
8513            other => panic!("expected number got {other:?}"),
8514        }
8515    }
8516    #[test]
8517    fn stdev_var_sample_population() {
8518        let wb = TestWorkbook::new()
8519            .with_function(std::sync::Arc::new(StdevSample))
8520            .with_function(std::sync::Arc::new(StdevPop))
8521            .with_function(std::sync::Arc::new(VarSample))
8522            .with_function(std::sync::Arc::new(VarPop));
8523        let ctx = interp(&wb);
8524        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...
8525        let stdev_p = ctx.context.get_function("", "STDEV.P").unwrap();
8526        let stdev_s = ctx.context.get_function("", "STDEV.S").unwrap();
8527        let var_p = ctx.context.get_function("", "VAR.P").unwrap();
8528        let var_s = ctx.context.get_function("", "VAR.S").unwrap();
8529        let args = [ArgumentHandle::new(&arr_node, &ctx)];
8530        match var_p
8531            .dispatch(&args, &ctx.function_context(None))
8532            .unwrap()
8533            .into_literal()
8534        {
8535            LiteralValue::Number(v) => assert!((v - 4.0).abs() < 1e-12),
8536            other => panic!("unexpected {other:?}"),
8537        }
8538        match var_s
8539            .dispatch(&args, &ctx.function_context(None))
8540            .unwrap()
8541            .into_literal()
8542        {
8543            LiteralValue::Number(v) => assert!((v - 4.571428571428571).abs() < 1e-9),
8544            other => panic!("unexpected {other:?}"),
8545        }
8546        match stdev_p
8547            .dispatch(&args, &ctx.function_context(None))
8548            .unwrap()
8549            .into_literal()
8550        {
8551            LiteralValue::Number(v) => assert!((v - 2.0).abs() < 1e-12),
8552            other => panic!("unexpected {other:?}"),
8553        }
8554        match stdev_s
8555            .dispatch(&args, &ctx.function_context(None))
8556            .unwrap()
8557            .into_literal()
8558        {
8559            LiteralValue::Number(v) => assert!((v - 2.138089935).abs() < 1e-9),
8560            other => panic!("unexpected {other:?}"),
8561        }
8562    }
8563    #[test]
8564    fn quartile_inc_exc() {
8565        let wb = TestWorkbook::new()
8566            .with_function(std::sync::Arc::new(QuartileInc))
8567            .with_function(std::sync::Arc::new(QuartileExc));
8568        let ctx = interp(&wb);
8569        let arr_node = arr(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
8570        let q1 = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(1.0)), None);
8571        let q2 = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(2.0)), None);
8572        let f_inc = ctx.context.get_function("", "QUARTILE.INC").unwrap();
8573        let f_exc = ctx.context.get_function("", "QUARTILE.EXC").unwrap();
8574        let arg_inc_q1 = [
8575            ArgumentHandle::new(&arr_node, &ctx),
8576            ArgumentHandle::new(&q1, &ctx),
8577        ];
8578        let arg_inc_q2 = [
8579            ArgumentHandle::new(&arr_node, &ctx),
8580            ArgumentHandle::new(&q2, &ctx),
8581        ];
8582        match f_inc
8583            .dispatch(&arg_inc_q1, &ctx.function_context(None))
8584            .unwrap()
8585            .into_literal()
8586        {
8587            LiteralValue::Number(v) => assert!((v - 2.0).abs() < 1e-12),
8588            other => panic!("unexpected {other:?}"),
8589        }
8590        match f_inc
8591            .dispatch(&arg_inc_q2, &ctx.function_context(None))
8592            .unwrap()
8593            .into_literal()
8594        {
8595            LiteralValue::Number(v) => assert!((v - 3.0).abs() < 1e-12),
8596            other => panic!("unexpected {other:?}"),
8597        }
8598        // QUARTILE.EXC Q1 for 5-point set uses exclusive percentile => 1.5
8599        match f_exc
8600            .dispatch(&arg_inc_q1, &ctx.function_context(None))
8601            .unwrap()
8602            .into_literal()
8603        {
8604            LiteralValue::Number(v) => assert!((v - 1.5).abs() < 1e-12),
8605            other => panic!("unexpected {other:?}"),
8606        }
8607        match f_exc
8608            .dispatch(&arg_inc_q2, &ctx.function_context(None))
8609            .unwrap()
8610            .into_literal()
8611        {
8612            LiteralValue::Number(v) => assert!((v - 3.0).abs() < 1e-12),
8613            other => panic!("unexpected {other:?}"),
8614        }
8615    }
8616
8617    // --- eval()/dispatch equivalence tests for variance / stdev ---
8618    #[test]
8619    fn fold_equivalence_var_stdev() {
8620        use crate::function::Function as _; // trait import
8621        let wb = TestWorkbook::new()
8622            .with_function(std::sync::Arc::new(VarSample))
8623            .with_function(std::sync::Arc::new(VarPop))
8624            .with_function(std::sync::Arc::new(StdevSample))
8625            .with_function(std::sync::Arc::new(StdevPop));
8626        let ctx = interp(&wb);
8627        let arr_node = arr(vec![1.0, 2.0, 5.0, 5.0, 9.0]);
8628        let args = [ArgumentHandle::new(&arr_node, &ctx)];
8629
8630        let var_s_fn = VarSample; // concrete instance to call eval()
8631        let var_p_fn = VarPop;
8632        let stdev_s_fn = StdevSample;
8633        let stdev_p_fn = StdevPop;
8634
8635        let fctx = ctx.function_context(None);
8636        // Dispatch results (will use fold path)
8637        let disp_var_s = ctx
8638            .context
8639            .get_function("", "VAR.S")
8640            .unwrap()
8641            .dispatch(&args, &fctx)
8642            .unwrap()
8643            .into_literal();
8644        let disp_var_p = ctx
8645            .context
8646            .get_function("", "VAR.P")
8647            .unwrap()
8648            .dispatch(&args, &fctx)
8649            .unwrap()
8650            .into_literal();
8651        let disp_stdev_s = ctx
8652            .context
8653            .get_function("", "STDEV.S")
8654            .unwrap()
8655            .dispatch(&args, &fctx)
8656            .unwrap()
8657            .into_literal();
8658        let disp_stdev_p = ctx
8659            .context
8660            .get_function("", "STDEV.P")
8661            .unwrap()
8662            .dispatch(&args, &fctx)
8663            .unwrap()
8664            .into_literal();
8665
8666        // Scalar path results
8667        let scalar_var_s = var_s_fn.eval(&args, &fctx).unwrap().into_literal();
8668        let scalar_var_p = var_p_fn.eval(&args, &fctx).unwrap().into_literal();
8669        let scalar_stdev_s = stdev_s_fn.eval(&args, &fctx).unwrap().into_literal();
8670        let scalar_stdev_p = stdev_p_fn.eval(&args, &fctx).unwrap().into_literal();
8671
8672        fn assert_close(a: &LiteralValue, b: &LiteralValue) {
8673            match (a, b) {
8674                (LiteralValue::Number(x), LiteralValue::Number(y)) => {
8675                    assert!((x - y).abs() < 1e-12, "mismatch {x} vs {y}")
8676                }
8677                _ => assert_eq!(a, b),
8678            }
8679        }
8680        assert_close(&disp_var_s, &scalar_var_s);
8681        assert_close(&disp_var_p, &scalar_var_p);
8682        assert_close(&disp_stdev_s, &scalar_stdev_s);
8683        assert_close(&disp_stdev_p, &scalar_stdev_p);
8684    }
8685
8686    #[test]
8687    fn fold_equivalence_edge_cases() {
8688        use crate::function::Function as _;
8689        let wb = TestWorkbook::new()
8690            .with_function(std::sync::Arc::new(VarSample))
8691            .with_function(std::sync::Arc::new(VarPop))
8692            .with_function(std::sync::Arc::new(StdevSample))
8693            .with_function(std::sync::Arc::new(StdevPop));
8694        let ctx = interp(&wb);
8695        // Single numeric element -> sample variance/div0, population variance 0
8696        let single = arr(vec![42.0]);
8697        let args_single = [ArgumentHandle::new(&single, &ctx)];
8698        let fctx = ctx.function_context(None);
8699        let disp_var_s = ctx
8700            .context
8701            .get_function("", "VAR.S")
8702            .unwrap()
8703            .dispatch(&args_single, &fctx)
8704            .unwrap();
8705        let scalar_var_s = VarSample.eval(&args_single, &fctx).unwrap().into_literal();
8706        assert_eq!(disp_var_s, scalar_var_s);
8707        let disp_var_p = ctx
8708            .context
8709            .get_function("", "VAR.P")
8710            .unwrap()
8711            .dispatch(&args_single, &fctx)
8712            .unwrap();
8713        let scalar_var_p = VarPop.eval(&args_single, &fctx).unwrap().into_literal();
8714        assert_eq!(disp_var_p, scalar_var_p);
8715        let disp_stdev_p = ctx
8716            .context
8717            .get_function("", "STDEV.P")
8718            .unwrap()
8719            .dispatch(&args_single, &fctx)
8720            .unwrap();
8721        let scalar_stdev_p = StdevPop.eval(&args_single, &fctx).unwrap().into_literal();
8722        assert_eq!(disp_stdev_p, scalar_stdev_p);
8723        let disp_stdev_s = ctx
8724            .context
8725            .get_function("", "STDEV.S")
8726            .unwrap()
8727            .dispatch(&args_single, &fctx)
8728            .unwrap();
8729        let scalar_stdev_s = StdevSample
8730            .eval(&args_single, &fctx)
8731            .unwrap()
8732            .into_literal();
8733        assert_eq!(disp_stdev_s, scalar_stdev_s);
8734    }
8735
8736    #[test]
8737    fn legacy_aliases_match_modern() {
8738        let wb = TestWorkbook::new()
8739            .with_function(std::sync::Arc::new(PercentileInc))
8740            .with_function(std::sync::Arc::new(QuartileInc))
8741            .with_function(std::sync::Arc::new(RankEqFn));
8742        let ctx = interp(&wb);
8743        let arr_node = arr(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
8744        let p = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(0.4)), None);
8745        let q2 = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(2.0)), None);
8746        let target = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(4.0)), None);
8747        let args_p = [
8748            ArgumentHandle::new(&arr_node, &ctx),
8749            ArgumentHandle::new(&p, &ctx),
8750        ];
8751        let args_q = [
8752            ArgumentHandle::new(&arr_node, &ctx),
8753            ArgumentHandle::new(&q2, &ctx),
8754        ];
8755        let args_rank = [
8756            ArgumentHandle::new(&target, &ctx),
8757            ArgumentHandle::new(&arr_node, &ctx),
8758        ];
8759        let modern_p = ctx
8760            .context
8761            .get_function("", "PERCENTILE.INC")
8762            .unwrap()
8763            .dispatch(&args_p, &ctx.function_context(None))
8764            .unwrap()
8765            .into_literal();
8766        let legacy_p = ctx
8767            .context
8768            .get_function("", "PERCENTILE")
8769            .unwrap()
8770            .dispatch(&args_p, &ctx.function_context(None))
8771            .unwrap()
8772            .into_literal();
8773        assert_eq!(modern_p, legacy_p);
8774        let modern_q = ctx
8775            .context
8776            .get_function("", "QUARTILE.INC")
8777            .unwrap()
8778            .dispatch(&args_q, &ctx.function_context(None))
8779            .unwrap()
8780            .into_literal();
8781        let legacy_q = ctx
8782            .context
8783            .get_function("", "QUARTILE")
8784            .unwrap()
8785            .dispatch(&args_q, &ctx.function_context(None))
8786            .unwrap()
8787            .into_literal();
8788        assert_eq!(modern_q, legacy_q);
8789        let modern_rank = ctx
8790            .context
8791            .get_function("", "RANK.EQ")
8792            .unwrap()
8793            .dispatch(&args_rank, &ctx.function_context(None))
8794            .unwrap()
8795            .into_literal();
8796        let legacy_rank = ctx
8797            .context
8798            .get_function("", "RANK")
8799            .unwrap()
8800            .dispatch(&args_rank, &ctx.function_context(None))
8801            .unwrap()
8802            .into_literal();
8803        assert_eq!(modern_rank, legacy_rank);
8804    }
8805
8806    #[test]
8807    fn mode_single_basic_and_alias() {
8808        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(ModeSingleFn));
8809        let ctx = interp(&wb);
8810        let arr_node = arr(vec![5.0, 2.0, 2.0, 3.0, 3.0, 3.0]);
8811        let args = [ArgumentHandle::new(&arr_node, &ctx)];
8812        let mode_sngl = ctx
8813            .context
8814            .get_function("", "MODE.SNGL")
8815            .unwrap()
8816            .dispatch(&args, &ctx.function_context(None))
8817            .unwrap()
8818            .into_literal();
8819        assert_eq!(mode_sngl, LiteralValue::Number(3.0));
8820        let mode_alias = ctx
8821            .context
8822            .get_function("", "MODE")
8823            .unwrap()
8824            .dispatch(&args, &ctx.function_context(None))
8825            .unwrap()
8826            .into_literal();
8827        assert_eq!(mode_alias, mode_sngl);
8828    }
8829
8830    #[test]
8831    fn mode_single_no_duplicates() {
8832        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(ModeSingleFn));
8833        let ctx = interp(&wb);
8834        let arr_node = arr(vec![1.0, 2.0, 3.0]);
8835        let args = [ArgumentHandle::new(&arr_node, &ctx)];
8836        let out = ctx
8837            .context
8838            .get_function("", "MODE.SNGL")
8839            .unwrap()
8840            .dispatch(&args, &ctx.function_context(None))
8841            .unwrap()
8842            .into_literal();
8843        match out {
8844            LiteralValue::Error(e) => assert!(e.to_string().contains("#N/A")),
8845            _ => panic!("expected #N/A"),
8846        }
8847    }
8848
8849    #[test]
8850    fn mode_multi_basic() {
8851        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(ModeMultiFn));
8852        let ctx = interp(&wb);
8853        let arr_node = arr(vec![2.0, 3.0, 2.0, 4.0, 3.0, 5.0, 2.0, 3.0]);
8854        let args = [ArgumentHandle::new(&arr_node, &ctx)];
8855        let out = ctx
8856            .context
8857            .get_function("", "MODE.MULT")
8858            .unwrap()
8859            .dispatch(&args, &ctx.function_context(None))
8860            .unwrap()
8861            .into_literal();
8862        let expected = LiteralValue::Array(vec![
8863            vec![LiteralValue::Number(2.0)],
8864            vec![LiteralValue::Number(3.0)],
8865        ]);
8866        assert_eq!(out, expected);
8867    }
8868
8869    #[test]
8870    fn large_small_fold_vs_scalar() {
8871        let wb = TestWorkbook::new()
8872            .with_function(std::sync::Arc::new(LARGE))
8873            .with_function(std::sync::Arc::new(SMALL));
8874        let ctx = interp(&wb);
8875        let arr_node = arr(vec![10.0, 5.0, 7.0, 12.0, 9.0]);
8876        let k_node = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(2.0)), None);
8877        let args = [
8878            ArgumentHandle::new(&arr_node, &ctx),
8879            ArgumentHandle::new(&k_node, &ctx),
8880        ];
8881        let f_large = ctx.context.get_function("", "LARGE").unwrap();
8882        let disp_large = f_large
8883            .dispatch(&args, &ctx.function_context(None))
8884            .unwrap()
8885            .into_literal();
8886        let scalar_large = LARGE
8887            .eval(&args, &ctx.function_context(None))
8888            .unwrap()
8889            .into_literal();
8890        assert_eq!(disp_large, scalar_large);
8891        let f_small = ctx.context.get_function("", "SMALL").unwrap();
8892        let disp_small = f_small
8893            .dispatch(&args, &ctx.function_context(None))
8894            .unwrap()
8895            .into_literal();
8896        let scalar_small = SMALL
8897            .eval(&args, &ctx.function_context(None))
8898            .unwrap()
8899            .into_literal();
8900        assert_eq!(disp_small, scalar_small);
8901    }
8902
8903    #[test]
8904    fn mode_fold_vs_scalar() {
8905        let wb = TestWorkbook::new()
8906            .with_function(std::sync::Arc::new(ModeSingleFn))
8907            .with_function(std::sync::Arc::new(ModeMultiFn));
8908        let ctx = interp(&wb);
8909        let arr_node = arr(vec![2.0, 3.0, 2.0, 4.0, 3.0, 3.0, 2.0]);
8910        let args = [ArgumentHandle::new(&arr_node, &ctx)];
8911        let f_single = ctx.context.get_function("", "MODE.SNGL").unwrap();
8912        let disp_single = f_single
8913            .dispatch(&args, &ctx.function_context(None))
8914            .unwrap()
8915            .into_literal();
8916        let scalar_single = ModeSingleFn
8917            .eval(&args, &ctx.function_context(None))
8918            .unwrap()
8919            .into_literal();
8920        assert_eq!(disp_single, scalar_single);
8921        let f_multi = ctx.context.get_function("", "MODE.MULT").unwrap();
8922        let disp_multi = f_multi
8923            .dispatch(&args, &ctx.function_context(None))
8924            .unwrap()
8925            .into_literal();
8926        let scalar_multi = ModeMultiFn
8927            .eval(&args, &ctx.function_context(None))
8928            .unwrap()
8929            .into_literal();
8930        assert_eq!(disp_multi, scalar_multi);
8931    }
8932
8933    #[test]
8934    fn median_fold_vs_scalar_even() {
8935        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(MEDIAN));
8936        let ctx = interp(&wb);
8937        let arr_node = arr(vec![7.0, 1.0, 9.0, 5.0]); // sorted: 1,5,7,9 median=(5+7)/2=6
8938        let args = [ArgumentHandle::new(&arr_node, &ctx)];
8939        let f_med = ctx.context.get_function("", "MEDIAN").unwrap();
8940        let disp = f_med
8941            .dispatch(&args, &ctx.function_context(None))
8942            .unwrap()
8943            .into_literal();
8944        let scalar = MEDIAN
8945            .eval(&args, &ctx.function_context(None))
8946            .unwrap()
8947            .into_literal();
8948        assert_eq!(disp, scalar);
8949        assert_eq!(disp, LiteralValue::Number(6.0));
8950    }
8951
8952    #[test]
8953    fn median_fold_vs_scalar_odd() {
8954        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(MEDIAN));
8955        let ctx = interp(&wb);
8956        let arr_node = arr(vec![9.0, 2.0, 5.0]); // sorted 2,5,9 median=5
8957        let args = [ArgumentHandle::new(&arr_node, &ctx)];
8958        let f_med = ctx.context.get_function("", "MEDIAN").unwrap();
8959        let disp = f_med
8960            .dispatch(&args, &ctx.function_context(None))
8961            .unwrap()
8962            .into_literal();
8963        let scalar = MEDIAN
8964            .eval(&args, &ctx.function_context(None))
8965            .unwrap()
8966            .into_literal();
8967        assert_eq!(disp, scalar);
8968        assert_eq!(disp, LiteralValue::Number(5.0));
8969    }
8970
8971    // Newly added edge case tests for statistical semantics.
8972    #[test]
8973    fn percentile_inc_edges() {
8974        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(PercentileInc));
8975        let ctx = interp(&wb);
8976        let arr_node = arr(vec![10.0, 20.0, 30.0, 40.0]);
8977        let p0 = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(0.0)), None);
8978        let p1 = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(1.0)), None);
8979        let f = ctx.context.get_function("", "PERCENTILE.INC").unwrap();
8980        let args0 = [
8981            ArgumentHandle::new(&arr_node, &ctx),
8982            ArgumentHandle::new(&p0, &ctx),
8983        ];
8984        let args1 = [
8985            ArgumentHandle::new(&arr_node, &ctx),
8986            ArgumentHandle::new(&p1, &ctx),
8987        ];
8988        assert_eq!(
8989            f.dispatch(&args0, &ctx.function_context(None))
8990                .unwrap()
8991                .into_literal(),
8992            LiteralValue::Number(10.0)
8993        );
8994        assert_eq!(
8995            f.dispatch(&args1, &ctx.function_context(None))
8996                .unwrap()
8997                .into_literal(),
8998            LiteralValue::Number(40.0)
8999        );
9000    }
9001
9002    #[test]
9003    fn percentile_exc_invalid() {
9004        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(PercentileExc));
9005        let ctx = interp(&wb);
9006        let arr_node = arr(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
9007        let p_bad0 = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(0.0)), None);
9008        let p_bad1 = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(1.0)), None);
9009        let f = ctx.context.get_function("", "PERCENTILE.EXC").unwrap();
9010        for bad in [&p_bad0, &p_bad1] {
9011            let args = [
9012                ArgumentHandle::new(&arr_node, &ctx),
9013                ArgumentHandle::new(bad, &ctx),
9014            ];
9015            match f
9016                .dispatch(&args, &ctx.function_context(None))
9017                .unwrap()
9018                .into_literal()
9019            {
9020                LiteralValue::Error(e) => assert!(e.to_string().contains("#NUM!")),
9021                other => panic!("expected #NUM! got {other:?}"),
9022            }
9023        }
9024    }
9025
9026    #[test]
9027    fn quartile_invalids() {
9028        let wb = TestWorkbook::new()
9029            .with_function(std::sync::Arc::new(QuartileInc))
9030            .with_function(std::sync::Arc::new(QuartileExc));
9031        let ctx = interp(&wb);
9032        let arr_node = arr(vec![1.0, 2.0, 3.0]);
9033        // QUARTILE.INC invalid q=5
9034        let q5 = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(5.0)), None);
9035        let args_bad_inc = [
9036            ArgumentHandle::new(&arr_node, &ctx),
9037            ArgumentHandle::new(&q5, &ctx),
9038        ];
9039        match ctx
9040            .context
9041            .get_function("", "QUARTILE.INC")
9042            .unwrap()
9043            .dispatch(&args_bad_inc, &ctx.function_context(None))
9044            .unwrap()
9045            .into_literal()
9046        {
9047            LiteralValue::Error(e) => assert!(e.to_string().contains("#NUM!")),
9048            other => panic!("expected #NUM! {other:?}"),
9049        }
9050        // QUARTILE.EXC invalid q=0
9051        let q0 = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(0.0)), None);
9052        let args_bad_exc = [
9053            ArgumentHandle::new(&arr_node, &ctx),
9054            ArgumentHandle::new(&q0, &ctx),
9055        ];
9056        match ctx
9057            .context
9058            .get_function("", "QUARTILE.EXC")
9059            .unwrap()
9060            .dispatch(&args_bad_exc, &ctx.function_context(None))
9061            .unwrap()
9062            .into_literal()
9063        {
9064            LiteralValue::Error(e) => assert!(e.to_string().contains("#NUM!")),
9065            other => panic!("expected #NUM! {other:?}"),
9066        }
9067    }
9068
9069    #[test]
9070    fn rank_target_not_found() {
9071        let wb = TestWorkbook::new()
9072            .with_function(std::sync::Arc::new(RankEqFn))
9073            .with_function(std::sync::Arc::new(RankAvgFn));
9074        let ctx = interp(&wb);
9075        let arr_node = arr(vec![1.0, 2.0, 3.0]);
9076        let target = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(4.0)), None);
9077        let args = [
9078            ArgumentHandle::new(&target, &ctx),
9079            ArgumentHandle::new(&arr_node, &ctx),
9080        ];
9081        for name in ["RANK.EQ", "RANK.AVG"] {
9082            match ctx
9083                .context
9084                .get_function("", name)
9085                .unwrap()
9086                .dispatch(&args, &ctx.function_context(None))
9087                .unwrap()
9088                .into_literal()
9089            {
9090                LiteralValue::Error(e) => assert!(e.to_string().contains("#N/A")),
9091                other => panic!("expected #N/A {other:?}"),
9092            }
9093        }
9094    }
9095
9096    #[test]
9097    fn mode_mult_ordering() {
9098        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(ModeMultiFn));
9099        let ctx = interp(&wb);
9100        // Two modes with same frequency; ensure ascending ordering in array result
9101        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
9102        let args = [ArgumentHandle::new(&arr_node, &ctx)];
9103        let out = ctx
9104            .context
9105            .get_function("", "MODE.MULT")
9106            .unwrap()
9107            .dispatch(&args, &ctx.function_context(None))
9108            .unwrap()
9109            .into_literal();
9110        match out {
9111            LiteralValue::Array(rows) => {
9112                let vals: Vec<f64> = rows
9113                    .into_iter()
9114                    .map(|r| {
9115                        if let LiteralValue::Number(n) = r[0] {
9116                            n
9117                        } else {
9118                            panic!("expected number")
9119                        }
9120                    })
9121                    .collect();
9122                assert_eq!(vals, vec![2.0, 5.0]);
9123            }
9124            other => panic!("expected array {other:?}"),
9125        }
9126    }
9127
9128    #[test]
9129    fn boolean_and_text_in_range_are_ignored() {
9130        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(StdevPop));
9131        let ctx = interp(&wb);
9132        let mixed = ASTNode::new(
9133            ASTNodeType::Literal(LiteralValue::Array(vec![vec![
9134                LiteralValue::Number(1.0),
9135                LiteralValue::Text("ABC".into()),
9136                LiteralValue::Boolean(true),
9137                LiteralValue::Number(4.0),
9138            ]])),
9139            None,
9140        );
9141        let f = ctx.context.get_function("", "STDEV.P").unwrap();
9142        let out = f
9143            .dispatch(
9144                &[ArgumentHandle::new(&mixed, &ctx)],
9145                &ctx.function_context(None),
9146            )
9147            .unwrap()
9148            .into_literal();
9149        // NOTE: Inline array literal is treated as a direct scalar argument (not a range reference),
9150        // so boolean TRUE is coerced to 1. Dataset becomes {1,1,4}; population stdev = sqrt(6/3)=sqrt(2).
9151        match out {
9152            LiteralValue::Number(v) => {
9153                assert!((v - 2f64.sqrt()).abs() < 1e-12, "expected sqrt(2) got {v}")
9154            }
9155            other => panic!("unexpected {other:?}"),
9156        }
9157    }
9158
9159    #[test]
9160    fn boolean_direct_arg_coerces() {
9161        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(StdevPop));
9162        let ctx = interp(&wb);
9163        let one = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(1.0)), None);
9164        let t = ASTNode::new(ASTNodeType::Literal(LiteralValue::Boolean(true)), None);
9165        let f = ctx.context.get_function("", "STDEV.P").unwrap();
9166        let args = [
9167            ArgumentHandle::new(&one, &ctx),
9168            ArgumentHandle::new(&t, &ctx),
9169        ];
9170        let out = f
9171            .dispatch(&args, &ctx.function_context(None))
9172            .unwrap()
9173            .into_literal();
9174        assert_eq!(out, LiteralValue::Number(0.0));
9175    }
9176}