formualizer_eval/builtins/stats/mod.rs
1//! Statistical basic functions (Sprint 6)
2//!
3//! Implementations target Excel semantic parity for:
4//! LARGE, SMALL, RANK.EQ, RANK.AVG, MEDIAN, STDEV.S, STDEV.P, VAR.S, VAR.P,
5//! PERCENTILE.INC, PERCENTILE.EXC, QUARTILE.INC, QUARTILE.EXC.
6//!
7//! Notes:
8//! - We materialize numeric values into a Vec<f64>. Functions that need only one or two order
9//! statistics (LARGE, SMALL, MEDIAN, PERCENTILE.INC/.EXC, QUARTILE.INC/.EXC) use quickselect
10//! (`select_nth_unstable_by`) instead of a full sort. Functions that need the complete sorted
11//! order keep the sort: RANK.EQ/RANK.AVG (positional scan), MODE.SNGL/MODE.MULT (run-length
12//! over sorted order), TRIMMEAN (f64 summation order over the sorted middle slice must stay
13//! bit-identical), PERCENTRANK.INC/.EXC (interpolating scan), FREQUENCY (sorted bins).
14//! - Text/boolean coercion nuance: For Excel statistical functions, values coming from range
15//! references should ignore text and logical values (they are skipped), while direct scalar
16//! arguments still coerce (e.g. =STDEV(1,TRUE) treats TRUE as 1). This file now implements that
17//! distinction. TODO(excel-nuance): refine numeric text literal vs non‑numeric text handling.
18//! - Errors encountered in any argument propagate immediately.
19//! - Empty numeric sets produce Excel-specific errors (#NUM! for LARGE/SMALL, #N/A for rank target
20//! out of range, #DIV/0! for STDEV/VAR sample with n < 2, etc.).
21
22use super::super::builtins::utils::{ARG_RANGE_NUM_LENIENT_ONE, coerce_num};
23use crate::args::ArgSchema;
24use crate::function::Function;
25use crate::function_contract::FunctionDependencyContract;
26use crate::traits::{ArgumentHandle, FunctionContext};
27use formualizer_common::{ExcelError, LiteralValue};
28// use std::collections::BTreeMap; // removed unused import
29use formualizer_macros::func_caps;
30
31fn scalar_like_value(arg: &ArgumentHandle<'_, '_>) -> Result<LiteralValue, ExcelError> {
32 Ok(match arg.value()? {
33 crate::traits::CalcValue::Scalar(v) => v,
34 crate::traits::CalcValue::Range(rv) => rv.get_cell(0, 0),
35 crate::traits::CalcValue::Callable(_) => LiteralValue::Error(
36 ExcelError::new(formualizer_common::ExcelErrorKind::Calc)
37 .with_message("LAMBDA value must be invoked"),
38 ),
39 })
40}
41
42/// Collect numeric inputs applying Excel statistical semantics:
43/// - Range references: include only numeric cells; skip text, logical, blank. Errors propagate.
44/// - Direct scalar arguments: attempt numeric coercion (so TRUE/FALSE, numeric text are included if
45/// coerce_num succeeds). Non-numeric text is ignored (Excel would treat a direct non-numeric text
46/// argument as #VALUE! in some contexts; covered by TODO for finer parity).
47fn collect_numeric_stats(args: &[ArgumentHandle]) -> Result<Vec<f64>, ExcelError> {
48 let mut out = Vec::new();
49 for a in args {
50 // Special-case: inline array literal argument should be treated like a list of direct scalar
51 // arguments (not a by-ref range). This allows boolean/text coercion per element akin to
52 // passing multiple scalars to the function.
53 if let Some(arr) = a.inline_array_literal()? {
54 for row in arr.into_iter() {
55 for cell in row.into_iter() {
56 match cell {
57 LiteralValue::Error(e) => return Err(e),
58 other => {
59 if let Ok(n) = coerce_num(&other) {
60 out.push(n);
61 }
62 }
63 }
64 }
65 }
66 continue;
67 }
68
69 if let Ok(view) = a.range_view() {
70 view.for_each_cell(&mut |v| {
71 match v {
72 LiteralValue::Error(e) => return Err(e.clone()),
73 LiteralValue::Number(n) => out.push(*n),
74 LiteralValue::Int(i) => out.push(*i as f64),
75 _ => {}
76 }
77 Ok(())
78 })?;
79 } else {
80 let v = scalar_like_value(a)?;
81 match v {
82 LiteralValue::Error(e) => return Err(e),
83 other => {
84 if let Ok(n) = coerce_num(&other) {
85 out.push(n);
86 }
87 }
88 }
89 }
90 }
91 Ok(out)
92}
93
94/* ─────────────── order-statistic selection (quickselect) ───────────────
95 *
96 * LARGE/SMALL/MEDIAN and the PERCENTILE/QUARTILE family need at most two
97 * adjacent order statistics, so a full O(n log n) sort is wasted work;
98 * `select_nth_unstable_by` (quickselect) finds them in expected O(n).
99 *
100 * The comparator is byte-for-byte the one used by the full sorts it
101 * replaces (`partial_cmp().unwrap()`): `collect_numeric_stats` only yields
102 * coerced finite numerics, and a NaN would have panicked the old sort the
103 * same way. Ties are exact duplicates for f64s compared `Equal` (modulo
104 * the ±0.0 sign bit), so the selected element is bit-identical to the
105 * sorted element at the same index.
106 */
107
108/// k-th order statistic (0-based, ascending). Reorders `nums` in place.
109fn nth_smallest(nums: &mut [f64], k: usize) -> f64 {
110 let (_, kth, _) = nums.select_nth_unstable_by(k, |a, b| a.partial_cmp(b).unwrap());
111 *kth
112}
113
114/// The adjacent order statistics (k, k+1) ascending: one quickselect for k,
115/// then the (k+1)-th is the minimum of the right partition.
116/// Requires `k + 1 < nums.len()`.
117fn adjacent_smallest(nums: &mut [f64], k: usize) -> (f64, f64) {
118 let (_, kth, right) = nums.select_nth_unstable_by(k, |a, b| a.partial_cmp(b).unwrap());
119 let kth = *kth;
120 let mut next = right[0];
121 for &v in &right[1..] {
122 if v < next {
123 next = v;
124 }
125 }
126 (kth, next)
127}
128
129/// PERCENTILE.INC over unsorted data (reorders `nums`): rank = p*(n-1) on
130/// the ascending order needs at most the two adjacent order statistics
131/// around the rank. The interpolation formula is unchanged from the old
132/// full-sort implementation.
133fn percentile_inc(nums: &mut [f64], p: f64) -> Result<f64, ExcelError> {
134 if nums.is_empty() {
135 return Err(ExcelError::new_num());
136 }
137 if !(0.0..=1.0).contains(&p) {
138 return Err(ExcelError::new_num());
139 }
140 if nums.len() == 1 {
141 return Ok(nums[0]);
142 }
143 let n = nums.len() as f64;
144 let rank = p * (n - 1.0); // 0-based rank
145 let lo = rank.floor() as usize;
146 let hi = rank.ceil() as usize;
147 if lo == hi {
148 return Ok(nth_smallest(nums, lo));
149 }
150 // hi == lo + 1 whenever rank is fractional.
151 let frac = rank - (lo as f64);
152 let (lo_v, hi_v) = adjacent_smallest(nums, lo);
153 Ok(lo_v + (hi_v - lo_v) * frac)
154}
155
156/// PERCENTILE.EXC over unsorted data (reorders `nums`); (n+1) rank basis,
157/// invalid when rank < 1 or > n. Same selection strategy as
158/// [`percentile_inc`]; interpolation formula unchanged.
159fn percentile_exc(nums: &mut [f64], p: f64) -> Result<f64, ExcelError> {
160 if nums.is_empty() {
161 return Err(ExcelError::new_num());
162 }
163 if !(0.0..=1.0).contains(&p) || p <= 0.0 || p >= 1.0 {
164 return Err(ExcelError::new_num());
165 }
166 let n = nums.len() as f64;
167 let rank = p * (n + 1.0); // 1..n domain
168 if rank < 1.0 || rank > n {
169 return Err(ExcelError::new_num());
170 }
171 let lo = rank.floor();
172 let hi = rank.ceil();
173 if (lo - hi).abs() < f64::EPSILON {
174 return Ok(nth_smallest(nums, (lo as usize) - 1));
175 }
176 // hi_idx == lo_idx + 1 whenever rank is fractional.
177 let frac = rank - lo;
178 let lo_idx = (lo as usize) - 1;
179 let (lo_v, hi_v) = adjacent_smallest(nums, lo_idx);
180 Ok(lo_v + (hi_v - lo_v) * frac)
181}
182
183/// Returns the rank position of a number within a data set, with ties sharing the same rank.
184///
185/// `RANK.EQ` defaults to descending order (largest value is rank 1), and can switch to ascending
186/// order when `order` is non-zero.
187///
188/// # Remarks
189/// - Omitting `order`, or setting `order` to `0`, ranks values in descending order.
190/// - Any non-zero `order` ranks values in ascending order.
191/// - Tied values receive the same rank (the first matching position in the sorted list).
192/// - Returns `#N/A` if `number` is not found in `ref`.
193///
194/// # Examples
195///
196/// ```yaml,sandbox
197/// title: "Descending rank with direct values"
198/// formula: "=RANK.EQ(7,{10,7,4,2})"
199/// expected: 2
200/// ```
201///
202/// ```yaml,sandbox
203/// title: "Ascending rank with ties in a range"
204/// grid:
205/// A1: 50
206/// A2: 20
207/// A3: 20
208/// A4: 10
209/// A5: 5
210/// formula: "=RANK.EQ(A2,A1:A5,1)"
211/// expected: 3
212/// ```
213///
214/// ```yaml,docs
215/// related:
216/// - RANK.AVG
217/// - LARGE
218/// - SMALL
219/// faq:
220/// - q: "When does RANK.EQ return #N/A?"
221/// a: "It returns #N/A when the target number does not appear in the reference set."
222/// ```
223#[derive(Debug)]
224pub struct RankEqFn;
225/// [formualizer-docgen:schema:start]
226/// Name: RANK.EQ
227/// Type: RankEqFn
228/// Min args: 2
229/// Max args: variadic
230/// Variadic: true
231/// Signature: RANK.EQ(arg1...: number@range)
232/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
233/// Caps: PURE, NUMERIC_ONLY
234/// [formualizer-docgen:schema:end]
235impl Function for RankEqFn {
236 func_caps!(PURE, NUMERIC_ONLY);
237 fn name(&self) -> &'static str {
238 "RANK.EQ"
239 }
240 fn aliases(&self) -> &'static [&'static str] {
241 &["RANK"]
242 }
243 fn min_args(&self) -> usize {
244 2
245 }
246 fn variadic(&self) -> bool {
247 true
248 } // allow optional order
249 fn arg_schema(&self) -> &'static [ArgSchema] {
250 &ARG_RANGE_NUM_LENIENT_ONE[..]
251 }
252 fn eval<'a, 'b, 'c>(
253 &self,
254 args: &'c [ArgumentHandle<'a, 'b>],
255 _ctx: &dyn FunctionContext<'b>,
256 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
257 if args.len() < 2 {
258 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
259 ExcelError::new_na(),
260 )));
261 }
262 let target = match coerce_num(&args[0].value()?.into_literal()) {
263 Ok(n) => n,
264 Err(_) => {
265 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
266 ExcelError::new_na(),
267 )));
268 }
269 };
270 // optional order arg at end if 3 args
271 let order = if args.len() >= 3 {
272 coerce_num(&args[2].value()?.into_literal()).unwrap_or(0.0)
273 } else {
274 0.0
275 };
276 let nums = collect_numeric_stats(&args[1..2])?; // only one ref range per Excel spec
277 if nums.is_empty() {
278 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
279 ExcelError::new_na(),
280 )));
281 }
282 let mut sorted = nums; // copy
283 if order.abs() < 1e-12 {
284 // descending
285 sorted.sort_by(|a, b| b.partial_cmp(a).unwrap());
286 } else {
287 // ascending
288 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
289 }
290 for (i, &v) in sorted.iter().enumerate() {
291 if (v - target).abs() < 1e-12 {
292 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
293 (i + 1) as f64,
294 )));
295 }
296 }
297 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
298 ExcelError::new_na(),
299 )))
300 }
301}
302
303/// Returns the rank position of a number, averaging the rank positions for ties.
304///
305/// Use `RANK.AVG` when tied values should share the average of their occupied rank positions.
306///
307/// # Remarks
308/// - Omitting `order`, or setting `order` to `0`, ranks values in descending order.
309/// - Any non-zero `order` ranks values in ascending order.
310/// - If `number` appears multiple times, the function returns the mean of those rank positions.
311/// - Returns `#N/A` if `number` is not found in `ref`.
312///
313/// # Examples
314///
315/// ```yaml,sandbox
316/// title: "Average rank for tied values"
317/// formula: "=RANK.AVG(20,{30,20,20,10})"
318/// expected: 2.5
319/// ```
320///
321/// ```yaml,sandbox
322/// title: "Ascending average rank from a range"
323/// grid:
324/// A1: 50
325/// A2: 20
326/// A3: 20
327/// A4: 10
328/// A5: 5
329/// formula: "=RANK.AVG(A2,A1:A5,1)"
330/// expected: 3.5
331/// ```
332///
333/// ```yaml,docs
334/// related:
335/// - RANK.EQ
336/// - LARGE
337/// - SMALL
338/// faq:
339/// - q: "How are ties handled by RANK.AVG?"
340/// a: "All tied occurrences share the average of their rank positions."
341/// ```
342#[derive(Debug)]
343pub struct RankAvgFn;
344/// [formualizer-docgen:schema:start]
345/// Name: RANK.AVG
346/// Type: RankAvgFn
347/// Min args: 2
348/// Max args: variadic
349/// Variadic: true
350/// Signature: RANK.AVG(arg1...: number@range)
351/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
352/// Caps: PURE, NUMERIC_ONLY
353/// [formualizer-docgen:schema:end]
354impl Function for RankAvgFn {
355 func_caps!(PURE, NUMERIC_ONLY);
356 fn name(&self) -> &'static str {
357 "RANK.AVG"
358 }
359 fn min_args(&self) -> usize {
360 2
361 }
362 fn variadic(&self) -> bool {
363 true
364 }
365 fn arg_schema(&self) -> &'static [ArgSchema] {
366 &ARG_RANGE_NUM_LENIENT_ONE[..]
367 }
368 fn eval<'a, 'b, 'c>(
369 &self,
370 args: &'c [ArgumentHandle<'a, 'b>],
371 _ctx: &dyn FunctionContext<'b>,
372 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
373 if args.len() < 2 {
374 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
375 ExcelError::new_na(),
376 )));
377 }
378 let t0 = scalar_like_value(&args[0])?;
379 let target = match coerce_num(&t0) {
380 Ok(n) => n,
381 Err(_) => {
382 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
383 ExcelError::new_na(),
384 )));
385 }
386 };
387 let order = if args.len() >= 3 {
388 let ord = scalar_like_value(&args[2])?;
389 coerce_num(&ord).unwrap_or(0.0)
390 } else {
391 0.0
392 };
393 let nums = collect_numeric_stats(&args[1..2])?;
394 if nums.is_empty() {
395 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
396 ExcelError::new_na(),
397 )));
398 }
399 let mut sorted = nums;
400 if order.abs() < 1e-12 {
401 sorted.sort_by(|a, b| b.partial_cmp(a).unwrap());
402 } else {
403 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
404 }
405 let mut positions = Vec::new();
406 for (i, &v) in sorted.iter().enumerate() {
407 if (v - target).abs() < 1e-12 {
408 positions.push(i + 1);
409 }
410 }
411 if positions.is_empty() {
412 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
413 ExcelError::new_na(),
414 )));
415 }
416 let avg = positions.iter().copied().sum::<usize>() as f64 / positions.len() as f64;
417 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(avg)))
418 }
419}
420
421/// Returns the k-th largest value in a data set.
422///
423/// `LARGE` is useful for top-N analysis, such as highest score, second-highest sale, or third-best
424/// result.
425///
426/// # Remarks
427/// - `k` must be at least `1`.
428/// - Returns `#NUM!` if `k` is greater than the count of numeric values.
429/// - Non-numeric values in referenced ranges are ignored.
430///
431/// # Examples
432///
433/// ```yaml,sandbox
434/// title: "Second-largest from direct values"
435/// formula: "=LARGE({4,9,1,7},2)"
436/// expected: 7
437/// ```
438///
439/// ```yaml,sandbox
440/// title: "Third-largest from a range"
441/// grid:
442/// A1: 3
443/// A2: 12
444/// A3: 8
445/// A4: 5
446/// formula: "=LARGE(A1:A4,3)"
447/// expected: 5
448/// ```
449///
450/// ```yaml,docs
451/// related:
452/// - SMALL
453/// - MAX
454/// - RANK.EQ
455/// faq:
456/// - q: "When does LARGE return #NUM!?"
457/// a: "It returns #NUM! when k < 1, k exceeds numeric count, or no numeric values exist."
458/// ```
459#[derive(Debug)]
460pub struct LARGE;
461/// [formualizer-docgen:schema:start]
462/// Name: LARGE
463/// Type: LARGE
464/// Min args: 2
465/// Max args: variadic
466/// Variadic: true
467/// Signature: LARGE(arg1...: number@range)
468/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
469/// Caps: PURE, REDUCTION, NUMERIC_ONLY
470/// [formualizer-docgen:schema:end]
471impl Function for LARGE {
472 func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
473 fn name(&self) -> &'static str {
474 "LARGE"
475 }
476 fn min_args(&self) -> usize {
477 2
478 }
479 fn variadic(&self) -> bool {
480 true
481 }
482 fn arg_schema(&self) -> &'static [ArgSchema] {
483 &ARG_RANGE_NUM_LENIENT_ONE[..]
484 }
485 fn eval<'a, 'b, 'c>(
486 &self,
487 args: &'c [ArgumentHandle<'a, 'b>],
488 _ctx: &dyn FunctionContext<'b>,
489 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
490 if args.len() < 2 {
491 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
492 ExcelError::new_num(),
493 )));
494 }
495 let k = match coerce_num(&args.last().unwrap().value()?.into_literal()) {
496 Ok(n) => n,
497 Err(_) => {
498 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
499 ExcelError::new_num(),
500 )));
501 }
502 };
503 let k = k as i64;
504 if k < 1 {
505 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
506 ExcelError::new_num(),
507 )));
508 }
509 let mut nums = collect_numeric_stats(&args[..args.len() - 1])?;
510 if nums.is_empty() || k as usize > nums.len() {
511 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
512 ExcelError::new_num(),
513 )));
514 }
515 // k-th largest == (n-k)-th smallest: quickselect instead of full sort.
516 let idx = nums.len() - k as usize;
517 let v = nth_smallest(&mut nums, idx);
518 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(v)))
519 }
520}
521
522/// Returns the k-th smallest value in a data set.
523///
524/// `SMALL` is often used to find low outliers, minimum thresholds, or bottom-N values.
525///
526/// # Remarks
527/// - `k` must be at least `1`.
528/// - Returns `#NUM!` if `k` is greater than the count of numeric values.
529/// - Non-numeric values in referenced ranges are ignored.
530///
531/// # Examples
532///
533/// ```yaml,sandbox
534/// title: "Second-smallest from direct values"
535/// formula: "=SMALL({4,9,1,7},2)"
536/// expected: 4
537/// ```
538///
539/// ```yaml,sandbox
540/// title: "Third-smallest from a range"
541/// grid:
542/// A1: 3
543/// A2: 12
544/// A3: 8
545/// A4: 5
546/// formula: "=SMALL(A1:A4,3)"
547/// expected: 8
548/// ```
549///
550/// ```yaml,docs
551/// related:
552/// - LARGE
553/// - MIN
554/// - RANK.EQ
555/// faq:
556/// - q: "Does SMALL include text in referenced ranges?"
557/// a: "No. Non-numeric range values are ignored when selecting the k-th smallest value."
558/// ```
559#[derive(Debug)]
560pub struct SMALL;
561/// [formualizer-docgen:schema:start]
562/// Name: SMALL
563/// Type: SMALL
564/// Min args: 2
565/// Max args: variadic
566/// Variadic: true
567/// Signature: SMALL(arg1...: number@range)
568/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
569/// Caps: PURE, REDUCTION, NUMERIC_ONLY
570/// [formualizer-docgen:schema:end]
571impl Function for SMALL {
572 func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
573 fn name(&self) -> &'static str {
574 "SMALL"
575 }
576 fn min_args(&self) -> usize {
577 2
578 }
579 fn variadic(&self) -> bool {
580 true
581 }
582 fn arg_schema(&self) -> &'static [ArgSchema] {
583 &ARG_RANGE_NUM_LENIENT_ONE[..]
584 }
585 fn eval<'a, 'b, 'c>(
586 &self,
587 args: &'c [ArgumentHandle<'a, 'b>],
588 _ctx: &dyn FunctionContext<'b>,
589 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
590 if args.len() < 2 {
591 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
592 ExcelError::new_num(),
593 )));
594 }
595 let k = match coerce_num(&args.last().unwrap().value()?.into_literal()) {
596 Ok(n) => n,
597 Err(_) => {
598 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
599 ExcelError::new_num(),
600 )));
601 }
602 };
603 let k = k as i64;
604 if k < 1 {
605 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
606 ExcelError::new_num(),
607 )));
608 }
609 let mut nums = collect_numeric_stats(&args[..args.len() - 1])?;
610 if nums.is_empty() || k as usize > nums.len() {
611 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
612 ExcelError::new_num(),
613 )));
614 }
615 // k-th smallest: quickselect instead of full sort.
616 let v = nth_smallest(&mut nums, k as usize - 1);
617 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(v)))
618 }
619}
620
621/// Returns the middle value of a numeric data set.
622///
623/// For an even number of values, `MEDIAN` returns the average of the two center values.
624///
625/// # Remarks
626/// - Ignores non-numeric values in referenced ranges.
627/// - Returns `#NUM!` when no numeric values are available.
628/// - Supports both scalar arguments and range inputs.
629///
630/// # Examples
631///
632/// ```yaml,sandbox
633/// title: "Median of an odd-sized set"
634/// formula: "=MEDIAN(1,3,8)"
635/// expected: 3
636/// ```
637///
638/// ```yaml,sandbox
639/// title: "Median of an even-sized range"
640/// grid:
641/// A1: 1
642/// A2: 2
643/// A3: 10
644/// A4: 12
645/// formula: "=MEDIAN(A1:A4)"
646/// expected: 6
647/// ```
648///
649/// ```yaml,docs
650/// related:
651/// - AVERAGE
652/// - MODE.SNGL
653/// - QUARTILE.INC
654/// faq:
655/// - q: "When does MEDIAN return #NUM!?"
656/// a: "MEDIAN returns #NUM! when no numeric values are available after filtering/coercion."
657/// ```
658#[derive(Debug)]
659pub struct MEDIAN;
660/// [formualizer-docgen:schema:start]
661/// Name: MEDIAN
662/// Type: MEDIAN
663/// Min args: 1
664/// Max args: variadic
665/// Variadic: true
666/// Signature: MEDIAN(arg1...: number@range)
667/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
668/// Caps: PURE, REDUCTION, NUMERIC_ONLY
669/// [formualizer-docgen:schema:end]
670impl Function for MEDIAN {
671 func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
672 fn name(&self) -> &'static str {
673 "MEDIAN"
674 }
675 fn min_args(&self) -> usize {
676 1
677 }
678 fn variadic(&self) -> bool {
679 true
680 }
681 fn arg_schema(&self) -> &'static [ArgSchema] {
682 &ARG_RANGE_NUM_LENIENT_ONE[..]
683 }
684 fn eval<'a, 'b, 'c>(
685 &self,
686 args: &'c [ArgumentHandle<'a, 'b>],
687 _ctx: &dyn FunctionContext<'b>,
688 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
689 let mut nums = collect_numeric_stats(args)?;
690 if nums.is_empty() {
691 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
692 ExcelError::new_num(),
693 )));
694 }
695 // Middle one/two order statistics: quickselect instead of full sort.
696 let n = nums.len();
697 let mid = n / 2;
698 let med = if n % 2 == 1 {
699 nth_smallest(&mut nums, mid)
700 } else {
701 let (lo, hi) = adjacent_smallest(&mut nums, mid - 1);
702 (lo + hi) / 2.0
703 };
704 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(med)))
705 }
706}
707
708/// Estimates sample standard deviation using `n-1` in the denominator.
709///
710/// `STDEV.S` measures spread when your values represent a sample of a larger population.
711///
712/// # Remarks
713/// - Requires at least two numeric values.
714/// - Returns `#DIV/0!` when fewer than two numeric values are provided.
715/// - Non-numeric values in referenced ranges are ignored.
716///
717/// # Examples
718///
719/// ```yaml,sandbox
720/// title: "Sample standard deviation from scalar arguments"
721/// formula: "=STDEV.S(2,4,6)"
722/// expected: 2
723/// ```
724///
725/// ```yaml,sandbox
726/// title: "Sample standard deviation from a range"
727/// grid:
728/// A1: 5
729/// A2: 7
730/// A3: 9
731/// formula: "=STDEV.S(A1:A3)"
732/// expected: 2
733/// ```
734///
735/// ```yaml,docs
736/// related:
737/// - STDEV.P
738/// - VAR.S
739/// - VAR.P
740/// faq:
741/// - q: "Why does STDEV.S return #DIV/0!?"
742/// a: "Sample standard deviation needs at least two numeric values."
743/// ```
744#[derive(Debug)]
745pub struct StdevSample; // sample
746/// [formualizer-docgen:schema:start]
747/// Name: STDEV.S
748/// Type: StdevSample
749/// Min args: 1
750/// Max args: variadic
751/// Variadic: true
752/// Signature: STDEV.S(arg1...: number@range)
753/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
754/// Caps: PURE, REDUCTION, NUMERIC_ONLY, STREAM_OK
755/// [formualizer-docgen:schema:end]
756impl Function for StdevSample {
757 func_caps!(PURE, NUMERIC_ONLY, REDUCTION, STREAM_OK);
758 fn name(&self) -> &'static str {
759 "STDEV.S"
760 }
761 fn aliases(&self) -> &'static [&'static str] {
762 &["STDEV"]
763 }
764 fn min_args(&self) -> usize {
765 1
766 }
767 fn variadic(&self) -> bool {
768 true
769 }
770 fn arg_schema(&self) -> &'static [ArgSchema] {
771 &ARG_RANGE_NUM_LENIENT_ONE[..]
772 }
773 fn eval<'a, 'b, 'c>(
774 &self,
775 args: &'c [ArgumentHandle<'a, 'b>],
776 _ctx: &dyn FunctionContext<'b>,
777 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
778 let nums = collect_numeric_stats(args)?;
779 let n = nums.len();
780 if n < 2 {
781 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
782 ExcelError::from_error_string("#DIV/0!"),
783 )));
784 }
785 let mean = nums.iter().sum::<f64>() / (n as f64);
786 let mut ss = 0.0;
787 for &v in &nums {
788 let d = v - mean;
789 ss += d * d;
790 }
791 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
792 (ss / ((n - 1) as f64)).sqrt(),
793 )))
794 }
795}
796
797/// Returns population standard deviation using `n` in the denominator.
798///
799/// Use `STDEV.P` when your values represent the entire population, not a sample.
800///
801/// # Remarks
802/// - Requires at least one numeric value.
803/// - Returns `#DIV/0!` when no numeric values are provided.
804/// - Non-numeric values in referenced ranges are ignored.
805///
806/// # Examples
807///
808/// ```yaml,sandbox
809/// title: "Population standard deviation from scalar arguments"
810/// formula: "=STDEV.P(2,4,6)"
811/// expected: 1.632993161855452
812/// ```
813///
814/// ```yaml,sandbox
815/// title: "Population standard deviation from a range"
816/// grid:
817/// A1: 1
818/// A2: 2
819/// A3: 3
820/// formula: "=STDEV.P(A1:A3)"
821/// expected: 0.816496580927726
822/// ```
823///
824/// ```yaml,docs
825/// related:
826/// - STDEV.S
827/// - VAR.P
828/// - VAR.S
829/// faq:
830/// - q: "When does STDEV.P return #DIV/0!?"
831/// a: "It returns #DIV/0! when no numeric values are provided."
832/// ```
833#[derive(Debug)]
834pub struct StdevPop; // population
835/// [formualizer-docgen:schema:start]
836/// Name: STDEV.P
837/// Type: StdevPop
838/// Min args: 1
839/// Max args: variadic
840/// Variadic: true
841/// Signature: STDEV.P(arg1...: number@range)
842/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
843/// Caps: PURE, REDUCTION, NUMERIC_ONLY, STREAM_OK
844/// [formualizer-docgen:schema:end]
845impl Function for StdevPop {
846 func_caps!(PURE, NUMERIC_ONLY, REDUCTION, STREAM_OK);
847 fn name(&self) -> &'static str {
848 "STDEV.P"
849 }
850 fn aliases(&self) -> &'static [&'static str] {
851 &["STDEVP"]
852 }
853 fn min_args(&self) -> usize {
854 1
855 }
856 fn variadic(&self) -> bool {
857 true
858 }
859 fn arg_schema(&self) -> &'static [ArgSchema] {
860 &ARG_RANGE_NUM_LENIENT_ONE[..]
861 }
862 fn eval<'a, 'b, 'c>(
863 &self,
864 args: &'c [ArgumentHandle<'a, 'b>],
865 _ctx: &dyn FunctionContext<'b>,
866 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
867 let nums = collect_numeric_stats(args)?;
868 let n = nums.len();
869 if n == 0 {
870 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
871 ExcelError::from_error_string("#DIV/0!"),
872 )));
873 }
874 let mean = nums.iter().sum::<f64>() / (n as f64);
875 let mut ss = 0.0;
876 for &v in &nums {
877 let d = v - mean;
878 ss += d * d;
879 }
880 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
881 (ss / (n as f64)).sqrt(),
882 )))
883 }
884}
885
886/// Estimates sample variance using `n-1` in the denominator.
887///
888/// `VAR.S` is the squared counterpart of `STDEV.S` for sample-based variability.
889///
890/// # Remarks
891/// - Requires at least two numeric values.
892/// - Returns `#DIV/0!` when fewer than two numeric values are provided.
893/// - Non-numeric values in referenced ranges are ignored.
894///
895/// # Examples
896///
897/// ```yaml,sandbox
898/// title: "Sample variance from scalar arguments"
899/// formula: "=VAR.S(2,4,6)"
900/// expected: 4
901/// ```
902///
903/// ```yaml,sandbox
904/// title: "Sample variance from a range"
905/// grid:
906/// A1: 1
907/// A2: 2
908/// A3: 3
909/// formula: "=VAR.S(A1:A3)"
910/// expected: 1
911/// ```
912///
913/// ```yaml,docs
914/// related:
915/// - VAR.P
916/// - STDEV.S
917/// - STDEV.P
918/// faq:
919/// - q: "Why does VAR.S return #DIV/0!?"
920/// a: "Sample variance requires at least two numeric observations."
921/// ```
922#[derive(Debug)]
923pub struct VarSample; // sample variance
924/// [formualizer-docgen:schema:start]
925/// Name: VAR.S
926/// Type: VarSample
927/// Min args: 1
928/// Max args: variadic
929/// Variadic: true
930/// Signature: VAR.S(arg1...: number@range)
931/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
932/// Caps: PURE, REDUCTION, NUMERIC_ONLY, STREAM_OK
933/// [formualizer-docgen:schema:end]
934impl Function for VarSample {
935 func_caps!(PURE, NUMERIC_ONLY, REDUCTION, STREAM_OK);
936 fn name(&self) -> &'static str {
937 "VAR.S"
938 }
939 fn aliases(&self) -> &'static [&'static str] {
940 &["VAR"]
941 }
942 fn min_args(&self) -> usize {
943 1
944 }
945 fn variadic(&self) -> bool {
946 true
947 }
948 fn arg_schema(&self) -> &'static [ArgSchema] {
949 &ARG_RANGE_NUM_LENIENT_ONE[..]
950 }
951 fn eval<'a, 'b, 'c>(
952 &self,
953 args: &'c [ArgumentHandle<'a, 'b>],
954 _ctx: &dyn FunctionContext<'b>,
955 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
956 let nums = collect_numeric_stats(args)?;
957 let n = nums.len();
958 if n < 2 {
959 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
960 ExcelError::from_error_string("#DIV/0!"),
961 )));
962 }
963 let mean = nums.iter().sum::<f64>() / (n as f64);
964 let mut ss = 0.0;
965 for &v in &nums {
966 let d = v - mean;
967 ss += d * d;
968 }
969 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
970 ss / ((n - 1) as f64),
971 )))
972 }
973}
974
975/// Returns population variance using `n` in the denominator.
976///
977/// `VAR.P` describes dispersion for a complete population of numeric values.
978///
979/// # Remarks
980/// - Requires at least one numeric value.
981/// - Returns `#DIV/0!` when no numeric values are provided.
982/// - Non-numeric values in referenced ranges are ignored.
983///
984/// # Examples
985///
986/// ```yaml,sandbox
987/// title: "Population variance from scalar arguments"
988/// formula: "=VAR.P(2,4,6)"
989/// expected: 2.6666666666666665
990/// ```
991///
992/// ```yaml,sandbox
993/// title: "Population variance from a range"
994/// grid:
995/// A1: 1
996/// A2: 2
997/// A3: 3
998/// formula: "=VAR.P(A1:A3)"
999/// expected: 0.6666666666666666
1000/// ```
1001///
1002/// ```yaml,docs
1003/// related:
1004/// - VAR.S
1005/// - STDEV.P
1006/// - STDEV.S
1007/// faq:
1008/// - q: "What is the denominator difference vs VAR.S?"
1009/// a: "VAR.P divides by n, while VAR.S divides by n-1."
1010/// ```
1011#[derive(Debug)]
1012pub struct VarPop; // population variance
1013/// [formualizer-docgen:schema:start]
1014/// Name: VAR.P
1015/// Type: VarPop
1016/// Min args: 1
1017/// Max args: variadic
1018/// Variadic: true
1019/// Signature: VAR.P(arg1...: number@range)
1020/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1021/// Caps: PURE, REDUCTION, NUMERIC_ONLY, STREAM_OK
1022/// [formualizer-docgen:schema:end]
1023impl Function for VarPop {
1024 func_caps!(PURE, NUMERIC_ONLY, REDUCTION, STREAM_OK);
1025 fn name(&self) -> &'static str {
1026 "VAR.P"
1027 }
1028 fn aliases(&self) -> &'static [&'static str] {
1029 &["VARP"]
1030 }
1031 fn min_args(&self) -> usize {
1032 1
1033 }
1034 fn variadic(&self) -> bool {
1035 true
1036 }
1037 fn arg_schema(&self) -> &'static [ArgSchema] {
1038 &ARG_RANGE_NUM_LENIENT_ONE[..]
1039 }
1040 fn eval<'a, 'b, 'c>(
1041 &self,
1042 args: &'c [ArgumentHandle<'a, 'b>],
1043 _ctx: &dyn FunctionContext<'b>,
1044 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1045 let nums = collect_numeric_stats(args)?;
1046 let n = nums.len();
1047 if n == 0 {
1048 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1049 ExcelError::from_error_string("#DIV/0!"),
1050 )));
1051 }
1052 let mean = nums.iter().sum::<f64>() / (n as f64);
1053 let mut ss = 0.0;
1054 for &v in &nums {
1055 let d = v - mean;
1056 ss += d * d;
1057 }
1058 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
1059 ss / (n as f64),
1060 )))
1061 }
1062}
1063
1064// MODE.SNGL (alias MODE) and MODE.MULT
1065/// Returns the most frequently occurring value in a data set.
1066///
1067/// `MODE.SNGL` returns a single mode value and reports `#N/A` if no value repeats.
1068///
1069/// # Remarks
1070/// - Returns the first mode encountered after sorting when frequencies tie.
1071/// - Returns `#N/A` when every numeric value appears only once.
1072/// - Alias `MODE` is supported.
1073///
1074/// # Examples
1075///
1076/// ```yaml,sandbox
1077/// title: "Single mode from scalar arguments"
1078/// formula: "=MODE.SNGL(1,2,2,3)"
1079/// expected: 2
1080/// ```
1081///
1082/// ```yaml,sandbox
1083/// title: "Single mode from a range"
1084/// grid:
1085/// A1: 4
1086/// A2: 4
1087/// A3: 6
1088/// A4: 6
1089/// A5: 6
1090/// formula: "=MODE.SNGL(A1:A5)"
1091/// expected: 6
1092/// ```
1093///
1094/// ```yaml,docs
1095/// related:
1096/// - MODE.MULT
1097/// - MEDIAN
1098/// - AVERAGE
1099/// faq:
1100/// - q: "When does MODE.SNGL return #N/A?"
1101/// a: "It returns #N/A when no value repeats in the numeric dataset."
1102/// ```
1103#[derive(Debug)]
1104pub struct ModeSingleFn;
1105/// [formualizer-docgen:schema:start]
1106/// Name: MODE.SNGL
1107/// Type: ModeSingleFn
1108/// Min args: 1
1109/// Max args: variadic
1110/// Variadic: true
1111/// Signature: MODE.SNGL(arg1...: number@range)
1112/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1113/// Caps: PURE, REDUCTION, NUMERIC_ONLY
1114/// [formualizer-docgen:schema:end]
1115impl Function for ModeSingleFn {
1116 func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
1117 fn name(&self) -> &'static str {
1118 "MODE.SNGL"
1119 }
1120 fn aliases(&self) -> &'static [&'static str] {
1121 &["MODE"]
1122 }
1123 fn min_args(&self) -> usize {
1124 1
1125 }
1126 fn variadic(&self) -> bool {
1127 true
1128 }
1129 fn arg_schema(&self) -> &'static [ArgSchema] {
1130 &ARG_RANGE_NUM_LENIENT_ONE[..]
1131 }
1132 fn eval<'a, 'b, 'c>(
1133 &self,
1134 args: &'c [ArgumentHandle<'a, 'b>],
1135 _ctx: &dyn FunctionContext<'b>,
1136 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1137 let mut nums = collect_numeric_stats(args)?;
1138 if nums.is_empty() {
1139 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1140 ExcelError::new_na(),
1141 )));
1142 }
1143 nums.sort_by(|a, b| a.partial_cmp(b).unwrap());
1144 let mut best_val = nums[0];
1145 let mut best_cnt = 1usize;
1146 let mut cur_val = nums[0];
1147 let mut cur_cnt = 1usize;
1148 for &v in &nums[1..] {
1149 if (v - cur_val).abs() < 1e-12 {
1150 cur_cnt += 1;
1151 } else {
1152 if cur_cnt > best_cnt {
1153 best_cnt = cur_cnt;
1154 best_val = cur_val;
1155 }
1156 cur_val = v;
1157 cur_cnt = 1;
1158 }
1159 }
1160 if cur_cnt > best_cnt {
1161 best_cnt = cur_cnt;
1162 best_val = cur_val;
1163 }
1164 if best_cnt <= 1 {
1165 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1166 ExcelError::new_na(),
1167 )))
1168 } else {
1169 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
1170 best_val,
1171 )))
1172 }
1173 }
1174}
1175
1176/// Returns all modal values as a vertical array.
1177///
1178/// Use `MODE.MULT` when a data set can have multiple values with the same highest frequency.
1179///
1180/// # Remarks
1181/// - Returns each tied mode as a separate row in the result array.
1182/// - Returns `#N/A` when every numeric value appears only once.
1183/// - Non-numeric values in referenced ranges are ignored.
1184///
1185/// # Examples
1186///
1187/// ```yaml,sandbox
1188/// title: "Multiple modes from direct values"
1189/// formula: "=MODE.MULT({1,2,2,3,3,4})"
1190/// expected:
1191/// - [2]
1192/// - [3]
1193/// ```
1194///
1195/// ```yaml,sandbox
1196/// title: "Single repeated mode still returns an array"
1197/// grid:
1198/// A1: 5
1199/// A2: 5
1200/// A3: 2
1201/// A4: 1
1202/// formula: "=MODE.MULT(A1:A4)"
1203/// expected:
1204/// - [5]
1205/// ```
1206///
1207/// ```yaml,docs
1208/// related:
1209/// - MODE.SNGL
1210/// - FREQUENCY
1211/// - MEDIAN
1212/// faq:
1213/// - q: "Why can MODE.MULT return an array result?"
1214/// a: "It emits every value tied for highest frequency as separate rows."
1215/// ```
1216#[derive(Debug)]
1217pub struct ModeMultiFn;
1218/// [formualizer-docgen:schema:start]
1219/// Name: MODE.MULT
1220/// Type: ModeMultiFn
1221/// Min args: 1
1222/// Max args: variadic
1223/// Variadic: true
1224/// Signature: MODE.MULT(arg1...: number@range)
1225/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1226/// Caps: PURE, REDUCTION, NUMERIC_ONLY
1227/// [formualizer-docgen:schema:end]
1228impl Function for ModeMultiFn {
1229 func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
1230 fn name(&self) -> &'static str {
1231 "MODE.MULT"
1232 }
1233 fn min_args(&self) -> usize {
1234 1
1235 }
1236 fn variadic(&self) -> bool {
1237 true
1238 }
1239 fn arg_schema(&self) -> &'static [ArgSchema] {
1240 &ARG_RANGE_NUM_LENIENT_ONE[..]
1241 }
1242 fn eval<'a, 'b, 'c>(
1243 &self,
1244 args: &'c [ArgumentHandle<'a, 'b>],
1245 _ctx: &dyn FunctionContext<'b>,
1246 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1247 let mut nums = collect_numeric_stats(args)?;
1248 if nums.is_empty() {
1249 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1250 ExcelError::new_na(),
1251 )));
1252 }
1253 nums.sort_by(|a, b| a.partial_cmp(b).unwrap());
1254 let mut runs: Vec<(f64, usize)> = Vec::new();
1255 let mut cur_val = nums[0];
1256 let mut cur_cnt = 1usize;
1257 for &v in &nums[1..] {
1258 if (v - cur_val).abs() < 1e-12 {
1259 cur_cnt += 1;
1260 } else {
1261 runs.push((cur_val, cur_cnt));
1262 cur_val = v;
1263 cur_cnt = 1;
1264 }
1265 }
1266 runs.push((cur_val, cur_cnt));
1267 let max_freq = runs.iter().map(|r| r.1).max().unwrap_or(0);
1268 if max_freq <= 1 {
1269 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1270 ExcelError::new_na(),
1271 )));
1272 }
1273 let rows: Vec<Vec<LiteralValue>> = runs
1274 .into_iter()
1275 .filter(|&(_, c)| c == max_freq)
1276 .map(|(v, _)| vec![LiteralValue::Number(v)])
1277 .collect();
1278 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Array(rows)))
1279 }
1280}
1281
1282/// Returns the k-th percentile of a data set using inclusive interpolation.
1283///
1284/// `PERCENTILE.INC` accepts percentile values from `0` through `1` and interpolates between
1285/// sorted values as needed.
1286///
1287/// # Remarks
1288/// - `k` must be in the inclusive range `[0, 1]`.
1289/// - Returns `#NUM!` for empty numeric input or invalid percentile arguments.
1290/// - Alias `PERCENTILE` is supported.
1291///
1292/// # Examples
1293///
1294/// ```yaml,sandbox
1295/// title: "Inclusive 25th percentile from direct values"
1296/// formula: "=PERCENTILE.INC({1,2,3,4,5},0.25)"
1297/// expected: 2
1298/// ```
1299///
1300/// ```yaml,sandbox
1301/// title: "Inclusive median-style interpolation from a range"
1302/// grid:
1303/// A1: 10
1304/// A2: 20
1305/// A3: 30
1306/// A4: 40
1307/// formula: "=PERCENTILE.INC(A1:A4,0.5)"
1308/// expected: 25
1309/// ```
1310///
1311/// ```yaml,docs
1312/// related:
1313/// - PERCENTILE.EXC
1314/// - QUARTILE.INC
1315/// - PERCENTRANK.INC
1316/// faq:
1317/// - q: "What k range is valid for PERCENTILE.INC?"
1318/// a: "k must be between 0 and 1 inclusive; outside that range returns #NUM!."
1319/// ```
1320#[derive(Debug)]
1321pub struct PercentileInc; // inclusive
1322/// [formualizer-docgen:schema:start]
1323/// Name: PERCENTILE.INC
1324/// Type: PercentileInc
1325/// Min args: 2
1326/// Max args: variadic
1327/// Variadic: true
1328/// Signature: PERCENTILE.INC(arg1...: number@range)
1329/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1330/// Caps: PURE, NUMERIC_ONLY
1331/// [formualizer-docgen:schema:end]
1332impl Function for PercentileInc {
1333 func_caps!(PURE, NUMERIC_ONLY);
1334 fn name(&self) -> &'static str {
1335 "PERCENTILE.INC"
1336 }
1337 fn aliases(&self) -> &'static [&'static str] {
1338 &["PERCENTILE"]
1339 }
1340 fn min_args(&self) -> usize {
1341 2
1342 }
1343 fn variadic(&self) -> bool {
1344 true
1345 }
1346 fn arg_schema(&self) -> &'static [ArgSchema] {
1347 &ARG_RANGE_NUM_LENIENT_ONE[..]
1348 }
1349 fn eval<'a, 'b, 'c>(
1350 &self,
1351 args: &'c [ArgumentHandle<'a, 'b>],
1352 _ctx: &dyn FunctionContext<'b>,
1353 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1354 if args.len() < 2 {
1355 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1356 ExcelError::new_num(),
1357 )));
1358 }
1359 let pv = scalar_like_value(args.last().unwrap())?;
1360 let p = match coerce_num(&pv) {
1361 Ok(n) => n,
1362 Err(_) => {
1363 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1364 ExcelError::new_num(),
1365 )));
1366 }
1367 };
1368 let mut nums = collect_numeric_stats(&args[..args.len() - 1])?;
1369 if nums.is_empty() {
1370 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1371 ExcelError::new_num(),
1372 )));
1373 }
1374 match percentile_inc(&mut nums, p) {
1375 Ok(v) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(v))),
1376 Err(e) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
1377 }
1378 }
1379}
1380
1381/// Returns the k-th percentile of a data set using exclusive interpolation.
1382///
1383/// `PERCENTILE.EXC` uses the `n+1` rank basis and excludes the exact endpoints `0` and `1`.
1384///
1385/// # Remarks
1386/// - `k` must satisfy `0 < k < 1`.
1387/// - Returns `#NUM!` when the percentile falls outside the valid rank range for the data size.
1388/// - Returns `#NUM!` for empty numeric input.
1389///
1390/// # Examples
1391///
1392/// ```yaml,sandbox
1393/// title: "Exclusive 25th percentile from direct values"
1394/// formula: "=PERCENTILE.EXC({1,2,3,4,5},0.25)"
1395/// expected: 1.5
1396/// ```
1397///
1398/// ```yaml,sandbox
1399/// title: "Exclusive percentile from a range"
1400/// grid:
1401/// A1: 10
1402/// A2: 20
1403/// A3: 30
1404/// A4: 40
1405/// A5: 50
1406/// formula: "=PERCENTILE.EXC(A1:A5,0.6)"
1407/// expected: 36
1408/// ```
1409///
1410/// ```yaml,docs
1411/// related:
1412/// - PERCENTILE.INC
1413/// - QUARTILE.EXC
1414/// - PERCENTRANK.EXC
1415/// faq:
1416/// - q: "Why does PERCENTILE.EXC reject k=0 or k=1?"
1417/// a: "Exclusive percentile uses the n+1 basis and requires strictly 0 < k < 1."
1418/// ```
1419#[derive(Debug)]
1420pub struct PercentileExc; // exclusive
1421/// [formualizer-docgen:schema:start]
1422/// Name: PERCENTILE.EXC
1423/// Type: PercentileExc
1424/// Min args: 2
1425/// Max args: variadic
1426/// Variadic: true
1427/// Signature: PERCENTILE.EXC(arg1...: number@range)
1428/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1429/// Caps: PURE, NUMERIC_ONLY
1430/// [formualizer-docgen:schema:end]
1431impl Function for PercentileExc {
1432 func_caps!(PURE, NUMERIC_ONLY);
1433 fn name(&self) -> &'static str {
1434 "PERCENTILE.EXC"
1435 }
1436 fn min_args(&self) -> usize {
1437 2
1438 }
1439 fn variadic(&self) -> bool {
1440 true
1441 }
1442 fn arg_schema(&self) -> &'static [ArgSchema] {
1443 &ARG_RANGE_NUM_LENIENT_ONE[..]
1444 }
1445 fn eval<'a, 'b, 'c>(
1446 &self,
1447 args: &'c [ArgumentHandle<'a, 'b>],
1448 _ctx: &dyn FunctionContext<'b>,
1449 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1450 if args.len() < 2 {
1451 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1452 ExcelError::new_num(),
1453 )));
1454 }
1455 let pv = scalar_like_value(args.last().unwrap())?;
1456 let p = match coerce_num(&pv) {
1457 Ok(n) => n,
1458 Err(_) => {
1459 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1460 ExcelError::new_num(),
1461 )));
1462 }
1463 };
1464 let mut nums = collect_numeric_stats(&args[..args.len() - 1])?;
1465 if nums.is_empty() {
1466 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1467 ExcelError::new_num(),
1468 )));
1469 }
1470 match percentile_exc(&mut nums, p) {
1471 Ok(v) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(v))),
1472 Err(e) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
1473 }
1474 }
1475}
1476
1477/// Returns an inclusive quartile value for a data set.
1478///
1479/// `QUARTILE.INC` maps quartile index `0..4` onto minimum, quartiles, median, and maximum.
1480///
1481/// # Remarks
1482/// - Valid quartile index values are `0`, `1`, `2`, `3`, and `4`.
1483/// - Uses inclusive percentile logic for quartiles `1` through `3`.
1484/// - Returns `#NUM!` for invalid quartile index values or empty numeric input.
1485/// - Alias `QUARTILE` is supported.
1486///
1487/// # Examples
1488///
1489/// ```yaml,sandbox
1490/// title: "First quartile from direct values"
1491/// formula: "=QUARTILE.INC({1,2,3,4,5},1)"
1492/// expected: 2
1493/// ```
1494///
1495/// ```yaml,sandbox
1496/// title: "Third quartile from a range"
1497/// grid:
1498/// A1: 10
1499/// A2: 20
1500/// A3: 30
1501/// A4: 40
1502/// formula: "=QUARTILE.INC(A1:A4,3)"
1503/// expected: 32.5
1504/// ```
1505///
1506/// ```yaml,docs
1507/// related:
1508/// - QUARTILE.EXC
1509/// - PERCENTILE.INC
1510/// - MEDIAN
1511/// faq:
1512/// - q: "Which quartile numbers are valid for QUARTILE.INC?"
1513/// a: "Only 0 through 4 are valid; other quartile indices return #NUM!."
1514/// ```
1515#[derive(Debug)]
1516pub struct QuartileInc; // quartile inclusive
1517/// [formualizer-docgen:schema:start]
1518/// Name: QUARTILE.INC
1519/// Type: QuartileInc
1520/// Min args: 2
1521/// Max args: variadic
1522/// Variadic: true
1523/// Signature: QUARTILE.INC(arg1...: number@range)
1524/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1525/// Caps: PURE, NUMERIC_ONLY
1526/// [formualizer-docgen:schema:end]
1527impl Function for QuartileInc {
1528 func_caps!(PURE, NUMERIC_ONLY);
1529 fn name(&self) -> &'static str {
1530 "QUARTILE.INC"
1531 }
1532 fn aliases(&self) -> &'static [&'static str] {
1533 &["QUARTILE"]
1534 }
1535 fn min_args(&self) -> usize {
1536 2
1537 }
1538 fn variadic(&self) -> bool {
1539 true
1540 }
1541 fn arg_schema(&self) -> &'static [ArgSchema] {
1542 &ARG_RANGE_NUM_LENIENT_ONE[..]
1543 }
1544 fn eval<'a, 'b, 'c>(
1545 &self,
1546 args: &'c [ArgumentHandle<'a, 'b>],
1547 _ctx: &dyn FunctionContext<'b>,
1548 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1549 if args.len() < 2 {
1550 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1551 ExcelError::new_num(),
1552 )));
1553 }
1554 let qv = scalar_like_value(args.last().unwrap())?;
1555 let q = match coerce_num(&qv) {
1556 Ok(n) => n,
1557 Err(_) => {
1558 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1559 ExcelError::new_num(),
1560 )));
1561 }
1562 };
1563 let q_i = q as i64;
1564 if !(0..=4).contains(&q_i) {
1565 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1566 ExcelError::new_num(),
1567 )));
1568 }
1569 let mut nums = collect_numeric_stats(&args[..args.len() - 1])?;
1570 if nums.is_empty() {
1571 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1572 ExcelError::new_num(),
1573 )));
1574 }
1575 let p = match q_i {
1576 0 => {
1577 let v = nth_smallest(&mut nums, 0);
1578 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(v)));
1579 }
1580 4 => {
1581 let idx = nums.len() - 1;
1582 let v = nth_smallest(&mut nums, idx);
1583 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(v)));
1584 }
1585 1 => 0.25,
1586 2 => 0.5,
1587 3 => 0.75,
1588 _ => {
1589 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1590 ExcelError::new_num(),
1591 )));
1592 }
1593 };
1594 match percentile_inc(&mut nums, p) {
1595 Ok(v) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(v))),
1596 Err(e) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
1597 }
1598 }
1599}
1600
1601/// Returns an exclusive quartile value for a data set.
1602///
1603/// `QUARTILE.EXC` applies exclusive percentile interpolation and supports quartiles `1` through
1604/// `3`.
1605///
1606/// # Remarks
1607/// - Valid quartile index values are `1`, `2`, and `3`.
1608/// - Returns `#NUM!` for invalid quartile index values.
1609/// - Returns `#NUM!` when the input is too small for exclusive quartile interpolation.
1610///
1611/// # Examples
1612///
1613/// ```yaml,sandbox
1614/// title: "First exclusive quartile from direct values"
1615/// formula: "=QUARTILE.EXC({1,2,3,4,5,6,7,8},1)"
1616/// expected: 2.25
1617/// ```
1618///
1619/// ```yaml,sandbox
1620/// title: "Third exclusive quartile from a range"
1621/// grid:
1622/// A1: 10
1623/// A2: 20
1624/// A3: 30
1625/// A4: 40
1626/// A5: 50
1627/// A6: 60
1628/// A7: 70
1629/// A8: 80
1630/// formula: "=QUARTILE.EXC(A1:A8,3)"
1631/// expected: 67.5
1632/// ```
1633///
1634/// ```yaml,docs
1635/// related:
1636/// - QUARTILE.INC
1637/// - PERCENTILE.EXC
1638/// - MEDIAN
1639/// faq:
1640/// - q: "Why can QUARTILE.EXC return #NUM! on small datasets?"
1641/// a: "Exclusive quartiles need enough data for valid interior rank interpolation."
1642/// ```
1643#[derive(Debug)]
1644pub struct QuartileExc; // quartile exclusive
1645/// [formualizer-docgen:schema:start]
1646/// Name: QUARTILE.EXC
1647/// Type: QuartileExc
1648/// Min args: 2
1649/// Max args: variadic
1650/// Variadic: true
1651/// Signature: QUARTILE.EXC(arg1...: number@range)
1652/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1653/// Caps: PURE, NUMERIC_ONLY
1654/// [formualizer-docgen:schema:end]
1655impl Function for QuartileExc {
1656 func_caps!(PURE, NUMERIC_ONLY);
1657 fn name(&self) -> &'static str {
1658 "QUARTILE.EXC"
1659 }
1660 fn min_args(&self) -> usize {
1661 2
1662 }
1663 fn variadic(&self) -> bool {
1664 true
1665 }
1666 fn arg_schema(&self) -> &'static [ArgSchema] {
1667 &ARG_RANGE_NUM_LENIENT_ONE[..]
1668 }
1669 fn eval<'a, 'b, 'c>(
1670 &self,
1671 args: &'c [ArgumentHandle<'a, 'b>],
1672 _ctx: &dyn FunctionContext<'b>,
1673 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1674 if args.len() < 2 {
1675 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1676 ExcelError::new_num(),
1677 )));
1678 }
1679 let qv = scalar_like_value(args.last().unwrap())?;
1680 let q = match coerce_num(&qv) {
1681 Ok(n) => n,
1682 Err(_) => {
1683 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1684 ExcelError::new_num(),
1685 )));
1686 }
1687 };
1688 let q_i = q as i64;
1689 if !(1..=3).contains(&q_i) {
1690 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1691 ExcelError::new_num(),
1692 )));
1693 }
1694 let mut nums = collect_numeric_stats(&args[..args.len() - 1])?;
1695 if nums.len() < 2 {
1696 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1697 ExcelError::new_num(),
1698 )));
1699 }
1700 let p = match q_i {
1701 1 => 0.25,
1702 2 => 0.5,
1703 3 => 0.75,
1704 _ => {
1705 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1706 ExcelError::new_num(),
1707 )));
1708 }
1709 };
1710 match percentile_exc(&mut nums, p) {
1711 Ok(v) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(v))),
1712 Err(e) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
1713 }
1714 }
1715}
1716
1717/// Multiplies all numeric arguments and returns their product.
1718///
1719/// `PRODUCT` is useful for chained growth factors, scaling ratios, and compound multipliers.
1720///
1721/// # Remarks
1722/// - Non-numeric values in referenced ranges are ignored.
1723/// - Returns `0` when no numeric values are found.
1724/// - Direct scalar arguments still attempt numeric coercion.
1725///
1726/// # Examples
1727///
1728/// ```yaml,sandbox
1729/// title: "Product of scalar values"
1730/// formula: "=PRODUCT(2,3,4)"
1731/// expected: 24
1732/// ```
1733///
1734/// ```yaml,sandbox
1735/// title: "Product from a range"
1736/// grid:
1737/// A1: 1
1738/// A2: 5
1739/// A3: 10
1740/// formula: "=PRODUCT(A1:A3)"
1741/// expected: 50
1742/// ```
1743///
1744/// ```yaml,docs
1745/// related:
1746/// - SUM
1747/// - GEOMEAN
1748/// - SUMPRODUCT
1749/// faq:
1750/// - q: "Why does PRODUCT return 0 when no numeric inputs are found?"
1751/// a: "This implementation returns 0 for an empty numeric set after filtering."
1752/// ```
1753#[derive(Debug)]
1754pub struct ProductFn;
1755/// [formualizer-docgen:schema:start]
1756/// Name: PRODUCT
1757/// Type: ProductFn
1758/// Min args: 1
1759/// Max args: variadic
1760/// Variadic: true
1761/// Signature: PRODUCT(arg1...: number@range)
1762/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1763/// Caps: PURE, REDUCTION, NUMERIC_ONLY
1764/// [formualizer-docgen:schema:end]
1765impl Function for ProductFn {
1766 func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
1767 fn name(&self) -> &'static str {
1768 "PRODUCT"
1769 }
1770 fn min_args(&self) -> usize {
1771 1
1772 }
1773 fn variadic(&self) -> bool {
1774 true
1775 }
1776 fn dependency_contract(&self, arity: usize) -> Option<FunctionDependencyContract> {
1777 FunctionDependencyContract::static_reduction(arity, self.min_args())
1778 }
1779 fn arg_schema(&self) -> &'static [ArgSchema] {
1780 &ARG_RANGE_NUM_LENIENT_ONE[..]
1781 }
1782 fn eval<'a, 'b, 'c>(
1783 &self,
1784 args: &'c [ArgumentHandle<'a, 'b>],
1785 _ctx: &dyn FunctionContext<'b>,
1786 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1787 let nums = collect_numeric_stats(args)?;
1788 if nums.is_empty() {
1789 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0)));
1790 }
1791 let result = nums.iter().product::<f64>();
1792 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
1793 result,
1794 )))
1795 }
1796}
1797
1798/// Returns the geometric mean of positive numeric values.
1799///
1800/// `GEOMEAN` is commonly used for rates of change and multiplicative growth comparisons.
1801///
1802/// # Remarks
1803/// - All numeric inputs must be strictly greater than `0`.
1804/// - Returns `#NUM!` if any value is `<= 0`, or if no numeric values are provided.
1805/// - Non-numeric values in referenced ranges are ignored.
1806///
1807/// # Examples
1808///
1809/// ```yaml,sandbox
1810/// title: "Geometric mean from scalar values"
1811/// formula: "=GEOMEAN(4,16)"
1812/// expected: 8
1813/// ```
1814///
1815/// ```yaml,sandbox
1816/// title: "Geometric mean from a range"
1817/// grid:
1818/// A1: 1
1819/// A2: 3
1820/// A3: 9
1821/// formula: "=GEOMEAN(A1:A3)"
1822/// expected: 3
1823/// ```
1824///
1825/// ```yaml,docs
1826/// related:
1827/// - HARMEAN
1828/// - PRODUCT
1829/// - AVERAGE
1830/// faq:
1831/// - q: "When does GEOMEAN return #NUM!?"
1832/// a: "GEOMEAN returns #NUM! if any numeric value is <= 0 or if no numeric values exist."
1833/// ```
1834#[derive(Debug)]
1835pub struct GeomeanFn;
1836/// [formualizer-docgen:schema:start]
1837/// Name: GEOMEAN
1838/// Type: GeomeanFn
1839/// Min args: 1
1840/// Max args: variadic
1841/// Variadic: true
1842/// Signature: GEOMEAN(arg1...: number@range)
1843/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1844/// Caps: PURE, REDUCTION, NUMERIC_ONLY
1845/// [formualizer-docgen:schema:end]
1846impl Function for GeomeanFn {
1847 func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
1848 fn name(&self) -> &'static str {
1849 "GEOMEAN"
1850 }
1851 fn min_args(&self) -> usize {
1852 1
1853 }
1854 fn variadic(&self) -> bool {
1855 true
1856 }
1857 fn arg_schema(&self) -> &'static [ArgSchema] {
1858 &ARG_RANGE_NUM_LENIENT_ONE[..]
1859 }
1860 fn eval<'a, 'b, 'c>(
1861 &self,
1862 args: &'c [ArgumentHandle<'a, 'b>],
1863 _ctx: &dyn FunctionContext<'b>,
1864 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1865 let nums = collect_numeric_stats(args)?;
1866 if nums.is_empty() {
1867 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1868 ExcelError::new_num(),
1869 )));
1870 }
1871 // All values must be positive
1872 if nums.iter().any(|&n| n <= 0.0) {
1873 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1874 ExcelError::new_num(),
1875 )));
1876 }
1877 // Geometric mean = (x1 * x2 * ... * xn)^(1/n)
1878 // Use log to avoid overflow: exp(mean(ln(x)))
1879 let log_sum: f64 = nums.iter().map(|x| x.ln()).sum();
1880 let result = (log_sum / nums.len() as f64).exp();
1881 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
1882 result,
1883 )))
1884 }
1885}
1886
1887/// Returns the harmonic mean of positive numeric values.
1888///
1889/// `HARMEAN` emphasizes smaller values and is useful for averaging rates and ratios.
1890///
1891/// # Remarks
1892/// - All numeric inputs must be strictly greater than `0`.
1893/// - Returns `#NUM!` if any value is `<= 0`, or if no numeric values are provided.
1894/// - Non-numeric values in referenced ranges are ignored.
1895///
1896/// # Examples
1897///
1898/// ```yaml,sandbox
1899/// title: "Harmonic mean from scalar values"
1900/// formula: "=HARMEAN(1,2,4)"
1901/// expected: 1.7142857142857142
1902/// ```
1903///
1904/// ```yaml,sandbox
1905/// title: "Harmonic mean from a range"
1906/// grid:
1907/// A1: 2
1908/// A2: 3
1909/// A3: 6
1910/// formula: "=HARMEAN(A1:A3)"
1911/// expected: 3
1912/// ```
1913///
1914/// ```yaml,docs
1915/// related:
1916/// - GEOMEAN
1917/// - AVERAGE
1918/// - PRODUCT
1919/// faq:
1920/// - q: "Why does HARMEAN fail on zeros?"
1921/// a: "Harmonic mean uses reciprocals, so inputs must be strictly positive."
1922/// ```
1923#[derive(Debug)]
1924pub struct HarmeanFn;
1925/// [formualizer-docgen:schema:start]
1926/// Name: HARMEAN
1927/// Type: HarmeanFn
1928/// Min args: 1
1929/// Max args: variadic
1930/// Variadic: true
1931/// Signature: HARMEAN(arg1...: number@range)
1932/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1933/// Caps: PURE, REDUCTION, NUMERIC_ONLY
1934/// [formualizer-docgen:schema:end]
1935impl Function for HarmeanFn {
1936 func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
1937 fn name(&self) -> &'static str {
1938 "HARMEAN"
1939 }
1940 fn min_args(&self) -> usize {
1941 1
1942 }
1943 fn variadic(&self) -> bool {
1944 true
1945 }
1946 fn arg_schema(&self) -> &'static [ArgSchema] {
1947 &ARG_RANGE_NUM_LENIENT_ONE[..]
1948 }
1949 fn eval<'a, 'b, 'c>(
1950 &self,
1951 args: &'c [ArgumentHandle<'a, 'b>],
1952 _ctx: &dyn FunctionContext<'b>,
1953 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1954 let nums = collect_numeric_stats(args)?;
1955 if nums.is_empty() {
1956 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1957 ExcelError::new_num(),
1958 )));
1959 }
1960 // All values must be positive
1961 if nums.iter().any(|&n| n <= 0.0) {
1962 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1963 ExcelError::new_num(),
1964 )));
1965 }
1966 // Harmonic mean = n / sum(1/x)
1967 let sum_reciprocals: f64 = nums.iter().map(|x| 1.0 / x).sum();
1968 let result = nums.len() as f64 / sum_reciprocals;
1969 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
1970 result,
1971 )))
1972 }
1973}
1974
1975/// Returns the average of absolute deviations from the mean.
1976///
1977/// `AVEDEV` provides a robust spread measure that is less sensitive to outliers than squared-error
1978/// metrics.
1979///
1980/// # Remarks
1981/// - Returns `#NUM!` when no numeric values are available.
1982/// - Non-numeric values in referenced ranges are ignored.
1983/// - Uses the arithmetic mean as the center point.
1984///
1985/// # Examples
1986///
1987/// ```yaml,sandbox
1988/// title: "Average absolute deviation from scalar values"
1989/// formula: "=AVEDEV(2,4,6)"
1990/// expected: 1.3333333333333333
1991/// ```
1992///
1993/// ```yaml,sandbox
1994/// title: "Average absolute deviation from a range"
1995/// grid:
1996/// A1: 1
1997/// A2: 1
1998/// A3: 3
1999/// A4: 5
2000/// formula: "=AVEDEV(A1:A4)"
2001/// expected: 1.5
2002/// ```
2003///
2004/// ```yaml,docs
2005/// related:
2006/// - DEVSQ
2007/// - STDEV.S
2008/// - VAR.S
2009/// faq:
2010/// - q: "What center does AVEDEV use for deviations?"
2011/// a: "It computes absolute deviations around the arithmetic mean of included values."
2012/// ```
2013#[derive(Debug)]
2014pub struct AvedevFn;
2015/// [formualizer-docgen:schema:start]
2016/// Name: AVEDEV
2017/// Type: AvedevFn
2018/// Min args: 1
2019/// Max args: variadic
2020/// Variadic: true
2021/// Signature: AVEDEV(arg1...: number@range)
2022/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
2023/// Caps: PURE, REDUCTION, NUMERIC_ONLY
2024/// [formualizer-docgen:schema:end]
2025impl Function for AvedevFn {
2026 func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
2027 fn name(&self) -> &'static str {
2028 "AVEDEV"
2029 }
2030 fn min_args(&self) -> usize {
2031 1
2032 }
2033 fn variadic(&self) -> bool {
2034 true
2035 }
2036 fn arg_schema(&self) -> &'static [ArgSchema] {
2037 &ARG_RANGE_NUM_LENIENT_ONE[..]
2038 }
2039 fn eval<'a, 'b, 'c>(
2040 &self,
2041 args: &'c [ArgumentHandle<'a, 'b>],
2042 _ctx: &dyn FunctionContext<'b>,
2043 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2044 let nums = collect_numeric_stats(args)?;
2045 if nums.is_empty() {
2046 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2047 ExcelError::new_num(),
2048 )));
2049 }
2050 let mean = nums.iter().sum::<f64>() / nums.len() as f64;
2051 let avedev = nums.iter().map(|x| (x - mean).abs()).sum::<f64>() / nums.len() as f64;
2052 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
2053 avedev,
2054 )))
2055 }
2056}
2057
2058/// Returns the sum of squared deviations from the mean.
2059///
2060/// `DEVSQ` is useful for variance-related calculations and diagnostics of spread.
2061///
2062/// # Remarks
2063/// - Returns `#NUM!` when no numeric values are available.
2064/// - Non-numeric values in referenced ranges are ignored.
2065/// - Uses the arithmetic mean of included values.
2066///
2067/// # Examples
2068///
2069/// ```yaml,sandbox
2070/// title: "Sum of squared deviations from scalar values"
2071/// formula: "=DEVSQ(2,4,6)"
2072/// expected: 8
2073/// ```
2074///
2075/// ```yaml,sandbox
2076/// title: "Sum of squared deviations from a range"
2077/// grid:
2078/// A1: 1
2079/// A2: 2
2080/// A3: 3
2081/// A4: 4
2082/// formula: "=DEVSQ(A1:A4)"
2083/// expected: 5
2084/// ```
2085#[derive(Debug)]
2086pub struct DevsqFn;
2087
2088/* ─────────────────────────── MAXIFS / MINIFS ──────────────────────────── */
2089
2090use super::utils::{ARG_ANY_ONE, criteria_match};
2091
2092/// Returns the maximum numeric value in a range that meets all criteria.
2093///
2094/// `MAXIFS` applies one or more `(criteria_range, criteria)` pairs and returns the largest
2095/// matching numeric value.
2096///
2097/// # Remarks
2098/// - Arguments must be `target_range` plus one or more criteria pairs.
2099/// - Criteria are combined with logical AND.
2100/// - Returns `0` when no cells satisfy all criteria.
2101/// - Non-numeric cells in `target_range` are ignored.
2102///
2103/// # Examples
2104///
2105/// ```yaml,sandbox
2106/// title: "Maximum value for one condition"
2107/// grid:
2108/// A1: 10
2109/// A2: 20
2110/// A3: 15
2111/// B1: "East"
2112/// B2: "West"
2113/// B3: "East"
2114/// formula: "=MAXIFS(A1:A3,B1:B3,\"East\")"
2115/// expected: 15
2116/// ```
2117///
2118/// ```yaml,sandbox
2119/// title: "Maximum value with two criteria"
2120/// grid:
2121/// A1: 100
2122/// A2: 80
2123/// A3: 90
2124/// A4: 70
2125/// B1: "A"
2126/// B2: "A"
2127/// B3: "B"
2128/// B4: "B"
2129/// C1: "Q1"
2130/// C2: "Q2"
2131/// C3: "Q1"
2132/// C4: "Q1"
2133/// formula: "=MAXIFS(A1:A4,B1:B4,\"B\",C1:C4,\"Q1\")"
2134/// expected: 90
2135/// ```
2136///
2137/// ```yaml,docs
2138/// related:
2139/// - MINIFS
2140/// - MAX
2141/// - SUMIFS
2142/// faq:
2143/// - q: "What does MAXIFS return when no rows match all criteria?"
2144/// a: "It returns 0 when no numeric target cells satisfy every criterion."
2145/// ```
2146#[derive(Debug)]
2147pub struct MaxIfsFn;
2148/// [formualizer-docgen:schema:start]
2149/// Name: MAXIFS
2150/// Type: MaxIfsFn
2151/// Min args: 3
2152/// Max args: variadic
2153/// Variadic: true
2154/// Signature: MAXIFS(arg1...: any@scalar)
2155/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
2156/// Caps: PURE, REDUCTION
2157/// [formualizer-docgen:schema:end]
2158impl Function for MaxIfsFn {
2159 func_caps!(PURE, REDUCTION);
2160 fn name(&self) -> &'static str {
2161 "MAXIFS"
2162 }
2163 fn min_args(&self) -> usize {
2164 3
2165 }
2166 fn variadic(&self) -> bool {
2167 true
2168 }
2169 fn arg_schema(&self) -> &'static [ArgSchema] {
2170 &ARG_ANY_ONE[..]
2171 }
2172 fn eval<'a, 'b, 'c>(
2173 &self,
2174 args: &'c [ArgumentHandle<'a, 'b>],
2175 _ctx: &dyn FunctionContext<'b>,
2176 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2177 eval_maxminifs(args, true)
2178 }
2179}
2180
2181/// Returns the minimum numeric value in a range that meets all criteria.
2182///
2183/// `MINIFS` evaluates one or more `(criteria_range, criteria)` pairs and returns the smallest
2184/// matching numeric value.
2185///
2186/// # Remarks
2187/// - Arguments must be `target_range` plus one or more criteria pairs.
2188/// - Criteria are combined with logical AND.
2189/// - Returns `0` when no cells satisfy all criteria.
2190/// - Non-numeric cells in `target_range` are ignored.
2191///
2192/// # Examples
2193///
2194/// ```yaml,sandbox
2195/// title: "Minimum value for one condition"
2196/// grid:
2197/// A1: 10
2198/// A2: 20
2199/// A3: 15
2200/// B1: "East"
2201/// B2: "West"
2202/// B3: "East"
2203/// formula: "=MINIFS(A1:A3,B1:B3,\"East\")"
2204/// expected: 10
2205/// ```
2206///
2207/// ```yaml,sandbox
2208/// title: "Minimum value with two criteria"
2209/// grid:
2210/// A1: 100
2211/// A2: 80
2212/// A3: 90
2213/// A4: 70
2214/// B1: "A"
2215/// B2: "A"
2216/// B3: "B"
2217/// B4: "B"
2218/// C1: "Q1"
2219/// C2: "Q2"
2220/// C3: "Q1"
2221/// C4: "Q1"
2222/// formula: "=MINIFS(A1:A4,B1:B4,\"B\",C1:C4,\"Q1\")"
2223/// expected: 70
2224/// ```
2225///
2226/// ```yaml,docs
2227/// related:
2228/// - MAXIFS
2229/// - MIN
2230/// - SUMIFS
2231/// faq:
2232/// - q: "How does MINIFS treat non-numeric target cells?"
2233/// a: "Non-numeric target cells are ignored; only numeric matches are eligible."
2234/// ```
2235#[derive(Debug)]
2236pub struct MinIfsFn;
2237/// [formualizer-docgen:schema:start]
2238/// Name: MINIFS
2239/// Type: MinIfsFn
2240/// Min args: 3
2241/// Max args: variadic
2242/// Variadic: true
2243/// Signature: MINIFS(arg1...: any@scalar)
2244/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
2245/// Caps: PURE, REDUCTION
2246/// [formualizer-docgen:schema:end]
2247impl Function for MinIfsFn {
2248 func_caps!(PURE, REDUCTION);
2249 fn name(&self) -> &'static str {
2250 "MINIFS"
2251 }
2252 fn min_args(&self) -> usize {
2253 3
2254 }
2255 fn variadic(&self) -> bool {
2256 true
2257 }
2258 fn arg_schema(&self) -> &'static [ArgSchema] {
2259 &ARG_ANY_ONE[..]
2260 }
2261 fn eval<'a, 'b, 'c>(
2262 &self,
2263 args: &'c [ArgumentHandle<'a, 'b>],
2264 _ctx: &dyn FunctionContext<'b>,
2265 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2266 eval_maxminifs(args, false)
2267 }
2268}
2269
2270/// Shared implementation for MAXIFS and MINIFS
2271fn eval_maxminifs<'a, 'b>(
2272 args: &[ArgumentHandle<'a, 'b>],
2273 is_max: bool,
2274) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2275 // Validate argument count: must be target_range + N pairs
2276 if args.len() < 3 || !(args.len() - 1).is_multiple_of(2) {
2277 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2278 ExcelError::new_value().with_message(format!(
2279 "Function expects 1 target_range followed by N pairs (criteria_range, criteria); got {} args",
2280 args.len()
2281 )),
2282 )));
2283 }
2284
2285 // Get target range
2286 let target_view = match args[0].range_view() {
2287 Ok(v) => v,
2288 Err(_) => {
2289 // Single value case - if criteria match, return that value
2290 let target_val = args[0].value()?.into_literal();
2291 if let LiteralValue::Error(e) = target_val {
2292 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
2293 }
2294 // Check all criteria against empty/scalar
2295 let mut all_match = true;
2296 for i in (1..args.len()).step_by(2) {
2297 let crit_val = args[i].value()?.into_literal();
2298 let pred = crate::args::parse_criteria(&args[i + 1].value()?.into_literal())?;
2299 if !criteria_match(&pred, &crit_val) {
2300 all_match = false;
2301 break;
2302 }
2303 }
2304 if all_match {
2305 return match coerce_num(&target_val) {
2306 Ok(n) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(n))),
2307 Err(_) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0))),
2308 };
2309 }
2310 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0)));
2311 }
2312 };
2313
2314 let (rows, cols) = target_view.dims();
2315
2316 // Parse all criteria
2317 let mut criteria_ranges = Vec::new();
2318 let mut predicates = Vec::new();
2319 for i in (1..args.len()).step_by(2) {
2320 let crit_view = args[i].range_view().ok();
2321 let pred = crate::args::parse_criteria(&args[i + 1].value()?.into_literal())?;
2322 criteria_ranges.push(crit_view);
2323 predicates.push(pred);
2324 }
2325
2326 // Iterate through all cells and find max/min where all criteria match
2327 let mut result: Option<f64> = None;
2328
2329 for r in 0..rows {
2330 for c in 0..cols {
2331 // Check all criteria
2332 let mut all_match = true;
2333 for (crit_idx, pred) in predicates.iter().enumerate() {
2334 let crit_val = match &criteria_ranges[crit_idx] {
2335 Some(view) => {
2336 let (cr, cc) = view.dims();
2337 if r < cr && c < cc {
2338 view.get_cell(r, c)
2339 } else {
2340 LiteralValue::Empty
2341 }
2342 }
2343 None => LiteralValue::Empty,
2344 };
2345 if !criteria_match(pred, &crit_val) {
2346 all_match = false;
2347 break;
2348 }
2349 }
2350
2351 if all_match {
2352 let target_val = target_view.get_cell(r, c);
2353 match target_val {
2354 LiteralValue::Error(e) => {
2355 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
2356 }
2357 LiteralValue::Number(n) => {
2358 result = Some(match result {
2359 None => n,
2360 Some(curr) => {
2361 if is_max {
2362 curr.max(n)
2363 } else {
2364 curr.min(n)
2365 }
2366 }
2367 });
2368 }
2369 LiteralValue::Int(i) => {
2370 let n = i as f64;
2371 result = Some(match result {
2372 None => n,
2373 Some(curr) => {
2374 if is_max {
2375 curr.max(n)
2376 } else {
2377 curr.min(n)
2378 }
2379 }
2380 });
2381 }
2382 _ => {} // Skip non-numeric
2383 }
2384 }
2385 }
2386 }
2387
2388 // Excel returns 0 if no matches found
2389 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
2390 result.unwrap_or(0.0),
2391 )))
2392}
2393
2394/* ─────────────────────────── TRIMMEAN ──────────────────────────── */
2395
2396/// Returns the mean after trimming a percentage of values from both tails.
2397///
2398/// `TRIMMEAN` sorts numeric data, removes an equal count from low and high ends, then averages the
2399/// remaining interior values.
2400///
2401/// # Remarks
2402/// - `percent` must satisfy `0 <= percent < 1`.
2403/// - The trimmed count per side is `floor(n * percent / 2)`.
2404/// - Returns `#NUM!` for invalid percent values or when no numeric values are available.
2405///
2406/// # Examples
2407///
2408/// ```yaml,sandbox
2409/// title: "Trimmed mean from direct values"
2410/// formula: "=TRIMMEAN({1,2,3,4,5,6},0.3333333333333333)"
2411/// expected: 3.5
2412/// ```
2413///
2414/// ```yaml,sandbox
2415/// title: "Trimmed mean from a range"
2416/// grid:
2417/// A1: 10
2418/// A2: 12
2419/// A3: 13
2420/// A4: 20
2421/// A5: 21
2422/// A6: 30
2423/// formula: "=TRIMMEAN(A1:A6,0.4)"
2424/// expected: 16.5
2425/// ```
2426#[derive(Debug)]
2427pub struct TrimmeanFn;
2428/// [formualizer-docgen:schema:start]
2429/// Name: TRIMMEAN
2430/// Type: TrimmeanFn
2431/// Min args: 2
2432/// Max args: 1
2433/// Variadic: false
2434/// Signature: TRIMMEAN(arg1: number@range)
2435/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
2436/// Caps: PURE, REDUCTION, NUMERIC_ONLY
2437/// [formualizer-docgen:schema:end]
2438impl Function for TrimmeanFn {
2439 func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
2440 fn name(&self) -> &'static str {
2441 "TRIMMEAN"
2442 }
2443 fn min_args(&self) -> usize {
2444 2
2445 }
2446 fn arg_schema(&self) -> &'static [ArgSchema] {
2447 &ARG_RANGE_NUM_LENIENT_ONE[..]
2448 }
2449 fn eval<'a, 'b, 'c>(
2450 &self,
2451 args: &'c [ArgumentHandle<'a, 'b>],
2452 _ctx: &dyn FunctionContext<'b>,
2453 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2454 let mut nums = collect_numeric_stats(&args[0..1])?;
2455 if nums.is_empty() {
2456 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2457 ExcelError::new_num(),
2458 )));
2459 }
2460
2461 let percent = match args[1].value()?.into_literal() {
2462 LiteralValue::Error(e) => {
2463 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
2464 }
2465 other => coerce_num(&other)?,
2466 };
2467
2468 // Percent must be between 0 and 1 (exclusive of 1)
2469 if !(0.0..1.0).contains(&percent) {
2470 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2471 ExcelError::new_num(),
2472 )));
2473 }
2474
2475 nums.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
2476
2477 let n = nums.len();
2478 // Number of values to exclude from each end
2479 let exclude = ((n as f64 * percent) / 2.0).floor() as usize;
2480
2481 if 2 * exclude >= n {
2482 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2483 ExcelError::new_num(),
2484 )));
2485 }
2486
2487 let trimmed = &nums[exclude..n - exclude];
2488 let sum: f64 = trimmed.iter().sum();
2489 let mean = sum / trimmed.len() as f64;
2490
2491 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(mean)))
2492 }
2493}
2494
2495/* ─────────────────────────── CORREL ──────────────────────────── */
2496
2497/// Helper to collect two paired arrays for regression/correlation functions
2498fn collect_paired_arrays(args: &[ArgumentHandle]) -> Result<(Vec<f64>, Vec<f64>), ExcelError> {
2499 let y_nums = collect_numeric_stats(&args[0..1])?;
2500 let x_nums = collect_numeric_stats(&args[1..2])?;
2501
2502 // Arrays must have same length
2503 if y_nums.len() != x_nums.len() {
2504 return Err(ExcelError::new_na());
2505 }
2506
2507 if y_nums.is_empty() {
2508 return Err(ExcelError::new_div());
2509 }
2510
2511 Ok((y_nums, x_nums))
2512}
2513
2514/// Returns the Pearson correlation coefficient between two numeric arrays.
2515///
2516/// `CORREL` measures linear relationship strength from `-1` (perfect inverse) to `1` (perfect
2517/// direct).
2518///
2519/// # Remarks
2520/// - Both arrays must produce the same number of numeric values.
2521/// - Returns `#N/A` when array lengths differ.
2522/// - Returns `#DIV/0!` when either series has zero variance.
2523///
2524/// # Examples
2525///
2526/// ```yaml,sandbox
2527/// title: "Perfect positive linear correlation"
2528/// formula: "=CORREL({2,4,6},{1,2,3})"
2529/// expected: 1
2530/// ```
2531///
2532/// ```yaml,sandbox
2533/// title: "Perfect negative linear correlation"
2534/// grid:
2535/// A1: 10
2536/// A2: 8
2537/// A3: 6
2538/// B1: 1
2539/// B2: 2
2540/// B3: 3
2541/// formula: "=CORREL(A1:A3,B1:B3)"
2542/// expected: -1
2543/// ```
2544#[derive(Debug)]
2545pub struct CorrelFn;
2546/// [formualizer-docgen:schema:start]
2547/// Name: CORREL
2548/// Type: CorrelFn
2549/// Min args: 2
2550/// Max args: 1
2551/// Variadic: false
2552/// Signature: CORREL(arg1: number@range)
2553/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
2554/// Caps: PURE, REDUCTION, NUMERIC_ONLY
2555/// [formualizer-docgen:schema:end]
2556impl Function for CorrelFn {
2557 func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
2558 fn name(&self) -> &'static str {
2559 "CORREL"
2560 }
2561 fn min_args(&self) -> usize {
2562 2
2563 }
2564 fn arg_schema(&self) -> &'static [ArgSchema] {
2565 &ARG_RANGE_NUM_LENIENT_ONE[..]
2566 }
2567 fn eval<'a, 'b, 'c>(
2568 &self,
2569 args: &'c [ArgumentHandle<'a, 'b>],
2570 _ctx: &dyn FunctionContext<'b>,
2571 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2572 let (y, x) = match collect_paired_arrays(args) {
2573 Ok(v) => v,
2574 Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
2575 };
2576
2577 let n = x.len() as f64;
2578 let mean_x = x.iter().sum::<f64>() / n;
2579 let mean_y = y.iter().sum::<f64>() / n;
2580
2581 let mut sum_xy = 0.0;
2582 let mut sum_x2 = 0.0;
2583 let mut sum_y2 = 0.0;
2584
2585 for i in 0..x.len() {
2586 let dx = x[i] - mean_x;
2587 let dy = y[i] - mean_y;
2588 sum_xy += dx * dy;
2589 sum_x2 += dx * dx;
2590 sum_y2 += dy * dy;
2591 }
2592
2593 let denom = (sum_x2 * sum_y2).sqrt();
2594 if denom == 0.0 {
2595 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2596 ExcelError::new_div(),
2597 )));
2598 }
2599
2600 let correl = sum_xy / denom;
2601 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
2602 correl,
2603 )))
2604 }
2605}
2606
2607/* ─────────────────────────── SLOPE ──────────────────────────── */
2608
2609/// Returns the slope of the linear regression line for paired data.
2610///
2611/// `SLOPE` fits `y = m*x + b` and returns `m`, the rate of change in `y` per unit of `x`.
2612///
2613/// # Remarks
2614/// - `known_y` and `known_x` must have the same numeric length.
2615/// - Returns `#N/A` for mismatched lengths.
2616/// - Returns `#DIV/0!` if all `x` values are identical.
2617///
2618/// # Examples
2619///
2620/// ```yaml,sandbox
2621/// title: "Positive slope from direct arrays"
2622/// formula: "=SLOPE({2,4,6},{1,2,3})"
2623/// expected: 2
2624/// ```
2625///
2626/// ```yaml,sandbox
2627/// title: "Negative slope from ranges"
2628/// grid:
2629/// A1: 10
2630/// A2: 8
2631/// A3: 6
2632/// B1: 1
2633/// B2: 2
2634/// B3: 3
2635/// formula: "=SLOPE(A1:A3,B1:B3)"
2636/// expected: -2
2637/// ```
2638#[derive(Debug)]
2639pub struct SlopeFn;
2640/// [formualizer-docgen:schema:start]
2641/// Name: SLOPE
2642/// Type: SlopeFn
2643/// Min args: 2
2644/// Max args: 1
2645/// Variadic: false
2646/// Signature: SLOPE(arg1: number@range)
2647/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
2648/// Caps: PURE, REDUCTION, NUMERIC_ONLY
2649/// [formualizer-docgen:schema:end]
2650impl Function for SlopeFn {
2651 func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
2652 fn name(&self) -> &'static str {
2653 "SLOPE"
2654 }
2655 fn min_args(&self) -> usize {
2656 2
2657 }
2658 fn arg_schema(&self) -> &'static [ArgSchema] {
2659 &ARG_RANGE_NUM_LENIENT_ONE[..]
2660 }
2661 fn eval<'a, 'b, 'c>(
2662 &self,
2663 args: &'c [ArgumentHandle<'a, 'b>],
2664 _ctx: &dyn FunctionContext<'b>,
2665 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2666 let (y, x) = match collect_paired_arrays(args) {
2667 Ok(v) => v,
2668 Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
2669 };
2670
2671 let n = x.len() as f64;
2672 let mean_x = x.iter().sum::<f64>() / n;
2673 let mean_y = y.iter().sum::<f64>() / n;
2674
2675 let mut sum_xy = 0.0;
2676 let mut sum_x2 = 0.0;
2677
2678 for i in 0..x.len() {
2679 let dx = x[i] - mean_x;
2680 let dy = y[i] - mean_y;
2681 sum_xy += dx * dy;
2682 sum_x2 += dx * dx;
2683 }
2684
2685 if sum_x2 == 0.0 {
2686 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2687 ExcelError::new_div(),
2688 )));
2689 }
2690
2691 let slope = sum_xy / sum_x2;
2692 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
2693 slope,
2694 )))
2695 }
2696}
2697
2698/* ─────────────────────────── INTERCEPT ──────────────────────────── */
2699
2700/// Returns the y-intercept of the linear regression line for paired data.
2701///
2702/// `INTERCEPT` fits `y = m*x + b` and returns `b`, the predicted `y` when `x = 0`.
2703///
2704/// # Remarks
2705/// - `known_y` and `known_x` must have the same numeric length.
2706/// - Returns `#N/A` for mismatched lengths.
2707/// - Returns `#DIV/0!` if all `x` values are identical.
2708///
2709/// # Examples
2710///
2711/// ```yaml,sandbox
2712/// title: "Positive intercept from direct arrays"
2713/// formula: "=INTERCEPT({3,5,7},{1,2,3})"
2714/// expected: 1
2715/// ```
2716///
2717/// ```yaml,sandbox
2718/// title: "Intercept from range-based linear trend"
2719/// grid:
2720/// A1: 10
2721/// A2: 8
2722/// A3: 6
2723/// B1: 1
2724/// B2: 2
2725/// B3: 3
2726/// formula: "=INTERCEPT(A1:A3,B1:B3)"
2727/// expected: 12
2728/// ```
2729#[derive(Debug)]
2730pub struct InterceptFn;
2731/// [formualizer-docgen:schema:start]
2732/// Name: INTERCEPT
2733/// Type: InterceptFn
2734/// Min args: 2
2735/// Max args: 1
2736/// Variadic: false
2737/// Signature: INTERCEPT(arg1: number@range)
2738/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
2739/// Caps: PURE, REDUCTION, NUMERIC_ONLY
2740/// [formualizer-docgen:schema:end]
2741impl Function for InterceptFn {
2742 func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
2743 fn name(&self) -> &'static str {
2744 "INTERCEPT"
2745 }
2746 fn min_args(&self) -> usize {
2747 2
2748 }
2749 fn arg_schema(&self) -> &'static [ArgSchema] {
2750 &ARG_RANGE_NUM_LENIENT_ONE[..]
2751 }
2752 fn eval<'a, 'b, 'c>(
2753 &self,
2754 args: &'c [ArgumentHandle<'a, 'b>],
2755 _ctx: &dyn FunctionContext<'b>,
2756 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2757 let (y, x) = match collect_paired_arrays(args) {
2758 Ok(v) => v,
2759 Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
2760 };
2761
2762 let n = x.len() as f64;
2763 let mean_x = x.iter().sum::<f64>() / n;
2764 let mean_y = y.iter().sum::<f64>() / n;
2765
2766 let mut sum_xy = 0.0;
2767 let mut sum_x2 = 0.0;
2768
2769 for i in 0..x.len() {
2770 let dx = x[i] - mean_x;
2771 let dy = y[i] - mean_y;
2772 sum_xy += dx * dy;
2773 sum_x2 += dx * dx;
2774 }
2775
2776 if sum_x2 == 0.0 {
2777 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2778 ExcelError::new_div(),
2779 )));
2780 }
2781
2782 let slope = sum_xy / sum_x2;
2783 let intercept = mean_y - slope * mean_x;
2784 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
2785 intercept,
2786 )))
2787 }
2788}
2789
2790/// [formualizer-docgen:schema:start]
2791/// Name: DEVSQ
2792/// Type: DevsqFn
2793/// Min args: 1
2794/// Max args: variadic
2795/// Variadic: true
2796/// Signature: DEVSQ(arg1...: number@range)
2797/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
2798/// Caps: PURE, REDUCTION, NUMERIC_ONLY
2799/// [formualizer-docgen:schema:end]
2800impl Function for DevsqFn {
2801 func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
2802 fn name(&self) -> &'static str {
2803 "DEVSQ"
2804 }
2805 fn min_args(&self) -> usize {
2806 1
2807 }
2808 fn variadic(&self) -> bool {
2809 true
2810 }
2811 fn arg_schema(&self) -> &'static [ArgSchema] {
2812 &ARG_RANGE_NUM_LENIENT_ONE[..]
2813 }
2814 fn eval<'a, 'b, 'c>(
2815 &self,
2816 args: &'c [ArgumentHandle<'a, 'b>],
2817 _ctx: &dyn FunctionContext<'b>,
2818 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2819 let nums = collect_numeric_stats(args)?;
2820 if nums.is_empty() {
2821 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2822 ExcelError::new_num(),
2823 )));
2824 }
2825 let mean = nums.iter().sum::<f64>() / nums.len() as f64;
2826 let devsq = nums.iter().map(|x| (x - mean).powi(2)).sum::<f64>();
2827 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
2828 devsq,
2829 )))
2830 }
2831}
2832
2833/* ═══════════════════════════════════════════════════════════════════════════
2834STATISTICAL DISTRIBUTION FUNCTIONS
2835═══════════════════════════════════════════════════════════════════════════ */
2836
2837/// Helper: Standard normal CDF using error function approximation
2838fn std_norm_cdf(z: f64) -> f64 {
2839 // Use the complementary error function: Φ(z) = 0.5 * erfc(-z / sqrt(2))
2840 // Approximation using Abramowitz and Stegun formula 7.1.26
2841 let a1 = 0.254829592;
2842 let a2 = -0.284496736;
2843 let a3 = 1.421413741;
2844 let a4 = -1.453152027;
2845 let a5 = 1.061405429;
2846 let p = 0.3275911;
2847
2848 let sign = if z < 0.0 { -1.0 } else { 1.0 };
2849 let z_abs = z.abs() / std::f64::consts::SQRT_2;
2850
2851 let t = 1.0 / (1.0 + p * z_abs);
2852 let y = 1.0 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * (-z_abs * z_abs).exp();
2853
2854 0.5 * (1.0 + sign * y)
2855}
2856
2857/// Helper: Standard normal PDF
2858fn std_norm_pdf(z: f64) -> f64 {
2859 let inv_sqrt_2pi = 1.0 / (2.0 * std::f64::consts::PI).sqrt();
2860 inv_sqrt_2pi * (-0.5 * z * z).exp()
2861}
2862
2863/// Helper: Inverse standard normal CDF (probit function)
2864/// Uses Rational approximation from Abramowitz and Stegun
2865#[allow(clippy::excessive_precision)]
2866fn std_norm_inv(p: f64) -> Option<f64> {
2867 if p <= 0.0 || p >= 1.0 {
2868 return None;
2869 }
2870
2871 // Coefficients for rational approximation
2872 const A: [f64; 6] = [
2873 -3.969683028665376e+01,
2874 2.209460984245205e+02,
2875 -2.759285104469687e+02,
2876 1.383577518672690e+02,
2877 -3.066479806614716e+01,
2878 2.506628277459239e+00,
2879 ];
2880 const B: [f64; 5] = [
2881 -5.447609879822406e+01,
2882 1.615858368580409e+02,
2883 -1.556989798598866e+02,
2884 6.680131188771972e+01,
2885 -1.328068155288572e+01,
2886 ];
2887 const C: [f64; 6] = [
2888 -7.784894002430293e-03,
2889 -3.223964580411365e-01,
2890 -2.400758277161838e+00,
2891 -2.549732539343734e+00,
2892 4.374664141464968e+00,
2893 2.938163982698783e+00,
2894 ];
2895 const D: [f64; 4] = [
2896 7.784695709041462e-03,
2897 3.224671290700398e-01,
2898 2.445134137142996e+00,
2899 3.754408661907416e+00,
2900 ];
2901
2902 const P_LOW: f64 = 0.02425;
2903 const P_HIGH: f64 = 1.0 - P_LOW;
2904
2905 let q = p - 0.5;
2906
2907 if p < P_LOW {
2908 // Lower tail
2909 let r = (-2.0 * p.ln()).sqrt();
2910 let num = ((((C[0] * r + C[1]) * r + C[2]) * r + C[3]) * r + C[4]) * r + C[5];
2911 let den = (((D[0] * r + D[1]) * r + D[2]) * r + D[3]) * r + 1.0;
2912 Some(num / den)
2913 } else if p <= P_HIGH {
2914 // Central region
2915 let r = q * q;
2916 let num = ((((A[0] * r + A[1]) * r + A[2]) * r + A[3]) * r + A[4]) * r + A[5];
2917 let den = ((((B[0] * r + B[1]) * r + B[2]) * r + B[3]) * r + B[4]) * r + 1.0;
2918 Some(q * num / den)
2919 } else {
2920 // Upper tail
2921 let r = (-2.0 * (1.0 - p).ln()).sqrt();
2922 let num = ((((C[0] * r + C[1]) * r + C[2]) * r + C[3]) * r + C[4]) * r + C[5];
2923 let den = (((D[0] * r + D[1]) * r + D[2]) * r + D[3]) * r + 1.0;
2924 Some(-num / den)
2925 }
2926}
2927
2928/// Returns the standard normal probability for a z-score as either a CDF or PDF value.
2929///
2930/// Use `NORM.S.DIST` for z-based probability lookups when the distribution has mean `0` and
2931/// standard deviation `1`.
2932///
2933/// # Remarks
2934/// - Set `cumulative` to a non-zero value for the cumulative distribution `P(Z <= z)`.
2935/// - Set `cumulative` to `0` for the probability density at exactly `z`.
2936/// - Accepts any real-valued `z`; no domain clipping is applied.
2937/// - Invalid numeric coercions propagate as spreadsheet errors.
2938///
2939/// # Examples
2940///
2941/// ```yaml,sandbox
2942/// title: "Standard normal CDF at zero"
2943/// formula: "=NORM.S.DIST(0,TRUE)"
2944/// expected: 0.5
2945/// ```
2946///
2947/// ```yaml,sandbox
2948/// title: "Standard normal PDF at zero"
2949/// formula: "=NORM.S.DIST(0,FALSE)"
2950/// expected: 0.3989422804014327
2951/// ```
2952#[derive(Debug)]
2953pub struct NormSDistFn;
2954/// [formualizer-docgen:schema:start]
2955/// Name: NORM.S.DIST
2956/// Type: NormSDistFn
2957/// Min args: 2
2958/// Max args: 2
2959/// Variadic: false
2960/// Signature: NORM.S.DIST(arg1: number@scalar, arg2: number@scalar)
2961/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
2962/// Caps: PURE
2963/// [formualizer-docgen:schema:end]
2964impl Function for NormSDistFn {
2965 func_caps!(PURE);
2966 fn name(&self) -> &'static str {
2967 "NORM.S.DIST"
2968 }
2969 fn min_args(&self) -> usize {
2970 2
2971 }
2972 fn arg_schema(&self) -> &'static [ArgSchema] {
2973 use std::sync::LazyLock;
2974 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
2975 vec![
2976 ArgSchema::number_lenient_scalar(),
2977 ArgSchema::number_lenient_scalar(),
2978 ]
2979 });
2980 &SCHEMA[..]
2981 }
2982 fn eval<'a, 'b, 'c>(
2983 &self,
2984 args: &'c [ArgumentHandle<'a, 'b>],
2985 _ctx: &dyn FunctionContext<'b>,
2986 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2987 let z = coerce_num(&scalar_like_value(&args[0])?)?;
2988 let cumulative = coerce_num(&scalar_like_value(&args[1])?)? != 0.0;
2989
2990 let result = if cumulative {
2991 std_norm_cdf(z)
2992 } else {
2993 std_norm_pdf(z)
2994 };
2995 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
2996 result,
2997 )))
2998 }
2999}
3000
3001/// Returns the z-score whose standard normal cumulative probability matches `probability`.
3002///
3003/// This is the inverse of `NORM.S.DIST(z, TRUE)` and is commonly used for critical-value
3004/// thresholds.
3005///
3006/// # Remarks
3007/// - `probability` must be strictly between `0` and `1`.
3008/// - Returns `#NUM!` when `probability <= 0` or `probability >= 1`.
3009/// - Output can be negative, zero, or positive depending on which side of `0.5` you query.
3010/// - Invalid numeric coercions propagate as spreadsheet errors.
3011///
3012/// # Examples
3013///
3014/// ```yaml,sandbox
3015/// title: "Median probability maps to zero"
3016/// formula: "=NORM.S.INV(0.5)"
3017/// expected: 0
3018/// ```
3019///
3020/// ```yaml,sandbox
3021/// title: "Upper-tail critical z-score"
3022/// formula: "=NORM.S.INV(0.975)"
3023/// expected: 1.959963986120195
3024/// ```
3025#[derive(Debug)]
3026pub struct NormSInvFn;
3027/// [formualizer-docgen:schema:start]
3028/// Name: NORM.S.INV
3029/// Type: NormSInvFn
3030/// Min args: 1
3031/// Max args: 1
3032/// Variadic: false
3033/// Signature: NORM.S.INV(arg1: number@scalar)
3034/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
3035/// Caps: PURE
3036/// [formualizer-docgen:schema:end]
3037impl Function for NormSInvFn {
3038 func_caps!(PURE);
3039 fn name(&self) -> &'static str {
3040 "NORM.S.INV"
3041 }
3042 fn min_args(&self) -> usize {
3043 1
3044 }
3045 fn arg_schema(&self) -> &'static [ArgSchema] {
3046 use std::sync::LazyLock;
3047 static SCHEMA: LazyLock<Vec<ArgSchema>> =
3048 LazyLock::new(|| vec![ArgSchema::number_lenient_scalar()]);
3049 &SCHEMA[..]
3050 }
3051 fn eval<'a, 'b, 'c>(
3052 &self,
3053 args: &'c [ArgumentHandle<'a, 'b>],
3054 _ctx: &dyn FunctionContext<'b>,
3055 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3056 let p = coerce_num(&scalar_like_value(&args[0])?)?;
3057
3058 match std_norm_inv(p) {
3059 Some(z) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(z))),
3060 None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3061 ExcelError::new_num(),
3062 ))),
3063 }
3064 }
3065}
3066
3067/// Returns the normal-distribution probability at `x` for a given mean and standard deviation.
3068///
3069/// Use `NORM.DIST` for either cumulative probabilities or point density under a non-standard
3070/// normal model.
3071///
3072/// # Remarks
3073/// - Set `cumulative` to non-zero for `P(X <= x)`; set it to `0` for density mode.
3074/// - `standard_dev` must be strictly greater than `0`.
3075/// - Returns `#NUM!` when `standard_dev <= 0`.
3076/// - Invalid numeric coercions propagate as spreadsheet errors.
3077///
3078/// # Examples
3079///
3080/// ```yaml,sandbox
3081/// title: "Normal CDF at the mean"
3082/// formula: "=NORM.DIST(50,50,10,TRUE)"
3083/// expected: 0.5
3084/// ```
3085///
3086/// ```yaml,sandbox
3087/// title: "Normal PDF at the mean"
3088/// formula: "=NORM.DIST(50,50,10,FALSE)"
3089/// expected: 0.03989422804014327
3090/// ```
3091#[derive(Debug)]
3092pub struct NormDistFn;
3093/// [formualizer-docgen:schema:start]
3094/// Name: NORM.DIST
3095/// Type: NormDistFn
3096/// Min args: 4
3097/// Max args: 4
3098/// Variadic: false
3099/// Signature: NORM.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar)
3100/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg4{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
3101/// Caps: PURE
3102/// [formualizer-docgen:schema:end]
3103impl Function for NormDistFn {
3104 func_caps!(PURE);
3105 fn name(&self) -> &'static str {
3106 "NORM.DIST"
3107 }
3108 fn min_args(&self) -> usize {
3109 4
3110 }
3111 fn arg_schema(&self) -> &'static [ArgSchema] {
3112 use std::sync::LazyLock;
3113 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
3114 vec![
3115 ArgSchema::number_lenient_scalar(),
3116 ArgSchema::number_lenient_scalar(),
3117 ArgSchema::number_lenient_scalar(),
3118 ArgSchema::number_lenient_scalar(),
3119 ]
3120 });
3121 &SCHEMA[..]
3122 }
3123 fn eval<'a, 'b, 'c>(
3124 &self,
3125 args: &'c [ArgumentHandle<'a, 'b>],
3126 _ctx: &dyn FunctionContext<'b>,
3127 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3128 let x = coerce_num(&scalar_like_value(&args[0])?)?;
3129 let mean = coerce_num(&scalar_like_value(&args[1])?)?;
3130 let std_dev = coerce_num(&scalar_like_value(&args[2])?)?;
3131 let cumulative = coerce_num(&scalar_like_value(&args[3])?)? != 0.0;
3132
3133 if std_dev <= 0.0 {
3134 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3135 ExcelError::new_num(),
3136 )));
3137 }
3138
3139 let z = (x - mean) / std_dev;
3140
3141 let result = if cumulative {
3142 std_norm_cdf(z)
3143 } else {
3144 std_norm_pdf(z) / std_dev
3145 };
3146 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
3147 result,
3148 )))
3149 }
3150}
3151
3152/// Returns the value `x` whose normal cumulative probability equals `probability`.
3153///
3154/// This function is the inverse of `NORM.DIST(x, mean, standard_dev, TRUE)`.
3155///
3156/// # Remarks
3157/// - `probability` must be strictly between `0` and `1`.
3158/// - `standard_dev` must be strictly greater than `0`.
3159/// - Returns `#NUM!` for invalid probability bounds or non-positive standard deviation.
3160/// - Invalid numeric coercions propagate as spreadsheet errors.
3161///
3162/// # Examples
3163///
3164/// ```yaml,sandbox
3165/// title: "Median probability returns the mean"
3166/// formula: "=NORM.INV(0.5,10,2)"
3167/// expected: 10
3168/// ```
3169///
3170/// ```yaml,sandbox
3171/// title: "One-standard-deviation quantile"
3172/// formula: "=NORM.INV(0.841344746068543,0,1)"
3173/// expected: 1
3174/// ```
3175#[derive(Debug)]
3176pub struct NormInvFn;
3177/// [formualizer-docgen:schema:start]
3178/// Name: NORM.INV
3179/// Type: NormInvFn
3180/// Min args: 3
3181/// Max args: 3
3182/// Variadic: false
3183/// Signature: NORM.INV(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
3184/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
3185/// Caps: PURE
3186/// [formualizer-docgen:schema:end]
3187impl Function for NormInvFn {
3188 func_caps!(PURE);
3189 fn name(&self) -> &'static str {
3190 "NORM.INV"
3191 }
3192 fn min_args(&self) -> usize {
3193 3
3194 }
3195 fn arg_schema(&self) -> &'static [ArgSchema] {
3196 use std::sync::LazyLock;
3197 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
3198 vec![
3199 ArgSchema::number_lenient_scalar(),
3200 ArgSchema::number_lenient_scalar(),
3201 ArgSchema::number_lenient_scalar(),
3202 ]
3203 });
3204 &SCHEMA[..]
3205 }
3206 fn eval<'a, 'b, 'c>(
3207 &self,
3208 args: &'c [ArgumentHandle<'a, 'b>],
3209 _ctx: &dyn FunctionContext<'b>,
3210 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3211 let p = coerce_num(&scalar_like_value(&args[0])?)?;
3212 let mean = coerce_num(&scalar_like_value(&args[1])?)?;
3213 let std_dev = coerce_num(&scalar_like_value(&args[2])?)?;
3214
3215 if std_dev <= 0.0 {
3216 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3217 ExcelError::new_num(),
3218 )));
3219 }
3220
3221 match std_norm_inv(p) {
3222 Some(z) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
3223 mean + z * std_dev,
3224 ))),
3225 None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3226 ExcelError::new_num(),
3227 ))),
3228 }
3229 }
3230}
3231
3232/// Returns the log-normal probability at `x` as either a cumulative value or density.
3233///
3234/// `LOGNORM.DIST` models positive-valued variables where `ln(X)` follows a normal distribution.
3235///
3236/// # Remarks
3237/// - Set `cumulative` to non-zero for CDF mode; set it to `0` for PDF mode.
3238/// - Requires `x > 0` and `standard_dev > 0`.
3239/// - Returns `#NUM!` when `x <= 0` or `standard_dev <= 0`.
3240/// - Invalid numeric coercions propagate as spreadsheet errors.
3241///
3242/// # Examples
3243///
3244/// ```yaml,sandbox
3245/// title: "Log-normal CDF at x = 1"
3246/// formula: "=LOGNORM.DIST(1,0,1,TRUE)"
3247/// expected: 0.5
3248/// ```
3249///
3250/// ```yaml,sandbox
3251/// title: "Log-normal PDF at x = 1"
3252/// formula: "=LOGNORM.DIST(1,0,1,FALSE)"
3253/// expected: 0.3989422804014327
3254/// ```
3255#[derive(Debug)]
3256pub struct LognormDistFn;
3257/// [formualizer-docgen:schema:start]
3258/// Name: LOGNORM.DIST
3259/// Type: LognormDistFn
3260/// Min args: 4
3261/// Max args: 4
3262/// Variadic: false
3263/// Signature: LOGNORM.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar)
3264/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg4{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
3265/// Caps: PURE
3266/// [formualizer-docgen:schema:end]
3267impl Function for LognormDistFn {
3268 func_caps!(PURE);
3269 fn name(&self) -> &'static str {
3270 "LOGNORM.DIST"
3271 }
3272 fn min_args(&self) -> usize {
3273 4
3274 }
3275 fn arg_schema(&self) -> &'static [ArgSchema] {
3276 use std::sync::LazyLock;
3277 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
3278 vec![
3279 ArgSchema::number_lenient_scalar(),
3280 ArgSchema::number_lenient_scalar(),
3281 ArgSchema::number_lenient_scalar(),
3282 ArgSchema::number_lenient_scalar(),
3283 ]
3284 });
3285 &SCHEMA[..]
3286 }
3287 fn eval<'a, 'b, 'c>(
3288 &self,
3289 args: &'c [ArgumentHandle<'a, 'b>],
3290 _ctx: &dyn FunctionContext<'b>,
3291 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3292 let x = coerce_num(&scalar_like_value(&args[0])?)?;
3293 let mean = coerce_num(&scalar_like_value(&args[1])?)?;
3294 let std_dev = coerce_num(&scalar_like_value(&args[2])?)?;
3295 let cumulative = coerce_num(&scalar_like_value(&args[3])?)? != 0.0;
3296
3297 if x <= 0.0 || std_dev <= 0.0 {
3298 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3299 ExcelError::new_num(),
3300 )));
3301 }
3302
3303 let z = (x.ln() - mean) / std_dev;
3304
3305 let result = if cumulative {
3306 std_norm_cdf(z)
3307 } else {
3308 std_norm_pdf(z) / (x * std_dev)
3309 };
3310 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
3311 result,
3312 )))
3313 }
3314}
3315
3316/// Returns the positive value `x` whose log-normal cumulative probability is `probability`.
3317///
3318/// This function inverts `LOGNORM.DIST(x, mean, standard_dev, TRUE)`.
3319///
3320/// # Remarks
3321/// - `probability` must be strictly between `0` and `1`.
3322/// - `standard_dev` must be strictly greater than `0`.
3323/// - Returns `#NUM!` when inputs violate probability or scale constraints.
3324/// - Invalid numeric coercions propagate as spreadsheet errors.
3325///
3326/// # Examples
3327///
3328/// ```yaml,sandbox
3329/// title: "Median log-normal quantile"
3330/// formula: "=LOGNORM.INV(0.5,0,1)"
3331/// expected: 1
3332/// ```
3333///
3334/// ```yaml,sandbox
3335/// title: "Upper quantile for mean 0 and stdev 1"
3336/// formula: "=LOGNORM.INV(0.841344746068543,0,1)"
3337/// expected: 2.718281828459045
3338/// ```
3339#[derive(Debug)]
3340pub struct LognormInvFn;
3341/// [formualizer-docgen:schema:start]
3342/// Name: LOGNORM.INV
3343/// Type: LognormInvFn
3344/// Min args: 3
3345/// Max args: 3
3346/// Variadic: false
3347/// Signature: LOGNORM.INV(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
3348/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
3349/// Caps: PURE
3350/// [formualizer-docgen:schema:end]
3351impl Function for LognormInvFn {
3352 func_caps!(PURE);
3353 fn name(&self) -> &'static str {
3354 "LOGNORM.INV"
3355 }
3356 fn min_args(&self) -> usize {
3357 3
3358 }
3359 fn arg_schema(&self) -> &'static [ArgSchema] {
3360 use std::sync::LazyLock;
3361 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
3362 vec![
3363 ArgSchema::number_lenient_scalar(),
3364 ArgSchema::number_lenient_scalar(),
3365 ArgSchema::number_lenient_scalar(),
3366 ]
3367 });
3368 &SCHEMA[..]
3369 }
3370 fn eval<'a, 'b, 'c>(
3371 &self,
3372 args: &'c [ArgumentHandle<'a, 'b>],
3373 _ctx: &dyn FunctionContext<'b>,
3374 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3375 let p = coerce_num(&scalar_like_value(&args[0])?)?;
3376 let mean = coerce_num(&scalar_like_value(&args[1])?)?;
3377 let std_dev = coerce_num(&scalar_like_value(&args[2])?)?;
3378
3379 if std_dev <= 0.0 {
3380 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3381 ExcelError::new_num(),
3382 )));
3383 }
3384
3385 match std_norm_inv(p) {
3386 Some(z) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
3387 (mean + z * std_dev).exp(),
3388 ))),
3389 None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3390 ExcelError::new_num(),
3391 ))),
3392 }
3393 }
3394}
3395
3396/// Returns the standard normal probability density at `x`.
3397///
3398/// `PHI` is equivalent to `NORM.S.DIST(x, FALSE)` and is useful in continuous-probability
3399/// calculations.
3400///
3401/// # Remarks
3402/// - Evaluates the density of a standard normal variable centered at `0`.
3403/// - The result is always non-negative and symmetric around `x = 0`.
3404/// - Works for any real input value.
3405/// - Invalid numeric coercions propagate as spreadsheet errors.
3406///
3407/// # Examples
3408///
3409/// ```yaml,sandbox
3410/// title: "Standard normal density at zero"
3411/// formula: "=PHI(0)"
3412/// expected: 0.3989422804014327
3413/// ```
3414///
3415/// ```yaml,sandbox
3416/// title: "Standard normal density at one"
3417/// formula: "=PHI(1)"
3418/// expected: 0.24197072451914337
3419/// ```
3420#[derive(Debug)]
3421pub struct PhiFn;
3422/// [formualizer-docgen:schema:start]
3423/// Name: PHI
3424/// Type: PhiFn
3425/// Min args: 1
3426/// Max args: 1
3427/// Variadic: false
3428/// Signature: PHI(arg1: number@scalar)
3429/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
3430/// Caps: PURE
3431/// [formualizer-docgen:schema:end]
3432impl Function for PhiFn {
3433 func_caps!(PURE);
3434 fn name(&self) -> &'static str {
3435 "PHI"
3436 }
3437 fn min_args(&self) -> usize {
3438 1
3439 }
3440 fn arg_schema(&self) -> &'static [ArgSchema] {
3441 use std::sync::LazyLock;
3442 static SCHEMA: LazyLock<Vec<ArgSchema>> =
3443 LazyLock::new(|| vec![ArgSchema::number_lenient_scalar()]);
3444 &SCHEMA[..]
3445 }
3446 fn eval<'a, 'b, 'c>(
3447 &self,
3448 args: &'c [ArgumentHandle<'a, 'b>],
3449 _ctx: &dyn FunctionContext<'b>,
3450 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3451 let z = coerce_num(&scalar_like_value(&args[0])?)?;
3452 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
3453 std_norm_pdf(z),
3454 )))
3455 }
3456}
3457
3458/// Returns the standard normal area between `0` and `z`.
3459///
3460/// `GAUSS` computes `NORM.S.DIST(z, TRUE) - 0.5`, preserving the sign of `z`.
3461///
3462/// # Remarks
3463/// - Positive `z` returns a positive area; negative `z` returns a negative area.
3464/// - `GAUSS(0)` returns `0`.
3465/// - Output magnitude is always less than `0.5`.
3466/// - Invalid numeric coercions propagate as spreadsheet errors.
3467///
3468/// # Examples
3469///
3470/// ```yaml,sandbox
3471/// title: "Area from mean to z = 1"
3472/// formula: "=GAUSS(1)"
3473/// expected: 0.3413447460685429
3474/// ```
3475///
3476/// ```yaml,sandbox
3477/// title: "Symmetric negative z-value"
3478/// formula: "=GAUSS(-1)"
3479/// expected: -0.3413447460685429
3480/// ```
3481#[derive(Debug)]
3482pub struct GaussFn;
3483/// [formualizer-docgen:schema:start]
3484/// Name: GAUSS
3485/// Type: GaussFn
3486/// Min args: 1
3487/// Max args: 1
3488/// Variadic: false
3489/// Signature: GAUSS(arg1: number@scalar)
3490/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
3491/// Caps: PURE
3492/// [formualizer-docgen:schema:end]
3493impl Function for GaussFn {
3494 func_caps!(PURE);
3495 fn name(&self) -> &'static str {
3496 "GAUSS"
3497 }
3498 fn min_args(&self) -> usize {
3499 1
3500 }
3501 fn arg_schema(&self) -> &'static [ArgSchema] {
3502 use std::sync::LazyLock;
3503 static SCHEMA: LazyLock<Vec<ArgSchema>> =
3504 LazyLock::new(|| vec![ArgSchema::number_lenient_scalar()]);
3505 &SCHEMA[..]
3506 }
3507 fn eval<'a, 'b, 'c>(
3508 &self,
3509 args: &'c [ArgumentHandle<'a, 'b>],
3510 _ctx: &dyn FunctionContext<'b>,
3511 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3512 let z = coerce_num(&scalar_like_value(&args[0])?)?;
3513 // GAUSS(z) = Φ(z) - 0.5
3514 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
3515 std_norm_cdf(z) - 0.5,
3516 )))
3517 }
3518}
3519
3520/// Helper: Log-gamma function
3521#[allow(clippy::excessive_precision)]
3522fn ln_gamma(x: f64) -> f64 {
3523 // Lanczos approximation
3524 const G: f64 = 7.0;
3525 const C: [f64; 9] = [
3526 0.99999999999980993,
3527 676.5203681218851,
3528 -1259.1392167224028,
3529 771.32342877765313,
3530 -176.61502916214059,
3531 12.507343278686905,
3532 -0.13857109526572012,
3533 9.9843695780195716e-6,
3534 1.5056327351493116e-7,
3535 ];
3536
3537 if x < 0.5 {
3538 // Reflection formula
3539 let pi = std::f64::consts::PI;
3540 pi.ln() - (pi * x).sin().ln() - ln_gamma(1.0 - x)
3541 } else {
3542 let x = x - 1.0;
3543 let mut ag = C[0];
3544 for (i, c) in C.iter().enumerate().skip(1) {
3545 ag += c / (x + i as f64);
3546 }
3547 let tmp = x + G + 0.5;
3548 0.5 * (2.0 * std::f64::consts::PI).ln() + (tmp).ln() * (x + 0.5) - tmp + ag.ln()
3549 }
3550}
3551
3552/// Helper: Regularized lower incomplete gamma function P(a, x)
3553fn gamma_p(a: f64, x: f64) -> f64 {
3554 if x < 0.0 || a <= 0.0 {
3555 return 0.0;
3556 }
3557 if x == 0.0 {
3558 return 0.0;
3559 }
3560
3561 // Use series expansion for x < a+1
3562 if x < a + 1.0 {
3563 gamma_series(a, x)
3564 } else {
3565 // Use continued fraction for x >= a+1
3566 1.0 - gamma_cf(a, x)
3567 }
3568}
3569
3570/// Helper: Series expansion for incomplete gamma
3571fn gamma_series(a: f64, x: f64) -> f64 {
3572 let ln_ga = ln_gamma(a);
3573 let mut sum = 1.0 / a;
3574 let mut term = sum;
3575 for n in 1..200 {
3576 term *= x / (a + n as f64);
3577 sum += term;
3578 if term.abs() < sum.abs() * 1e-15 {
3579 break;
3580 }
3581 }
3582 sum * (-x + a * x.ln() - ln_ga).exp()
3583}
3584
3585/// Helper: Continued fraction for upper incomplete gamma Q(a,x)
3586/// Using modified Lentz's algorithm (Numerical Recipes formulation)
3587fn gamma_cf(a: f64, x: f64) -> f64 {
3588 let ln_ga = ln_gamma(a);
3589 const TINY: f64 = 1e-30;
3590 const EPS: f64 = 1e-14;
3591
3592 // Set up for evaluating continued fraction by modified Lentz's method
3593 let mut b = x + 1.0 - a;
3594 let mut c = 1.0 / TINY;
3595 let mut d = 1.0 / b;
3596 let mut h = d;
3597
3598 for i in 1..=200 {
3599 let an = -(i as f64) * (i as f64 - a);
3600 b += 2.0;
3601 d = an * d + b;
3602 if d.abs() < TINY {
3603 d = TINY;
3604 }
3605 c = b + an / c;
3606 if c.abs() < TINY {
3607 c = TINY;
3608 }
3609 d = 1.0 / d;
3610 let delta = d * c;
3611 h *= delta;
3612 if (delta - 1.0).abs() <= EPS {
3613 break;
3614 }
3615 }
3616
3617 h * (-x + a * x.ln() - ln_ga).exp()
3618}
3619
3620/// Helper: Regularized incomplete beta function I_x(a,b)
3621/// Uses the continued fraction representation (NIST DLMF 8.17.22)
3622fn beta_i(x: f64, a: f64, b: f64) -> f64 {
3623 if x <= 0.0 {
3624 return 0.0;
3625 }
3626 if x >= 1.0 {
3627 return 1.0;
3628 }
3629 if a <= 0.0 || b <= 0.0 {
3630 return f64::NAN;
3631 }
3632
3633 // Use symmetry for better convergence: I_x(a,b) = 1 - I_{1-x}(b,a)
3634 if x > (a + 1.0) / (a + b + 2.0) {
3635 return 1.0 - beta_i(1.0 - x, b, a);
3636 }
3637
3638 // Compute the prefactor: x^a * (1-x)^b / (a * B(a,b))
3639 let ln_beta = ln_gamma(a) + ln_gamma(b) - ln_gamma(a + b);
3640 let ln_prefactor = a * x.ln() + b * (1.0 - x).ln() - ln_beta - a.ln();
3641 let prefactor = ln_prefactor.exp();
3642
3643 // Evaluate the continued fraction using modified Lentz algorithm
3644 // The CF is: 1 / (1 + d1/(1 + d2/(1 + ...)))
3645 // where d_{2m+1} = -(a+m)(a+b+m)x / ((a+2m)(a+2m+1))
3646 // d_{2m} = m(b-m)x / ((a+2m-1)(a+2m))
3647 const EPS: f64 = 1e-14;
3648 const TINY: f64 = 1e-30;
3649
3650 let qab = a + b;
3651 let qap = a + 1.0;
3652 let qam = a - 1.0;
3653 let mut c = 1.0;
3654 let mut d = 1.0 - qab * x / qap;
3655 if d.abs() < TINY {
3656 d = TINY;
3657 }
3658 d = 1.0 / d;
3659 let mut h = d;
3660
3661 for m in 1..=200 {
3662 let m_f64 = m as f64;
3663 let m2 = 2.0 * m_f64;
3664
3665 // Even step: d_{2m} = m(b-m)x / ((a+2m-1)(a+2m))
3666 let aa = m_f64 * (b - m_f64) * x / ((qam + m2) * (a + m2));
3667 d = 1.0 + aa * d;
3668 if d.abs() < TINY {
3669 d = TINY;
3670 }
3671 c = 1.0 + aa / c;
3672 if c.abs() < TINY {
3673 c = TINY;
3674 }
3675 d = 1.0 / d;
3676 h *= d * c;
3677
3678 // Odd step: d_{2m+1} = -(a+m)(a+b+m)x / ((a+2m)(a+2m+1))
3679 let aa = -((a + m_f64) * (qab + m_f64) * x) / ((a + m2) * (qap + m2));
3680 d = 1.0 + aa * d;
3681 if d.abs() < TINY {
3682 d = TINY;
3683 }
3684 c = 1.0 + aa / c;
3685 if c.abs() < TINY {
3686 c = TINY;
3687 }
3688 d = 1.0 / d;
3689 let delta = d * c;
3690 h *= delta;
3691
3692 if (delta - 1.0).abs() <= EPS {
3693 break;
3694 }
3695 }
3696
3697 prefactor * h
3698}
3699
3700/// Helper: T distribution CDF
3701fn t_cdf(t: f64, df: f64) -> f64 {
3702 let x = df / (df + t * t);
3703 0.5 * (1.0 + t.signum() * (1.0 - beta_i(x, df / 2.0, 0.5)))
3704}
3705
3706/// Helper: T distribution inverse CDF using Newton-Raphson
3707fn t_inv(p: f64, df: f64) -> Option<f64> {
3708 if p <= 0.0 || p >= 1.0 {
3709 return None;
3710 }
3711
3712 // Initial guess using normal approximation
3713 let mut t = std_norm_inv(p)?;
3714
3715 // Newton-Raphson iteration
3716 for _ in 0..50 {
3717 let cdf = t_cdf(t, df);
3718 let pdf = t_pdf(t, df);
3719 if pdf.abs() < 1e-30 {
3720 break;
3721 }
3722 let delta = (cdf - p) / pdf;
3723 t -= delta;
3724 if delta.abs() < 1e-12 {
3725 break;
3726 }
3727 }
3728
3729 Some(t)
3730}
3731
3732/// Helper: T distribution PDF
3733fn t_pdf(t: f64, df: f64) -> f64 {
3734 let coef =
3735 (ln_gamma((df + 1.0) / 2.0) - ln_gamma(df / 2.0) - 0.5 * (df * std::f64::consts::PI).ln())
3736 .exp();
3737 coef * (1.0 + t * t / df).powf(-(df + 1.0) / 2.0)
3738}
3739
3740/// Helper: Chi-square CDF
3741fn chisq_cdf(x: f64, df: f64) -> f64 {
3742 if x <= 0.0 {
3743 return 0.0;
3744 }
3745 gamma_p(df / 2.0, x / 2.0)
3746}
3747
3748/// Helper: Chi-square inverse CDF using Newton-Raphson
3749fn chisq_inv(p: f64, df: f64) -> Option<f64> {
3750 if p <= 0.0 || p >= 1.0 {
3751 return None;
3752 }
3753
3754 // Initial guess
3755 let mut x = df.max(1.0);
3756 if p < 0.5 {
3757 x = x.min(1.0);
3758 }
3759
3760 // Newton-Raphson iteration
3761 for _ in 0..100 {
3762 let cdf = chisq_cdf(x, df);
3763 let pdf = chisq_pdf(x, df);
3764 if pdf.abs() < 1e-30 {
3765 break;
3766 }
3767 let delta = (cdf - p) / pdf;
3768 let new_x = (x - delta).max(1e-15);
3769 if (new_x - x).abs() < 1e-12 * x {
3770 x = new_x;
3771 break;
3772 }
3773 x = new_x;
3774 }
3775
3776 Some(x)
3777}
3778
3779/// Helper: Chi-square PDF
3780fn chisq_pdf(x: f64, df: f64) -> f64 {
3781 if x <= 0.0 {
3782 return 0.0;
3783 }
3784 let k = df / 2.0;
3785 ((k - 1.0) * x.ln() - x / 2.0 - k * 2.0_f64.ln() - ln_gamma(k)).exp()
3786}
3787
3788/// Helper: F distribution CDF
3789fn f_cdf(f: f64, d1: f64, d2: f64) -> f64 {
3790 if f <= 0.0 {
3791 return 0.0;
3792 }
3793 let x = d1 * f / (d1 * f + d2);
3794 beta_i(x, d1 / 2.0, d2 / 2.0)
3795}
3796
3797/// Helper: F distribution inverse CDF using Newton-Raphson
3798fn f_inv(p: f64, d1: f64, d2: f64) -> Option<f64> {
3799 if p <= 0.0 || p >= 1.0 {
3800 return None;
3801 }
3802
3803 // Initial guess
3804 let mut f = 1.0;
3805
3806 // Newton-Raphson iteration
3807 for _ in 0..100 {
3808 let cdf = f_cdf(f, d1, d2);
3809 let pdf = f_pdf(f, d1, d2);
3810 if pdf.abs() < 1e-30 {
3811 break;
3812 }
3813 let delta = (cdf - p) / pdf;
3814 let new_f = (f - delta).max(1e-15);
3815 if (new_f - f).abs() < 1e-12 * f {
3816 f = new_f;
3817 break;
3818 }
3819 f = new_f;
3820 }
3821
3822 Some(f)
3823}
3824
3825/// Helper: F distribution PDF
3826fn f_pdf(f: f64, d1: f64, d2: f64) -> f64 {
3827 if f <= 0.0 {
3828 return 0.0;
3829 }
3830 let ln_beta = ln_gamma(d1 / 2.0) + ln_gamma(d2 / 2.0) - ln_gamma((d1 + d2) / 2.0);
3831 let coef = (d1 / 2.0) * (d1 / d2).ln() + (d1 / 2.0 - 1.0) * f.ln()
3832 - ((d1 + d2) / 2.0) * (1.0 + d1 * f / d2).ln()
3833 - ln_beta;
3834 coef.exp()
3835}
3836
3837/// Returns the Student's t probability for `x` and a given degrees-of-freedom value.
3838///
3839/// Use `T.DIST` in either cumulative mode (left-tail probability) or density mode.
3840///
3841/// # Remarks
3842/// - Set `cumulative` to non-zero for CDF mode, or `0` for PDF mode.
3843/// - `deg_freedom` must be at least `1`.
3844/// - Returns `#NUM!` when `deg_freedom < 1`.
3845/// - Invalid numeric coercions propagate as spreadsheet errors.
3846///
3847/// # Examples
3848///
3849/// ```yaml,sandbox
3850/// title: "t CDF at zero"
3851/// formula: "=T.DIST(0,10,TRUE)"
3852/// expected: 0.5
3853/// ```
3854///
3855/// ```yaml,sandbox
3856/// title: "t PDF at zero"
3857/// formula: "=T.DIST(0,10,FALSE)"
3858/// expected: 0.389108383966031
3859/// ```
3860#[derive(Debug)]
3861pub struct TDistFn;
3862/// [formualizer-docgen:schema:start]
3863/// Name: T.DIST
3864/// Type: TDistFn
3865/// Min args: 3
3866/// Max args: 3
3867/// Variadic: false
3868/// Signature: T.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
3869/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
3870/// Caps: PURE
3871/// [formualizer-docgen:schema:end]
3872impl Function for TDistFn {
3873 func_caps!(PURE);
3874 fn name(&self) -> &'static str {
3875 "T.DIST"
3876 }
3877 fn min_args(&self) -> usize {
3878 3
3879 }
3880 fn arg_schema(&self) -> &'static [ArgSchema] {
3881 use std::sync::LazyLock;
3882 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
3883 vec![
3884 ArgSchema::number_lenient_scalar(),
3885 ArgSchema::number_lenient_scalar(),
3886 ArgSchema::number_lenient_scalar(),
3887 ]
3888 });
3889 &SCHEMA[..]
3890 }
3891 fn eval<'a, 'b, 'c>(
3892 &self,
3893 args: &'c [ArgumentHandle<'a, 'b>],
3894 _ctx: &dyn FunctionContext<'b>,
3895 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3896 let x = coerce_num(&scalar_like_value(&args[0])?)?;
3897 let df = coerce_num(&scalar_like_value(&args[1])?)?;
3898 let cumulative = coerce_num(&scalar_like_value(&args[2])?)? != 0.0;
3899
3900 if df < 1.0 {
3901 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3902 ExcelError::new_num(),
3903 )));
3904 }
3905
3906 let result = if cumulative {
3907 t_cdf(x, df)
3908 } else {
3909 t_pdf(x, df)
3910 };
3911 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
3912 result,
3913 )))
3914 }
3915}
3916
3917/// Returns the t-value whose left-tail probability equals `probability`.
3918///
3919/// `T.INV` is the inverse of `T.DIST(x, deg_freedom, TRUE)`.
3920///
3921/// # Remarks
3922/// - `probability` must be strictly between `0` and `1`.
3923/// - `deg_freedom` must be at least `1`.
3924/// - Returns `#NUM!` for out-of-range probability or invalid degrees of freedom.
3925/// - Invalid numeric coercions propagate as spreadsheet errors.
3926///
3927/// # Examples
3928///
3929/// ```yaml,sandbox
3930/// title: "Median t quantile"
3931/// formula: "=T.INV(0.5,10)"
3932/// expected: 0
3933/// ```
3934///
3935/// ```yaml,sandbox
3936/// title: "Upper-tail critical value"
3937/// formula: "=T.INV(0.975,10)"
3938/// expected: 2.228138851986273
3939/// ```
3940#[derive(Debug)]
3941pub struct TInvFn;
3942/// [formualizer-docgen:schema:start]
3943/// Name: T.INV
3944/// Type: TInvFn
3945/// Min args: 2
3946/// Max args: 2
3947/// Variadic: false
3948/// Signature: T.INV(arg1: number@scalar, arg2: number@scalar)
3949/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
3950/// Caps: PURE
3951/// [formualizer-docgen:schema:end]
3952impl Function for TInvFn {
3953 func_caps!(PURE);
3954 fn name(&self) -> &'static str {
3955 "T.INV"
3956 }
3957 fn min_args(&self) -> usize {
3958 2
3959 }
3960 fn arg_schema(&self) -> &'static [ArgSchema] {
3961 use std::sync::LazyLock;
3962 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
3963 vec![
3964 ArgSchema::number_lenient_scalar(),
3965 ArgSchema::number_lenient_scalar(),
3966 ]
3967 });
3968 &SCHEMA[..]
3969 }
3970 fn eval<'a, 'b, 'c>(
3971 &self,
3972 args: &'c [ArgumentHandle<'a, 'b>],
3973 _ctx: &dyn FunctionContext<'b>,
3974 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3975 let p = coerce_num(&scalar_like_value(&args[0])?)?;
3976 let df = coerce_num(&scalar_like_value(&args[1])?)?;
3977
3978 if df < 1.0 {
3979 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3980 ExcelError::new_num(),
3981 )));
3982 }
3983
3984 match t_inv(p, df) {
3985 Some(result) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
3986 result,
3987 ))),
3988 None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3989 ExcelError::new_num(),
3990 ))),
3991 }
3992 }
3993}
3994
3995/// Returns the chi-square probability for `x` with the specified degrees of freedom.
3996///
3997/// Use `CHISQ.DIST` in cumulative mode for left-tail probability or density mode for the PDF.
3998///
3999/// # Remarks
4000/// - Set `cumulative` to non-zero for CDF mode, or `0` for PDF mode.
4001/// - Requires `x >= 0` and `deg_freedom >= 1`.
4002/// - Returns `#NUM!` for negative `x` or invalid degrees of freedom.
4003/// - Invalid numeric coercions propagate as spreadsheet errors.
4004///
4005/// # Examples
4006///
4007/// ```yaml,sandbox
4008/// title: "Chi-square CDF at zero"
4009/// formula: "=CHISQ.DIST(0,4,TRUE)"
4010/// expected: 0
4011/// ```
4012///
4013/// ```yaml,sandbox
4014/// title: "Chi-square PDF example"
4015/// formula: "=CHISQ.DIST(2,2,FALSE)"
4016/// expected: 0.18393972058572117
4017/// ```
4018#[derive(Debug)]
4019pub struct ChisqDistFn;
4020/// [formualizer-docgen:schema:start]
4021/// Name: CHISQ.DIST
4022/// Type: ChisqDistFn
4023/// Min args: 3
4024/// Max args: 3
4025/// Variadic: false
4026/// Signature: CHISQ.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
4027/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
4028/// Caps: PURE
4029/// [formualizer-docgen:schema:end]
4030impl Function for ChisqDistFn {
4031 func_caps!(PURE);
4032 fn name(&self) -> &'static str {
4033 "CHISQ.DIST"
4034 }
4035 fn min_args(&self) -> usize {
4036 3
4037 }
4038 fn arg_schema(&self) -> &'static [ArgSchema] {
4039 use std::sync::LazyLock;
4040 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
4041 vec![
4042 ArgSchema::number_lenient_scalar(),
4043 ArgSchema::number_lenient_scalar(),
4044 ArgSchema::number_lenient_scalar(),
4045 ]
4046 });
4047 &SCHEMA[..]
4048 }
4049 fn eval<'a, 'b, 'c>(
4050 &self,
4051 args: &'c [ArgumentHandle<'a, 'b>],
4052 _ctx: &dyn FunctionContext<'b>,
4053 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4054 let x = coerce_num(&scalar_like_value(&args[0])?)?;
4055 let df = coerce_num(&scalar_like_value(&args[1])?)?;
4056 let cumulative = coerce_num(&scalar_like_value(&args[2])?)? != 0.0;
4057
4058 if df < 1.0 || x < 0.0 {
4059 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4060 ExcelError::new_num(),
4061 )));
4062 }
4063
4064 let result = if cumulative {
4065 chisq_cdf(x, df)
4066 } else {
4067 chisq_pdf(x, df)
4068 };
4069 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
4070 result,
4071 )))
4072 }
4073}
4074
4075/// Returns the chi-square value whose left-tail probability is `probability`.
4076///
4077/// `CHISQ.INV` inverts `CHISQ.DIST(x, deg_freedom, TRUE)`.
4078///
4079/// # Remarks
4080/// - `probability` must be strictly between `0` and `1`.
4081/// - `deg_freedom` must be at least `1`.
4082/// - Returns `#NUM!` when arguments are outside valid ranges.
4083/// - Invalid numeric coercions propagate as spreadsheet errors.
4084///
4085/// # Examples
4086///
4087/// ```yaml,sandbox
4088/// title: "Median chi-square quantile for df=2"
4089/// formula: "=CHISQ.INV(0.5,2)"
4090/// expected: 1.3862943611198906
4091/// ```
4092///
4093/// ```yaml,sandbox
4094/// title: "Upper quantile for df=10"
4095/// formula: "=CHISQ.INV(0.95,10)"
4096/// expected: 18.307038053275146
4097/// ```
4098#[derive(Debug)]
4099pub struct ChisqInvFn;
4100/// [formualizer-docgen:schema:start]
4101/// Name: CHISQ.INV
4102/// Type: ChisqInvFn
4103/// Min args: 2
4104/// Max args: 2
4105/// Variadic: false
4106/// Signature: CHISQ.INV(arg1: number@scalar, arg2: number@scalar)
4107/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
4108/// Caps: PURE
4109/// [formualizer-docgen:schema:end]
4110impl Function for ChisqInvFn {
4111 func_caps!(PURE);
4112 fn name(&self) -> &'static str {
4113 "CHISQ.INV"
4114 }
4115 fn min_args(&self) -> usize {
4116 2
4117 }
4118 fn arg_schema(&self) -> &'static [ArgSchema] {
4119 use std::sync::LazyLock;
4120 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
4121 vec![
4122 ArgSchema::number_lenient_scalar(),
4123 ArgSchema::number_lenient_scalar(),
4124 ]
4125 });
4126 &SCHEMA[..]
4127 }
4128 fn eval<'a, 'b, 'c>(
4129 &self,
4130 args: &'c [ArgumentHandle<'a, 'b>],
4131 _ctx: &dyn FunctionContext<'b>,
4132 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4133 let p = coerce_num(&scalar_like_value(&args[0])?)?;
4134 let df = coerce_num(&scalar_like_value(&args[1])?)?;
4135
4136 if df < 1.0 {
4137 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4138 ExcelError::new_num(),
4139 )));
4140 }
4141
4142 match chisq_inv(p, df) {
4143 Some(result) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
4144 result,
4145 ))),
4146 None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4147 ExcelError::new_num(),
4148 ))),
4149 }
4150 }
4151}
4152
4153/// Returns the F-distribution probability for `x` with numerator and denominator degrees of freedom.
4154///
4155/// Use `F.DIST` for left-tail cumulative probabilities or density values in variance-ratio tests.
4156///
4157/// # Remarks
4158/// - Set `cumulative` to non-zero for CDF mode, or `0` for PDF mode.
4159/// - Requires `x >= 0`, `deg_freedom1 >= 1`, and `deg_freedom2 >= 1`.
4160/// - Returns `#NUM!` when any domain constraint is violated.
4161/// - Invalid numeric coercions propagate as spreadsheet errors.
4162///
4163/// # Examples
4164///
4165/// ```yaml,sandbox
4166/// title: "F CDF with symmetric 2 and 2 degrees of freedom"
4167/// formula: "=F.DIST(1,2,2,TRUE)"
4168/// expected: 0.5
4169/// ```
4170///
4171/// ```yaml,sandbox
4172/// title: "F PDF with symmetric 2 and 2 degrees of freedom"
4173/// formula: "=F.DIST(1,2,2,FALSE)"
4174/// expected: 0.25
4175/// ```
4176#[derive(Debug)]
4177pub struct FDistFn;
4178/// [formualizer-docgen:schema:start]
4179/// Name: F.DIST
4180/// Type: FDistFn
4181/// Min args: 4
4182/// Max args: 4
4183/// Variadic: false
4184/// Signature: F.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar)
4185/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg4{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
4186/// Caps: PURE
4187/// [formualizer-docgen:schema:end]
4188impl Function for FDistFn {
4189 func_caps!(PURE);
4190 fn name(&self) -> &'static str {
4191 "F.DIST"
4192 }
4193 fn min_args(&self) -> usize {
4194 4
4195 }
4196 fn arg_schema(&self) -> &'static [ArgSchema] {
4197 use std::sync::LazyLock;
4198 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
4199 vec![
4200 ArgSchema::number_lenient_scalar(),
4201 ArgSchema::number_lenient_scalar(),
4202 ArgSchema::number_lenient_scalar(),
4203 ArgSchema::number_lenient_scalar(),
4204 ]
4205 });
4206 &SCHEMA[..]
4207 }
4208 fn eval<'a, 'b, 'c>(
4209 &self,
4210 args: &'c [ArgumentHandle<'a, 'b>],
4211 _ctx: &dyn FunctionContext<'b>,
4212 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4213 let x = coerce_num(&scalar_like_value(&args[0])?)?;
4214 let d1 = coerce_num(&scalar_like_value(&args[1])?)?;
4215 let d2 = coerce_num(&scalar_like_value(&args[2])?)?;
4216 let cumulative = coerce_num(&scalar_like_value(&args[3])?)? != 0.0;
4217
4218 if d1 < 1.0 || d2 < 1.0 || x < 0.0 {
4219 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4220 ExcelError::new_num(),
4221 )));
4222 }
4223
4224 let result = if cumulative {
4225 f_cdf(x, d1, d2)
4226 } else {
4227 f_pdf(x, d1, d2)
4228 };
4229 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
4230 result,
4231 )))
4232 }
4233}
4234
4235/// Returns the F value whose left-tail probability equals `probability`.
4236///
4237/// `F.INV` inverts `F.DIST(x, deg_freedom1, deg_freedom2, TRUE)`.
4238///
4239/// # Remarks
4240/// - `probability` must be strictly between `0` and `1`.
4241/// - `deg_freedom1` and `deg_freedom2` must each be at least `1`.
4242/// - Returns `#NUM!` for invalid probability or degree-of-freedom arguments.
4243/// - Invalid numeric coercions propagate as spreadsheet errors.
4244///
4245/// # Examples
4246///
4247/// ```yaml,sandbox
4248/// title: "Median F quantile with symmetric 2 and 2 degrees of freedom"
4249/// formula: "=F.INV(0.5,2,2)"
4250/// expected: 1
4251/// ```
4252///
4253/// ```yaml,sandbox
4254/// title: "Upper-tail F critical value"
4255/// formula: "=F.INV(0.95,5,10)"
4256/// expected: 3.3258345304130112
4257/// ```
4258#[derive(Debug)]
4259pub struct FInvFn;
4260/// [formualizer-docgen:schema:start]
4261/// Name: F.INV
4262/// Type: FInvFn
4263/// Min args: 3
4264/// Max args: 3
4265/// Variadic: false
4266/// Signature: F.INV(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
4267/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
4268/// Caps: PURE
4269/// [formualizer-docgen:schema:end]
4270impl Function for FInvFn {
4271 func_caps!(PURE);
4272 fn name(&self) -> &'static str {
4273 "F.INV"
4274 }
4275 fn min_args(&self) -> usize {
4276 3
4277 }
4278 fn arg_schema(&self) -> &'static [ArgSchema] {
4279 use std::sync::LazyLock;
4280 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
4281 vec![
4282 ArgSchema::number_lenient_scalar(),
4283 ArgSchema::number_lenient_scalar(),
4284 ArgSchema::number_lenient_scalar(),
4285 ]
4286 });
4287 &SCHEMA[..]
4288 }
4289 fn eval<'a, 'b, 'c>(
4290 &self,
4291 args: &'c [ArgumentHandle<'a, 'b>],
4292 _ctx: &dyn FunctionContext<'b>,
4293 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4294 let p = coerce_num(&scalar_like_value(&args[0])?)?;
4295 let d1 = coerce_num(&scalar_like_value(&args[1])?)?;
4296 let d2 = coerce_num(&scalar_like_value(&args[2])?)?;
4297
4298 if d1 < 1.0 || d2 < 1.0 {
4299 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4300 ExcelError::new_num(),
4301 )));
4302 }
4303
4304 match f_inv(p, d1, d2) {
4305 Some(result) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
4306 result,
4307 ))),
4308 None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4309 ExcelError::new_num(),
4310 ))),
4311 }
4312 }
4313}
4314
4315/// Returns the z-score of `x` relative to a mean and standard deviation.
4316///
4317/// `STANDARDIZE` computes `(x - mean) / standard_dev`.
4318///
4319/// # Remarks
4320/// - `standard_dev` must be strictly greater than `0`.
4321/// - Returns `#NUM!` when `standard_dev <= 0`.
4322/// - Positive output means `x` is above the mean; negative output means below.
4323/// - Invalid numeric coercions propagate as spreadsheet errors.
4324///
4325/// # Examples
4326///
4327/// ```yaml,sandbox
4328/// title: "One standard deviation above the mean"
4329/// formula: "=STANDARDIZE(42,40,2)"
4330/// expected: 1
4331/// ```
4332///
4333/// ```yaml,sandbox
4334/// title: "Exactly at the mean"
4335/// formula: "=STANDARDIZE(100,100,10)"
4336/// expected: 0
4337/// ```
4338#[derive(Debug)]
4339pub struct StandardizeFn;
4340/// [formualizer-docgen:schema:start]
4341/// Name: STANDARDIZE
4342/// Type: StandardizeFn
4343/// Min args: 3
4344/// Max args: 3
4345/// Variadic: false
4346/// Signature: STANDARDIZE(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
4347/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
4348/// Caps: PURE
4349/// [formualizer-docgen:schema:end]
4350impl Function for StandardizeFn {
4351 func_caps!(PURE);
4352 fn name(&self) -> &'static str {
4353 "STANDARDIZE"
4354 }
4355 fn min_args(&self) -> usize {
4356 3
4357 }
4358 fn arg_schema(&self) -> &'static [ArgSchema] {
4359 use std::sync::LazyLock;
4360 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
4361 vec![
4362 ArgSchema::number_lenient_scalar(),
4363 ArgSchema::number_lenient_scalar(),
4364 ArgSchema::number_lenient_scalar(),
4365 ]
4366 });
4367 &SCHEMA[..]
4368 }
4369 fn eval<'a, 'b, 'c>(
4370 &self,
4371 args: &'c [ArgumentHandle<'a, 'b>],
4372 _ctx: &dyn FunctionContext<'b>,
4373 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4374 let x = coerce_num(&scalar_like_value(&args[0])?)?;
4375 let mean = coerce_num(&scalar_like_value(&args[1])?)?;
4376 let std_dev = coerce_num(&scalar_like_value(&args[2])?)?;
4377
4378 if std_dev <= 0.0 {
4379 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4380 ExcelError::new_num(),
4381 )));
4382 }
4383
4384 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
4385 (x - mean) / std_dev,
4386 )))
4387 }
4388}
4389
4390/// Helper: Factorial function
4391fn factorial(n: i64) -> f64 {
4392 if n < 0 {
4393 return f64::NAN;
4394 }
4395 if n <= 1 {
4396 return 1.0;
4397 }
4398 // For large n, use gamma function: n! = Gamma(n+1)
4399 if n > 20 {
4400 return ln_gamma((n + 1) as f64).exp();
4401 }
4402 let mut result = 1.0;
4403 for i in 2..=n {
4404 result *= i as f64;
4405 }
4406 result
4407}
4408
4409/// Helper: Log of binomial coefficient (n choose k)
4410fn ln_binom(n: i64, k: i64) -> f64 {
4411 if k < 0 || k > n {
4412 return f64::NEG_INFINITY;
4413 }
4414 if k == 0 || k == n {
4415 return 0.0;
4416 }
4417 ln_gamma((n + 1) as f64) - ln_gamma((k + 1) as f64) - ln_gamma((n - k + 1) as f64)
4418}
4419
4420/// Returns the binomial probability for a count of successes across independent trials.
4421///
4422/// Use `BINOM.DIST` to evaluate either exact-success probability (PMF) or cumulative probability
4423/// up to a success count (CDF).
4424///
4425/// # Remarks
4426/// - `number_s` and `trials` are truncated to integers.
4427/// - Requires `0 <= number_s <= trials`, `trials >= 0`, and `0 <= probability_s <= 1`.
4428/// - Set `cumulative` to non-zero for CDF mode, or `0` for PMF mode.
4429/// - Returns `#NUM!` for invalid count or probability ranges.
4430///
4431/// # Examples
4432///
4433/// ```yaml,sandbox
4434/// title: "Binomial PMF for exactly 3 successes"
4435/// formula: "=BINOM.DIST(3,10,0.5,FALSE)"
4436/// expected: 0.1171875
4437/// ```
4438///
4439/// ```yaml,sandbox
4440/// title: "Binomial CDF for at most 3 successes"
4441/// formula: "=BINOM.DIST(3,10,0.5,TRUE)"
4442/// expected: 0.171875
4443/// ```
4444#[derive(Debug)]
4445pub struct BinomDistFn;
4446/// [formualizer-docgen:schema:start]
4447/// Name: BINOM.DIST
4448/// Type: BinomDistFn
4449/// Min args: 4
4450/// Max args: 4
4451/// Variadic: false
4452/// Signature: BINOM.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar)
4453/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg4{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
4454/// Caps: PURE
4455/// [formualizer-docgen:schema:end]
4456impl Function for BinomDistFn {
4457 func_caps!(PURE);
4458 fn name(&self) -> &'static str {
4459 "BINOM.DIST"
4460 }
4461 fn min_args(&self) -> usize {
4462 4
4463 }
4464 fn arg_schema(&self) -> &'static [ArgSchema] {
4465 use std::sync::LazyLock;
4466 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
4467 vec![
4468 ArgSchema::number_lenient_scalar(),
4469 ArgSchema::number_lenient_scalar(),
4470 ArgSchema::number_lenient_scalar(),
4471 ArgSchema::number_lenient_scalar(),
4472 ]
4473 });
4474 &SCHEMA[..]
4475 }
4476 fn eval<'a, 'b, 'c>(
4477 &self,
4478 args: &'c [ArgumentHandle<'a, 'b>],
4479 _ctx: &dyn FunctionContext<'b>,
4480 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4481 let k = coerce_num(&scalar_like_value(&args[0])?)?.trunc() as i64;
4482 let n = coerce_num(&scalar_like_value(&args[1])?)?.trunc() as i64;
4483 let p = coerce_num(&scalar_like_value(&args[2])?)?;
4484 let cumulative = coerce_num(&scalar_like_value(&args[3])?)? != 0.0;
4485
4486 if n < 0 || k < 0 || k > n || !(0.0..=1.0).contains(&p) {
4487 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4488 ExcelError::new_num(),
4489 )));
4490 }
4491
4492 let result = if cumulative {
4493 // CDF: sum from i=0 to k of P(X=i)
4494 let mut sum = 0.0;
4495 for i in 0..=k {
4496 let ln_prob =
4497 ln_binom(n, i) + (i as f64) * p.ln() + ((n - i) as f64) * (1.0 - p).ln();
4498 sum += ln_prob.exp();
4499 }
4500 sum
4501 } else {
4502 // PMF: P(X=k)
4503 let ln_prob = ln_binom(n, k) + (k as f64) * p.ln() + ((n - k) as f64) * (1.0 - p).ln();
4504 ln_prob.exp()
4505 };
4506
4507 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
4508 result,
4509 )))
4510 }
4511}
4512
4513/// Returns the Poisson probability for event count `x` at average rate `mean`.
4514///
4515/// `POISSON.DIST` supports exact-count mode (PMF) and cumulative mode (CDF).
4516///
4517/// # Remarks
4518/// - `x` is truncated to an integer and must be at least `0`.
4519/// - `mean` must be non-negative.
4520/// - Set `cumulative` to non-zero for CDF mode, or `0` for PMF mode.
4521/// - Returns `#NUM!` for negative counts or negative mean values.
4522///
4523/// # Examples
4524///
4525/// ```yaml,sandbox
4526/// title: "Poisson PMF for zero events"
4527/// formula: "=POISSON.DIST(0,2,FALSE)"
4528/// expected: 0.1353352832366127
4529/// ```
4530///
4531/// ```yaml,sandbox
4532/// title: "Poisson CDF up to two events"
4533/// formula: "=POISSON.DIST(2,2,TRUE)"
4534/// expected: 0.6766764161830634
4535/// ```
4536#[derive(Debug)]
4537pub struct PoissonDistFn;
4538/// [formualizer-docgen:schema:start]
4539/// Name: POISSON.DIST
4540/// Type: PoissonDistFn
4541/// Min args: 3
4542/// Max args: 3
4543/// Variadic: false
4544/// Signature: POISSON.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
4545/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
4546/// Caps: PURE
4547/// [formualizer-docgen:schema:end]
4548impl Function for PoissonDistFn {
4549 func_caps!(PURE);
4550 fn name(&self) -> &'static str {
4551 "POISSON.DIST"
4552 }
4553 fn min_args(&self) -> usize {
4554 3
4555 }
4556 fn arg_schema(&self) -> &'static [ArgSchema] {
4557 use std::sync::LazyLock;
4558 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
4559 vec![
4560 ArgSchema::number_lenient_scalar(),
4561 ArgSchema::number_lenient_scalar(),
4562 ArgSchema::number_lenient_scalar(),
4563 ]
4564 });
4565 &SCHEMA[..]
4566 }
4567 fn eval<'a, 'b, 'c>(
4568 &self,
4569 args: &'c [ArgumentHandle<'a, 'b>],
4570 _ctx: &dyn FunctionContext<'b>,
4571 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4572 let k = coerce_num(&scalar_like_value(&args[0])?)?.trunc() as i64;
4573 let lambda = coerce_num(&scalar_like_value(&args[1])?)?;
4574 let cumulative = coerce_num(&scalar_like_value(&args[2])?)? != 0.0;
4575
4576 if k < 0 || lambda < 0.0 {
4577 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4578 ExcelError::new_num(),
4579 )));
4580 }
4581
4582 let result = if cumulative {
4583 // CDF: sum from i=0 to k of P(X=i) = 1 - Q(k+1, lambda)
4584 // Using the regularized incomplete gamma function
4585 1.0 - gamma_p((k + 1) as f64, lambda)
4586 } else {
4587 // PMF: P(X=k) = lambda^k * e^(-lambda) / k!
4588 // Use log to avoid overflow
4589 let ln_prob = (k as f64) * lambda.ln() - lambda - ln_gamma((k + 1) as f64);
4590 ln_prob.exp()
4591 };
4592
4593 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
4594 result,
4595 )))
4596 }
4597}
4598
4599/// Returns the exponential-distribution probability at `x` for rate `lambda`.
4600///
4601/// Use `EXPON.DIST` for waiting-time models where events occur with a constant hazard rate.
4602///
4603/// # Remarks
4604/// - Requires `x >= 0` and `lambda > 0`.
4605/// - Set `cumulative` to non-zero for CDF mode, or `0` for PDF mode.
4606/// - Returns `#NUM!` when inputs violate domain requirements.
4607/// - Invalid numeric coercions propagate as spreadsheet errors.
4608///
4609/// # Examples
4610///
4611/// ```yaml,sandbox
4612/// title: "Exponential CDF"
4613/// formula: "=EXPON.DIST(1,1,TRUE)"
4614/// expected: 0.6321205588285577
4615/// ```
4616///
4617/// ```yaml,sandbox
4618/// title: "Exponential PDF"
4619/// formula: "=EXPON.DIST(1,1,FALSE)"
4620/// expected: 0.36787944117144233
4621/// ```
4622#[derive(Debug)]
4623pub struct ExponDistFn;
4624/// [formualizer-docgen:schema:start]
4625/// Name: EXPON.DIST
4626/// Type: ExponDistFn
4627/// Min args: 3
4628/// Max args: 3
4629/// Variadic: false
4630/// Signature: EXPON.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
4631/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
4632/// Caps: PURE
4633/// [formualizer-docgen:schema:end]
4634impl Function for ExponDistFn {
4635 func_caps!(PURE);
4636 fn name(&self) -> &'static str {
4637 "EXPON.DIST"
4638 }
4639 fn min_args(&self) -> usize {
4640 3
4641 }
4642 fn arg_schema(&self) -> &'static [ArgSchema] {
4643 use std::sync::LazyLock;
4644 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
4645 vec![
4646 ArgSchema::number_lenient_scalar(),
4647 ArgSchema::number_lenient_scalar(),
4648 ArgSchema::number_lenient_scalar(),
4649 ]
4650 });
4651 &SCHEMA[..]
4652 }
4653 fn eval<'a, 'b, 'c>(
4654 &self,
4655 args: &'c [ArgumentHandle<'a, 'b>],
4656 _ctx: &dyn FunctionContext<'b>,
4657 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4658 let x = coerce_num(&scalar_like_value(&args[0])?)?;
4659 let lambda = coerce_num(&scalar_like_value(&args[1])?)?;
4660 let cumulative = coerce_num(&scalar_like_value(&args[2])?)? != 0.0;
4661
4662 if x < 0.0 || lambda <= 0.0 {
4663 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4664 ExcelError::new_num(),
4665 )));
4666 }
4667
4668 let result = if cumulative {
4669 // CDF: 1 - e^(-lambda*x)
4670 1.0 - (-lambda * x).exp()
4671 } else {
4672 // PDF: lambda * e^(-lambda*x)
4673 lambda * (-lambda * x).exp()
4674 };
4675
4676 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
4677 result,
4678 )))
4679 }
4680}
4681
4682/// Returns the gamma-distribution probability at `x` for shape `alpha` and scale `beta`.
4683///
4684/// `GAMMA.DIST` supports cumulative and density modes for right-skewed waiting-time models.
4685///
4686/// # Remarks
4687/// - Requires `x >= 0`, `alpha > 0`, and `beta > 0`.
4688/// - Set `cumulative` to non-zero for CDF mode, or `0` for PDF mode.
4689/// - Returns `#NUM!` when any parameter is outside its valid range.
4690/// - Invalid numeric coercions propagate as spreadsheet errors.
4691///
4692/// # Examples
4693///
4694/// ```yaml,sandbox
4695/// title: "Gamma CDF with alpha=1 and beta=2"
4696/// formula: "=GAMMA.DIST(2,1,2,TRUE)"
4697/// expected: 0.6321205588285577
4698/// ```
4699///
4700/// ```yaml,sandbox
4701/// title: "Gamma PDF with alpha=1 and beta=2"
4702/// formula: "=GAMMA.DIST(2,1,2,FALSE)"
4703/// expected: 0.18393972058572117
4704/// ```
4705#[derive(Debug)]
4706pub struct GammaDistFn;
4707/// [formualizer-docgen:schema:start]
4708/// Name: GAMMA.DIST
4709/// Type: GammaDistFn
4710/// Min args: 4
4711/// Max args: 4
4712/// Variadic: false
4713/// Signature: GAMMA.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar)
4714/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg4{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
4715/// Caps: PURE
4716/// [formualizer-docgen:schema:end]
4717impl Function for GammaDistFn {
4718 func_caps!(PURE);
4719 fn name(&self) -> &'static str {
4720 "GAMMA.DIST"
4721 }
4722 fn min_args(&self) -> usize {
4723 4
4724 }
4725 fn arg_schema(&self) -> &'static [ArgSchema] {
4726 use std::sync::LazyLock;
4727 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
4728 vec![
4729 ArgSchema::number_lenient_scalar(),
4730 ArgSchema::number_lenient_scalar(),
4731 ArgSchema::number_lenient_scalar(),
4732 ArgSchema::number_lenient_scalar(),
4733 ]
4734 });
4735 &SCHEMA[..]
4736 }
4737 fn eval<'a, 'b, 'c>(
4738 &self,
4739 args: &'c [ArgumentHandle<'a, 'b>],
4740 _ctx: &dyn FunctionContext<'b>,
4741 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4742 let x = coerce_num(&scalar_like_value(&args[0])?)?;
4743 let alpha = coerce_num(&scalar_like_value(&args[1])?)?; // shape
4744 let beta = coerce_num(&scalar_like_value(&args[2])?)?; // scale
4745 let cumulative = coerce_num(&scalar_like_value(&args[3])?)? != 0.0;
4746
4747 if x < 0.0 || alpha <= 0.0 || beta <= 0.0 {
4748 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4749 ExcelError::new_num(),
4750 )));
4751 }
4752
4753 let result = if cumulative {
4754 // CDF: P(alpha, x/beta) where P is the regularized lower incomplete gamma
4755 gamma_p(alpha, x / beta)
4756 } else {
4757 // PDF: x^(alpha-1) * e^(-x/beta) / (beta^alpha * Gamma(alpha))
4758 let ln_pdf = (alpha - 1.0) * x.ln() - x / beta - alpha * beta.ln() - ln_gamma(alpha);
4759 ln_pdf.exp()
4760 };
4761
4762 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
4763 result,
4764 )))
4765 }
4766}
4767
4768/// Returns the Weibull-distribution probability at `x` for shape `alpha` and scale `beta`.
4769///
4770/// `WEIBULL.DIST` is commonly used for reliability and time-to-failure analysis.
4771///
4772/// # Remarks
4773/// - Requires `x >= 0`, `alpha > 0`, and `beta > 0`.
4774/// - Set `cumulative` to non-zero for CDF mode, or `0` for PDF mode.
4775/// - Returns `#NUM!` when parameters fall outside valid ranges.
4776/// - In PDF mode at `x = 0`, behavior follows the Weibull shape-specific limit.
4777///
4778/// # Examples
4779///
4780/// ```yaml,sandbox
4781/// title: "Weibull CDF with alpha=1 and beta=2"
4782/// formula: "=WEIBULL.DIST(2,1,2,TRUE)"
4783/// expected: 0.6321205588285577
4784/// ```
4785///
4786/// ```yaml,sandbox
4787/// title: "Weibull PDF with alpha=1 and beta=2"
4788/// formula: "=WEIBULL.DIST(2,1,2,FALSE)"
4789/// expected: 0.18393972058572117
4790/// ```
4791#[derive(Debug)]
4792pub struct WeibullDistFn;
4793/// [formualizer-docgen:schema:start]
4794/// Name: WEIBULL.DIST
4795/// Type: WeibullDistFn
4796/// Min args: 4
4797/// Max args: 4
4798/// Variadic: false
4799/// Signature: WEIBULL.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar)
4800/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg4{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
4801/// Caps: PURE
4802/// [formualizer-docgen:schema:end]
4803impl Function for WeibullDistFn {
4804 func_caps!(PURE);
4805 fn name(&self) -> &'static str {
4806 "WEIBULL.DIST"
4807 }
4808 fn min_args(&self) -> usize {
4809 4
4810 }
4811 fn arg_schema(&self) -> &'static [ArgSchema] {
4812 use std::sync::LazyLock;
4813 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
4814 vec![
4815 ArgSchema::number_lenient_scalar(),
4816 ArgSchema::number_lenient_scalar(),
4817 ArgSchema::number_lenient_scalar(),
4818 ArgSchema::number_lenient_scalar(),
4819 ]
4820 });
4821 &SCHEMA[..]
4822 }
4823 fn eval<'a, 'b, 'c>(
4824 &self,
4825 args: &'c [ArgumentHandle<'a, 'b>],
4826 _ctx: &dyn FunctionContext<'b>,
4827 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4828 let x = coerce_num(&scalar_like_value(&args[0])?)?;
4829 let alpha = coerce_num(&scalar_like_value(&args[1])?)?; // shape
4830 let beta = coerce_num(&scalar_like_value(&args[2])?)?; // scale
4831 let cumulative = coerce_num(&scalar_like_value(&args[3])?)? != 0.0;
4832
4833 if x < 0.0 || alpha <= 0.0 || beta <= 0.0 {
4834 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4835 ExcelError::new_num(),
4836 )));
4837 }
4838
4839 let result = if cumulative {
4840 // CDF: 1 - e^(-(x/beta)^alpha)
4841 1.0 - (-(x / beta).powf(alpha)).exp()
4842 } else {
4843 // PDF: (alpha/beta) * (x/beta)^(alpha-1) * e^(-(x/beta)^alpha)
4844 if x == 0.0 {
4845 if alpha < 1.0 {
4846 f64::INFINITY
4847 } else if alpha == 1.0 {
4848 alpha / beta
4849 } else {
4850 0.0
4851 }
4852 } else {
4853 (alpha / beta) * (x / beta).powf(alpha - 1.0) * (-(x / beta).powf(alpha)).exp()
4854 }
4855 };
4856
4857 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
4858 result,
4859 )))
4860 }
4861}
4862
4863/// Returns the beta-distribution probability for `x`, with optional lower/upper bounds.
4864///
4865/// `BETA.DIST` can evaluate either the cumulative probability or density on `[A, B]` (default
4866/// `[0, 1]`).
4867///
4868/// # Remarks
4869/// - Requires `alpha > 0`, `beta > 0`, and `A < B`.
4870/// - `x` must lie within the inclusive interval `[A, B]`.
4871/// - Set `cumulative` to non-zero for CDF mode, or `0` for PDF mode.
4872/// - Returns `#NUM!` for invalid bounds, parameters, or out-of-range `x`.
4873///
4874/// # Examples
4875///
4876/// ```yaml,sandbox
4877/// title: "Uniform beta CDF on [0,1]"
4878/// formula: "=BETA.DIST(0.3,1,1,TRUE)"
4879/// expected: 0.3
4880/// ```
4881///
4882/// ```yaml,sandbox
4883/// title: "Uniform beta PDF on [0,1]"
4884/// formula: "=BETA.DIST(0.3,1,1,FALSE)"
4885/// expected: 1
4886/// ```
4887#[derive(Debug)]
4888pub struct BetaDistFn;
4889/// [formualizer-docgen:schema:start]
4890/// Name: BETA.DIST
4891/// Type: BetaDistFn
4892/// Min args: 4
4893/// Max args: variadic
4894/// Variadic: true
4895/// Signature: BETA.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar, arg5: number@scalar, arg6...: number@scalar)
4896/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg4{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg5{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg6{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
4897/// Caps: PURE
4898/// [formualizer-docgen:schema:end]
4899impl Function for BetaDistFn {
4900 func_caps!(PURE);
4901 fn name(&self) -> &'static str {
4902 "BETA.DIST"
4903 }
4904 fn min_args(&self) -> usize {
4905 4
4906 }
4907 fn variadic(&self) -> bool {
4908 true
4909 }
4910 fn arg_schema(&self) -> &'static [ArgSchema] {
4911 use std::sync::LazyLock;
4912 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
4913 vec![
4914 ArgSchema::number_lenient_scalar(),
4915 ArgSchema::number_lenient_scalar(),
4916 ArgSchema::number_lenient_scalar(),
4917 ArgSchema::number_lenient_scalar(),
4918 ArgSchema::number_lenient_scalar(),
4919 ArgSchema::number_lenient_scalar(),
4920 ]
4921 });
4922 &SCHEMA[..]
4923 }
4924 fn eval<'a, 'b, 'c>(
4925 &self,
4926 args: &'c [ArgumentHandle<'a, 'b>],
4927 _ctx: &dyn FunctionContext<'b>,
4928 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4929 let x = coerce_num(&scalar_like_value(&args[0])?)?;
4930 let alpha = coerce_num(&scalar_like_value(&args[1])?)?;
4931 let beta_param = coerce_num(&scalar_like_value(&args[2])?)?;
4932 let cumulative = coerce_num(&scalar_like_value(&args[3])?)? != 0.0;
4933
4934 // Optional bounds A and B (default 0 and 1)
4935 let a = if args.len() > 4 {
4936 coerce_num(&scalar_like_value(&args[4])?)?
4937 } else {
4938 0.0
4939 };
4940 let b = if args.len() > 5 {
4941 coerce_num(&scalar_like_value(&args[5])?)?
4942 } else {
4943 1.0
4944 };
4945
4946 if alpha <= 0.0 || beta_param <= 0.0 || a >= b {
4947 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4948 ExcelError::new_num(),
4949 )));
4950 }
4951
4952 // x must be in [a, b]
4953 if x < a || x > b {
4954 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4955 ExcelError::new_num(),
4956 )));
4957 }
4958
4959 // Transform x to standard [0,1] interval
4960 let x_std = (x - a) / (b - a);
4961
4962 let result = if cumulative {
4963 // CDF: I_x(alpha, beta) - regularized incomplete beta function
4964 beta_i(x_std, alpha, beta_param)
4965 } else {
4966 // PDF: (x-A)^(alpha-1) * (B-x)^(beta-1) / ((B-A)^(alpha+beta-1) * B(alpha, beta))
4967 let ln_beta = ln_gamma(alpha) + ln_gamma(beta_param) - ln_gamma(alpha + beta_param);
4968 let scale = b - a;
4969 if (x_std == 0.0 && alpha < 1.0) || (x_std == 1.0 && beta_param < 1.0) {
4970 f64::INFINITY
4971 } else if x_std == 0.0 {
4972 if alpha == 1.0 {
4973 (1.0 - x_std).powf(beta_param - 1.0) / (scale * ln_beta.exp())
4974 } else {
4975 0.0
4976 }
4977 } else if x_std == 1.0 {
4978 if beta_param == 1.0 {
4979 x_std.powf(alpha - 1.0) / (scale * ln_beta.exp())
4980 } else {
4981 0.0
4982 }
4983 } else {
4984 let ln_pdf =
4985 (alpha - 1.0) * x_std.ln() + (beta_param - 1.0) * (1.0 - x_std).ln() - ln_beta;
4986 ln_pdf.exp() / scale
4987 }
4988 };
4989
4990 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
4991 result,
4992 )))
4993 }
4994}
4995
4996/// Returns negative-binomial probabilities for failures observed before a target success count.
4997///
4998/// `NEGBINOM.DIST` supports exact-failure mode (PMF) and cumulative mode (CDF).
4999///
5000/// # Remarks
5001/// - `number_f` is truncated and must be `>= 0`.
5002/// - `number_s` is truncated and must be `>= 1`.
5003/// - `probability_s` must satisfy `0 < p < 1`.
5004/// - Returns `#NUM!` when counts or probability are outside valid ranges.
5005///
5006/// # Examples
5007///
5008/// ```yaml,sandbox
5009/// title: "Negative binomial PMF"
5010/// formula: "=NEGBINOM.DIST(2,1,0.5,FALSE)"
5011/// expected: 0.125
5012/// ```
5013///
5014/// ```yaml,sandbox
5015/// title: "Negative binomial CDF"
5016/// formula: "=NEGBINOM.DIST(2,1,0.5,TRUE)"
5017/// expected: 0.875
5018/// ```
5019#[derive(Debug)]
5020pub struct NegbinomDistFn;
5021/// [formualizer-docgen:schema:start]
5022/// Name: NEGBINOM.DIST
5023/// Type: NegbinomDistFn
5024/// Min args: 4
5025/// Max args: 4
5026/// Variadic: false
5027/// Signature: NEGBINOM.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar)
5028/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg4{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
5029/// Caps: PURE
5030/// [formualizer-docgen:schema:end]
5031impl Function for NegbinomDistFn {
5032 func_caps!(PURE);
5033 fn name(&self) -> &'static str {
5034 "NEGBINOM.DIST"
5035 }
5036 fn min_args(&self) -> usize {
5037 4
5038 }
5039 fn arg_schema(&self) -> &'static [ArgSchema] {
5040 use std::sync::LazyLock;
5041 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
5042 vec![
5043 ArgSchema::number_lenient_scalar(),
5044 ArgSchema::number_lenient_scalar(),
5045 ArgSchema::number_lenient_scalar(),
5046 ArgSchema::number_lenient_scalar(),
5047 ]
5048 });
5049 &SCHEMA[..]
5050 }
5051 fn eval<'a, 'b, 'c>(
5052 &self,
5053 args: &'c [ArgumentHandle<'a, 'b>],
5054 _ctx: &dyn FunctionContext<'b>,
5055 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5056 let number_f = coerce_num(&scalar_like_value(&args[0])?)?.trunc() as i64; // number of failures
5057 let number_s = coerce_num(&scalar_like_value(&args[1])?)?.trunc() as i64; // number of successes
5058 let prob_s = coerce_num(&scalar_like_value(&args[2])?)?; // probability of success
5059 let cumulative = coerce_num(&scalar_like_value(&args[3])?)? != 0.0;
5060
5061 if number_f < 0 || number_s < 1 || prob_s <= 0.0 || prob_s >= 1.0 {
5062 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5063 ExcelError::new_num(),
5064 )));
5065 }
5066
5067 let result = if cumulative {
5068 // CDF: sum from i=0 to number_f of P(X=i)
5069 // This is equivalent to I_{prob_s}(number_s, number_f + 1) using regularized beta
5070 beta_i(prob_s, number_s as f64, (number_f + 1) as f64)
5071 } else {
5072 // PMF: C(number_f + number_s - 1, number_s - 1) * prob_s^number_s * (1-prob_s)^number_f
5073 // = C(k + r - 1, r - 1) * p^r * (1-p)^k where k = number_f, r = number_s
5074 let ln_prob = ln_binom(number_f + number_s - 1, number_s - 1)
5075 + (number_s as f64) * prob_s.ln()
5076 + (number_f as f64) * (1.0 - prob_s).ln();
5077 ln_prob.exp()
5078 };
5079
5080 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
5081 result,
5082 )))
5083 }
5084}
5085
5086/// Returns hypergeometric probabilities for successes drawn without replacement.
5087///
5088/// Use `HYPGEOM.DIST` for finite-population sampling where each draw changes remaining odds.
5089///
5090/// # Remarks
5091/// - Count inputs are truncated to integers.
5092/// - Requires valid population/sample bounds and feasible success counts.
5093/// - Set `cumulative` to non-zero for CDF mode, or `0` for PMF mode.
5094/// - Returns `#NUM!` for invalid population setup; out-of-support PMF values return `0`.
5095///
5096/// # Examples
5097///
5098/// ```yaml,sandbox
5099/// title: "Hypergeometric PMF"
5100/// formula: "=HYPGEOM.DIST(1,3,4,10,FALSE)"
5101/// expected: 0.5
5102/// ```
5103///
5104/// ```yaml,sandbox
5105/// title: "Hypergeometric CDF"
5106/// formula: "=HYPGEOM.DIST(1,3,4,10,TRUE)"
5107/// expected: 0.6666666666666666
5108/// ```
5109#[derive(Debug)]
5110pub struct HypgeomDistFn;
5111/// [formualizer-docgen:schema:start]
5112/// Name: HYPGEOM.DIST
5113/// Type: HypgeomDistFn
5114/// Min args: 5
5115/// Max args: 5
5116/// Variadic: false
5117/// Signature: HYPGEOM.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar, arg5: number@scalar)
5118/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg4{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg5{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
5119/// Caps: PURE
5120/// [formualizer-docgen:schema:end]
5121impl Function for HypgeomDistFn {
5122 func_caps!(PURE);
5123 fn name(&self) -> &'static str {
5124 "HYPGEOM.DIST"
5125 }
5126 fn min_args(&self) -> usize {
5127 5
5128 }
5129 fn arg_schema(&self) -> &'static [ArgSchema] {
5130 use std::sync::LazyLock;
5131 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
5132 vec![
5133 ArgSchema::number_lenient_scalar(),
5134 ArgSchema::number_lenient_scalar(),
5135 ArgSchema::number_lenient_scalar(),
5136 ArgSchema::number_lenient_scalar(),
5137 ArgSchema::number_lenient_scalar(),
5138 ]
5139 });
5140 &SCHEMA[..]
5141 }
5142 fn eval<'a, 'b, 'c>(
5143 &self,
5144 args: &'c [ArgumentHandle<'a, 'b>],
5145 _ctx: &dyn FunctionContext<'b>,
5146 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5147 let sample_s = coerce_num(&scalar_like_value(&args[0])?)?.trunc() as i64; // successes in sample
5148 let number_sample = coerce_num(&scalar_like_value(&args[1])?)?.trunc() as i64; // sample size
5149 let population_s = coerce_num(&scalar_like_value(&args[2])?)?.trunc() as i64; // successes in population
5150 let number_pop = coerce_num(&scalar_like_value(&args[3])?)?.trunc() as i64; // population size
5151 let cumulative = coerce_num(&scalar_like_value(&args[4])?)? != 0.0;
5152
5153 // Validation
5154 if number_pop <= 0
5155 || population_s < 0
5156 || population_s > number_pop
5157 || number_sample < 0
5158 || number_sample > number_pop
5159 || sample_s < 0
5160 {
5161 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5162 ExcelError::new_num(),
5163 )));
5164 }
5165
5166 // sample_s must be at least max(0, number_sample - (number_pop - population_s))
5167 // and at most min(number_sample, population_s)
5168 let min_successes = 0.max(number_sample - (number_pop - population_s));
5169 let max_successes = number_sample.min(population_s);
5170
5171 if sample_s < min_successes || sample_s > max_successes {
5172 // Return 0 for PMF, or appropriate CDF value
5173 if cumulative {
5174 if sample_s < min_successes {
5175 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0)));
5176 } else {
5177 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(1.0)));
5178 }
5179 } else {
5180 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0)));
5181 }
5182 }
5183
5184 let result = if cumulative {
5185 // CDF: sum from i=min_successes to sample_s of P(X=i)
5186 let mut sum = 0.0;
5187 for i in min_successes..=sample_s {
5188 sum += hypgeom_pmf(i, number_sample, population_s, number_pop);
5189 }
5190 sum
5191 } else {
5192 // PMF: C(population_s, sample_s) * C(number_pop - population_s, number_sample - sample_s) / C(number_pop, number_sample)
5193 hypgeom_pmf(sample_s, number_sample, population_s, number_pop)
5194 };
5195
5196 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
5197 result,
5198 )))
5199 }
5200}
5201
5202/// Helper: Hypergeometric PMF
5203fn hypgeom_pmf(k: i64, n: i64, k_pop: i64, n_pop: i64) -> f64 {
5204 // P(X=k) = C(K, k) * C(N-K, n-k) / C(N, n)
5205 // Using logs to avoid overflow
5206 let ln_prob = ln_binom(k_pop, k) + ln_binom(n_pop - k_pop, n - k) - ln_binom(n_pop, n);
5207 ln_prob.exp()
5208}
5209
5210/* ═══════════════════════════════════════════════════════════════════════════
5211COVARIANCE AND CORRELATION FUNCTIONS
5212═══════════════════════════════════════════════════════════════════════════ */
5213
5214/// Returns population covariance for two paired numeric data sets.
5215///
5216/// `COVARIANCE.P` measures joint variability using `n` in the denominator.
5217///
5218/// # Remarks
5219/// - Arrays must resolve to the same number of numeric points.
5220/// - Uses population scaling (`/ n`) rather than sample scaling.
5221/// - Positive output indicates same-direction movement; negative output indicates opposite movement.
5222/// - Pairing and shape mismatches return spreadsheet errors from paired-array validation.
5223///
5224/// # Examples
5225///
5226/// ```yaml,sandbox
5227/// title: "Positive population covariance"
5228/// formula: "=COVARIANCE.P({1,3,5},{2,4,6})"
5229/// expected: 2.6666666666666665
5230/// ```
5231///
5232/// ```yaml,sandbox
5233/// title: "Negative population covariance"
5234/// formula: "=COVARIANCE.P({1,2,3},{3,2,1})"
5235/// expected: -0.6666666666666666
5236/// ```
5237#[derive(Debug)]
5238pub struct CovariancePFn;
5239/// [formualizer-docgen:schema:start]
5240/// Name: COVARIANCE.P
5241/// Type: CovariancePFn
5242/// Min args: 2
5243/// Max args: 1
5244/// Variadic: false
5245/// Signature: COVARIANCE.P(arg1: number@range)
5246/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
5247/// Caps: PURE, REDUCTION, NUMERIC_ONLY
5248/// [formualizer-docgen:schema:end]
5249impl Function for CovariancePFn {
5250 func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
5251 fn name(&self) -> &'static str {
5252 "COVARIANCE.P"
5253 }
5254 fn aliases(&self) -> &'static [&'static str] {
5255 &["COVAR"]
5256 }
5257 fn min_args(&self) -> usize {
5258 2
5259 }
5260 fn arg_schema(&self) -> &'static [ArgSchema] {
5261 &ARG_RANGE_NUM_LENIENT_ONE[..]
5262 }
5263 fn eval<'a, 'b, 'c>(
5264 &self,
5265 args: &'c [ArgumentHandle<'a, 'b>],
5266 _ctx: &dyn FunctionContext<'b>,
5267 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5268 let (y, x) = match collect_paired_arrays(args) {
5269 Ok(v) => v,
5270 Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
5271 };
5272
5273 let n = x.len() as f64;
5274 let mean_x = x.iter().sum::<f64>() / n;
5275 let mean_y = y.iter().sum::<f64>() / n;
5276
5277 let mut sum_xy = 0.0;
5278 for i in 0..x.len() {
5279 let dx = x[i] - mean_x;
5280 let dy = y[i] - mean_y;
5281 sum_xy += dx * dy;
5282 }
5283
5284 // Population covariance divides by n
5285 let covar = sum_xy / n;
5286 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
5287 covar,
5288 )))
5289 }
5290}
5291
5292/// Returns sample covariance for two paired numeric data sets.
5293///
5294/// `COVARIANCE.S` measures joint variability using `n - 1` in the denominator.
5295///
5296/// # Remarks
5297/// - Arrays must contain paired numeric values with matching lengths.
5298/// - Requires at least two paired points.
5299/// - Returns `#DIV/0!` when fewer than two numeric pairs are available.
5300/// - Pairing and shape mismatches return spreadsheet errors from paired-array validation.
5301///
5302/// # Examples
5303///
5304/// ```yaml,sandbox
5305/// title: "Positive sample covariance"
5306/// formula: "=COVARIANCE.S({1,3,5},{2,4,6})"
5307/// expected: 4
5308/// ```
5309///
5310/// ```yaml,sandbox
5311/// title: "Negative sample covariance"
5312/// formula: "=COVARIANCE.S({1,2,3},{3,2,1})"
5313/// expected: -1
5314/// ```
5315#[derive(Debug)]
5316pub struct CovarianceSFn;
5317/// [formualizer-docgen:schema:start]
5318/// Name: COVARIANCE.S
5319/// Type: CovarianceSFn
5320/// Min args: 2
5321/// Max args: 1
5322/// Variadic: false
5323/// Signature: COVARIANCE.S(arg1: number@range)
5324/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
5325/// Caps: PURE, REDUCTION, NUMERIC_ONLY
5326/// [formualizer-docgen:schema:end]
5327impl Function for CovarianceSFn {
5328 func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
5329 fn name(&self) -> &'static str {
5330 "COVARIANCE.S"
5331 }
5332 fn min_args(&self) -> usize {
5333 2
5334 }
5335 fn arg_schema(&self) -> &'static [ArgSchema] {
5336 &ARG_RANGE_NUM_LENIENT_ONE[..]
5337 }
5338 fn eval<'a, 'b, 'c>(
5339 &self,
5340 args: &'c [ArgumentHandle<'a, 'b>],
5341 _ctx: &dyn FunctionContext<'b>,
5342 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5343 let (y, x) = match collect_paired_arrays(args) {
5344 Ok(v) => v,
5345 Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
5346 };
5347
5348 let n = x.len();
5349 if n < 2 {
5350 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5351 ExcelError::new_div(),
5352 )));
5353 }
5354
5355 let mean_x = x.iter().sum::<f64>() / n as f64;
5356 let mean_y = y.iter().sum::<f64>() / n as f64;
5357
5358 let mut sum_xy = 0.0;
5359 for i in 0..n {
5360 let dx = x[i] - mean_x;
5361 let dy = y[i] - mean_y;
5362 sum_xy += dx * dy;
5363 }
5364
5365 // Sample covariance divides by (n - 1)
5366 let covar = sum_xy / (n - 1) as f64;
5367 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
5368 covar,
5369 )))
5370 }
5371}
5372
5373/// Returns the Pearson correlation coefficient between two paired numeric arrays.
5374///
5375/// `PEARSON` reports linear association on a normalized scale from `-1` to `1`.
5376///
5377/// # Remarks
5378/// - Arrays must contain the same number of numeric observations.
5379/// - Returns `#DIV/0!` when either array has zero variance.
5380/// - Positive values indicate positive linear association; negative values indicate inverse association.
5381/// - Pairing and shape mismatches return spreadsheet errors from paired-array validation.
5382///
5383/// # Examples
5384///
5385/// ```yaml,sandbox
5386/// title: "Perfect positive linear correlation"
5387/// formula: "=PEARSON({1,2,3},{2,4,6})"
5388/// expected: 1
5389/// ```
5390///
5391/// ```yaml,sandbox
5392/// title: "Perfect negative linear correlation"
5393/// formula: "=PEARSON({1,2,3},{3,2,1})"
5394/// expected: -1
5395/// ```
5396#[derive(Debug)]
5397pub struct PearsonFn;
5398/// [formualizer-docgen:schema:start]
5399/// Name: PEARSON
5400/// Type: PearsonFn
5401/// Min args: 2
5402/// Max args: 1
5403/// Variadic: false
5404/// Signature: PEARSON(arg1: number@range)
5405/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
5406/// Caps: PURE, REDUCTION, NUMERIC_ONLY
5407/// [formualizer-docgen:schema:end]
5408impl Function for PearsonFn {
5409 func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
5410 fn name(&self) -> &'static str {
5411 "PEARSON"
5412 }
5413 fn min_args(&self) -> usize {
5414 2
5415 }
5416 fn arg_schema(&self) -> &'static [ArgSchema] {
5417 &ARG_RANGE_NUM_LENIENT_ONE[..]
5418 }
5419 fn eval<'a, 'b, 'c>(
5420 &self,
5421 args: &'c [ArgumentHandle<'a, 'b>],
5422 _ctx: &dyn FunctionContext<'b>,
5423 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5424 let (y, x) = match collect_paired_arrays(args) {
5425 Ok(v) => v,
5426 Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
5427 };
5428
5429 let n = x.len() as f64;
5430 let mean_x = x.iter().sum::<f64>() / n;
5431 let mean_y = y.iter().sum::<f64>() / n;
5432
5433 let mut sum_xy = 0.0;
5434 let mut sum_x2 = 0.0;
5435 let mut sum_y2 = 0.0;
5436
5437 for i in 0..x.len() {
5438 let dx = x[i] - mean_x;
5439 let dy = y[i] - mean_y;
5440 sum_xy += dx * dy;
5441 sum_x2 += dx * dx;
5442 sum_y2 += dy * dy;
5443 }
5444
5445 let denom = (sum_x2 * sum_y2).sqrt();
5446 if denom == 0.0 {
5447 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5448 ExcelError::new_div(),
5449 )));
5450 }
5451
5452 let correl = sum_xy / denom;
5453 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
5454 correl,
5455 )))
5456 }
5457}
5458
5459/// Returns the coefficient of determination (`R^2`) for paired x/y data.
5460///
5461/// `RSQ` is the square of Pearson correlation and indicates explained linear variance.
5462///
5463/// # Remarks
5464/// - Arrays must contain the same number of numeric observations.
5465/// - Result is in `[0, 1]` for valid numeric inputs.
5466/// - Returns `#DIV/0!` when either input array has zero variance.
5467/// - Pairing and shape mismatches return spreadsheet errors from paired-array validation.
5468///
5469/// # Examples
5470///
5471/// ```yaml,sandbox
5472/// title: "Perfect linear fit"
5473/// formula: "=RSQ({1,2,3},{2,4,6})"
5474/// expected: 1
5475/// ```
5476///
5477/// ```yaml,sandbox
5478/// title: "Strong but imperfect linear relationship"
5479/// formula: "=RSQ({1,2,3},{1,2,4})"
5480/// expected: 0.9642857142857143
5481/// ```
5482#[derive(Debug)]
5483pub struct RsqFn;
5484/// [formualizer-docgen:schema:start]
5485/// Name: RSQ
5486/// Type: RsqFn
5487/// Min args: 2
5488/// Max args: 1
5489/// Variadic: false
5490/// Signature: RSQ(arg1: number@range)
5491/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
5492/// Caps: PURE, REDUCTION, NUMERIC_ONLY
5493/// [formualizer-docgen:schema:end]
5494impl Function for RsqFn {
5495 func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
5496 fn name(&self) -> &'static str {
5497 "RSQ"
5498 }
5499 fn min_args(&self) -> usize {
5500 2
5501 }
5502 fn arg_schema(&self) -> &'static [ArgSchema] {
5503 &ARG_RANGE_NUM_LENIENT_ONE[..]
5504 }
5505 fn eval<'a, 'b, 'c>(
5506 &self,
5507 args: &'c [ArgumentHandle<'a, 'b>],
5508 _ctx: &dyn FunctionContext<'b>,
5509 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5510 let (y, x) = match collect_paired_arrays(args) {
5511 Ok(v) => v,
5512 Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
5513 };
5514
5515 let n = x.len() as f64;
5516 let mean_x = x.iter().sum::<f64>() / n;
5517 let mean_y = y.iter().sum::<f64>() / n;
5518
5519 let mut sum_xy = 0.0;
5520 let mut sum_x2 = 0.0;
5521 let mut sum_y2 = 0.0;
5522
5523 for i in 0..x.len() {
5524 let dx = x[i] - mean_x;
5525 let dy = y[i] - mean_y;
5526 sum_xy += dx * dy;
5527 sum_x2 += dx * dx;
5528 sum_y2 += dy * dy;
5529 }
5530
5531 let denom = sum_x2 * sum_y2;
5532 if denom == 0.0 {
5533 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5534 ExcelError::new_div(),
5535 )));
5536 }
5537
5538 // R-squared = r^2 = (sum_xy)^2 / (sum_x2 * sum_y2)
5539 let rsq = (sum_xy * sum_xy) / denom;
5540 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(rsq)))
5541 }
5542}
5543
5544/// Returns the standard error of y-estimates from a simple linear regression.
5545///
5546/// `STEYX` measures the typical residual size around the fitted regression line.
5547///
5548/// # Remarks
5549/// - Requires paired x/y inputs with matching numeric lengths.
5550/// - Requires at least three paired points.
5551/// - Returns `#DIV/0!` when `n < 3` or x-values have zero variance.
5552/// - Pairing and shape mismatches return spreadsheet errors from paired-array validation.
5553///
5554/// # Examples
5555///
5556/// ```yaml,sandbox
5557/// title: "Perfect linear fit has zero standard error"
5558/// formula: "=STEYX({2,4,6},{1,2,3})"
5559/// expected: 0
5560/// ```
5561///
5562/// ```yaml,sandbox
5563/// title: "Non-zero regression standard error"
5564/// formula: "=STEYX({2,5,7},{1,2,3})"
5565/// expected: 0.408248290463863
5566/// ```
5567#[derive(Debug)]
5568pub struct SteyxFn;
5569/// [formualizer-docgen:schema:start]
5570/// Name: STEYX
5571/// Type: SteyxFn
5572/// Min args: 2
5573/// Max args: 1
5574/// Variadic: false
5575/// Signature: STEYX(arg1: number@range)
5576/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
5577/// Caps: PURE, REDUCTION, NUMERIC_ONLY
5578/// [formualizer-docgen:schema:end]
5579impl Function for SteyxFn {
5580 func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
5581 fn name(&self) -> &'static str {
5582 "STEYX"
5583 }
5584 fn min_args(&self) -> usize {
5585 2
5586 }
5587 fn arg_schema(&self) -> &'static [ArgSchema] {
5588 &ARG_RANGE_NUM_LENIENT_ONE[..]
5589 }
5590 fn eval<'a, 'b, 'c>(
5591 &self,
5592 args: &'c [ArgumentHandle<'a, 'b>],
5593 _ctx: &dyn FunctionContext<'b>,
5594 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5595 let (y, x) = match collect_paired_arrays(args) {
5596 Ok(v) => v,
5597 Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
5598 };
5599
5600 let n = x.len();
5601 if n < 3 {
5602 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5603 ExcelError::new_div(),
5604 )));
5605 }
5606
5607 let n_f = n as f64;
5608 let mean_x = x.iter().sum::<f64>() / n_f;
5609 let mean_y = y.iter().sum::<f64>() / n_f;
5610
5611 let mut sum_xy = 0.0;
5612 let mut sum_x2 = 0.0;
5613 let mut sum_y2 = 0.0;
5614
5615 for i in 0..n {
5616 let dx = x[i] - mean_x;
5617 let dy = y[i] - mean_y;
5618 sum_xy += dx * dy;
5619 sum_x2 += dx * dx;
5620 sum_y2 += dy * dy;
5621 }
5622
5623 if sum_x2 == 0.0 {
5624 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5625 ExcelError::new_div(),
5626 )));
5627 }
5628
5629 // STEYX = sqrt((sum_y2 - (sum_xy)^2 / sum_x2) / (n - 2))
5630 let sse = sum_y2 - (sum_xy * sum_xy) / sum_x2;
5631 if sse < 0.0 {
5632 // This can happen due to floating point errors; return 0 in such case
5633 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0)));
5634 }
5635 let steyx = (sse / (n_f - 2.0)).sqrt();
5636 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
5637 steyx,
5638 )))
5639 }
5640}
5641
5642/* ─────────────────────────── SKEW ──────────────────────────── */
5643
5644/// Returns the sample skewness of a numeric distribution.
5645///
5646/// `SKEW` quantifies asymmetry: positive values indicate a longer right tail, negative values a
5647/// longer left tail.
5648///
5649/// # Remarks
5650/// - Requires at least three numeric values.
5651/// - Returns `#DIV/0!` when there are fewer than three numbers or zero sample standard deviation.
5652/// - Non-numeric values in ranges are ignored by statistical-collection rules.
5653/// - Uses the Excel-style sample skewness correction factor.
5654///
5655/// # Examples
5656///
5657/// ```yaml,sandbox
5658/// title: "Symmetric sample"
5659/// formula: "=SKEW({1,2,3})"
5660/// expected: 0
5661/// ```
5662///
5663/// ```yaml,sandbox
5664/// title: "Right-skewed sample"
5665/// formula: "=SKEW({1,1,2,10})"
5666/// expected: 1.9683567600862015
5667/// ```
5668#[derive(Debug)]
5669pub struct SkewFn;
5670/// [formualizer-docgen:schema:start]
5671/// Name: SKEW
5672/// Type: SkewFn
5673/// Min args: 1
5674/// Max args: variadic
5675/// Variadic: true
5676/// Signature: SKEW(arg1...: number@range)
5677/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
5678/// Caps: PURE, REDUCTION, NUMERIC_ONLY
5679/// [formualizer-docgen:schema:end]
5680impl Function for SkewFn {
5681 func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
5682 fn name(&self) -> &'static str {
5683 "SKEW"
5684 }
5685 fn min_args(&self) -> usize {
5686 1
5687 }
5688 fn variadic(&self) -> bool {
5689 true
5690 }
5691 fn arg_schema(&self) -> &'static [ArgSchema] {
5692 &ARG_RANGE_NUM_LENIENT_ONE[..]
5693 }
5694 fn eval<'a, 'b, 'c>(
5695 &self,
5696 args: &'c [ArgumentHandle<'a, 'b>],
5697 _ctx: &dyn FunctionContext<'b>,
5698 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5699 let nums = collect_numeric_stats(args)?;
5700 let n = nums.len();
5701
5702 // SKEW requires at least 3 data points
5703 if n < 3 {
5704 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5705 ExcelError::new_div(),
5706 )));
5707 }
5708
5709 let n_f = n as f64;
5710 let mean = nums.iter().sum::<f64>() / n_f;
5711
5712 // Calculate sample standard deviation
5713 let mut sum_sq = 0.0;
5714 for &v in &nums {
5715 let d = v - mean;
5716 sum_sq += d * d;
5717 }
5718 let stdev = (sum_sq / (n_f - 1.0)).sqrt();
5719
5720 if stdev == 0.0 {
5721 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5722 ExcelError::new_div(),
5723 )));
5724 }
5725
5726 // Calculate sum of cubed deviations normalized by stdev
5727 let mut sum_cubed = 0.0;
5728 for &v in &nums {
5729 let d = (v - mean) / stdev;
5730 sum_cubed += d * d * d;
5731 }
5732
5733 // Excel SKEW formula: n / ((n-1)*(n-2)) * sum((xi - mean)/stdev)^3
5734 let skew = (n_f / ((n_f - 1.0) * (n_f - 2.0))) * sum_cubed;
5735 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(skew)))
5736 }
5737}
5738
5739/* ─────────────────────────── KURT ──────────────────────────── */
5740
5741/// Returns the sample excess kurtosis of a numeric distribution.
5742///
5743/// `KURT` indicates tail heaviness relative to a normal distribution after Excel-style sample
5744/// correction.
5745///
5746/// # Remarks
5747/// - Requires at least four numeric values.
5748/// - Returns `#DIV/0!` when there are fewer than four numbers or zero sample standard deviation.
5749/// - Positive values suggest heavier tails; negative values suggest lighter tails.
5750/// - Non-numeric values in ranges are ignored by statistical-collection rules.
5751///
5752/// # Examples
5753///
5754/// ```yaml,sandbox
5755/// title: "Uniformly spaced values"
5756/// formula: "=KURT({1,2,3,4})"
5757/// expected: -1.2
5758/// ```
5759///
5760/// ```yaml,sandbox
5761/// title: "Heavier-tail sample"
5762/// formula: "=KURT({1,1,1,2,10,10,10,10})"
5763/// expected: -2.3069755007920767
5764/// ```
5765#[derive(Debug)]
5766pub struct KurtFn;
5767/// [formualizer-docgen:schema:start]
5768/// Name: KURT
5769/// Type: KurtFn
5770/// Min args: 1
5771/// Max args: variadic
5772/// Variadic: true
5773/// Signature: KURT(arg1...: number@range)
5774/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
5775/// Caps: PURE, REDUCTION, NUMERIC_ONLY
5776/// [formualizer-docgen:schema:end]
5777impl Function for KurtFn {
5778 func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
5779 fn name(&self) -> &'static str {
5780 "KURT"
5781 }
5782 fn min_args(&self) -> usize {
5783 1
5784 }
5785 fn variadic(&self) -> bool {
5786 true
5787 }
5788 fn arg_schema(&self) -> &'static [ArgSchema] {
5789 &ARG_RANGE_NUM_LENIENT_ONE[..]
5790 }
5791 fn eval<'a, 'b, 'c>(
5792 &self,
5793 args: &'c [ArgumentHandle<'a, 'b>],
5794 _ctx: &dyn FunctionContext<'b>,
5795 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5796 let nums = collect_numeric_stats(args)?;
5797 let n = nums.len();
5798
5799 // KURT requires at least 4 data points
5800 if n < 4 {
5801 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5802 ExcelError::new_div(),
5803 )));
5804 }
5805
5806 let n_f = n as f64;
5807 let mean = nums.iter().sum::<f64>() / n_f;
5808
5809 // Calculate sample standard deviation
5810 let mut sum_sq = 0.0;
5811 for &v in &nums {
5812 let d = v - mean;
5813 sum_sq += d * d;
5814 }
5815 let stdev = (sum_sq / (n_f - 1.0)).sqrt();
5816
5817 if stdev == 0.0 {
5818 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5819 ExcelError::new_div(),
5820 )));
5821 }
5822
5823 // Calculate sum of fourth powers of deviations normalized by stdev
5824 let mut sum_fourth = 0.0;
5825 for &v in &nums {
5826 let d = (v - mean) / stdev;
5827 sum_fourth += d * d * d * d;
5828 }
5829
5830 // Excel KURT formula (excess kurtosis):
5831 // n*(n+1) / ((n-1)*(n-2)*(n-3)) * sum((xi - mean)/stdev)^4 - 3*(n-1)^2 / ((n-2)*(n-3))
5832 let term1 = (n_f * (n_f + 1.0)) / ((n_f - 1.0) * (n_f - 2.0) * (n_f - 3.0)) * sum_fourth;
5833 let term2 = (3.0 * (n_f - 1.0) * (n_f - 1.0)) / ((n_f - 2.0) * (n_f - 3.0));
5834 let kurt = term1 - term2;
5835 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(kurt)))
5836 }
5837}
5838
5839/* ─────────────────────────── FISHER ──────────────────────────── */
5840
5841/// Returns the Fisher z-transformation of a correlation-like value `x`.
5842///
5843/// `FISHER` maps `(-1, 1)` into `(-inf, +inf)` and is commonly used in correlation inference.
5844///
5845/// # Remarks
5846/// - Input must satisfy `-1 < x < 1`.
5847/// - Returns `#NUM!` when `x <= -1` or `x >= 1`.
5848/// - The transformation is `0.5 * ln((1 + x) / (1 - x))`.
5849/// - Invalid numeric coercions propagate as spreadsheet errors.
5850///
5851/// # Examples
5852///
5853/// ```yaml,sandbox
5854/// title: "Fisher transform at zero"
5855/// formula: "=FISHER(0)"
5856/// expected: 0
5857/// ```
5858///
5859/// ```yaml,sandbox
5860/// title: "Fisher transform at x=0.5"
5861/// formula: "=FISHER(0.5)"
5862/// expected: 0.5493061443340549
5863/// ```
5864#[derive(Debug)]
5865pub struct FisherFn;
5866/// [formualizer-docgen:schema:start]
5867/// Name: FISHER
5868/// Type: FisherFn
5869/// Min args: 1
5870/// Max args: 1
5871/// Variadic: false
5872/// Signature: FISHER(arg1: number@range)
5873/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
5874/// Caps: PURE, NUMERIC_ONLY
5875/// [formualizer-docgen:schema:end]
5876impl Function for FisherFn {
5877 func_caps!(PURE, NUMERIC_ONLY);
5878 fn name(&self) -> &'static str {
5879 "FISHER"
5880 }
5881 fn min_args(&self) -> usize {
5882 1
5883 }
5884 fn arg_schema(&self) -> &'static [ArgSchema] {
5885 &ARG_RANGE_NUM_LENIENT_ONE[..]
5886 }
5887 fn eval<'a, 'b, 'c>(
5888 &self,
5889 args: &'c [ArgumentHandle<'a, 'b>],
5890 _ctx: &dyn FunctionContext<'b>,
5891 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5892 let x = coerce_num(&scalar_like_value(&args[0])?)?;
5893
5894 // FISHER requires -1 < x < 1
5895 if x <= -1.0 || x >= 1.0 {
5896 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5897 ExcelError::new_num(),
5898 )));
5899 }
5900
5901 // Fisher transformation: 0.5 * ln((1 + x) / (1 - x))
5902 let fisher = 0.5 * ((1.0 + x) / (1.0 - x)).ln();
5903 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
5904 fisher,
5905 )))
5906 }
5907}
5908
5909/* ─────────────────────────── FISHERINV ──────────────────────────── */
5910
5911/// Returns the inverse Fisher transformation of `y`.
5912///
5913/// `FISHERINV` maps Fisher z-values back to the open interval `(-1, 1)`.
5914///
5915/// # Remarks
5916/// - The inverse form is `(e^(2y) - 1) / (e^(2y) + 1)`.
5917/// - Output is always strictly between `-1` and `1` for finite inputs.
5918/// - This function is useful for converting transformed correlation estimates back to r-space.
5919/// - Invalid numeric coercions propagate as spreadsheet errors.
5920///
5921/// # Examples
5922///
5923/// ```yaml,sandbox
5924/// title: "Inverse Fisher at zero"
5925/// formula: "=FISHERINV(0)"
5926/// expected: 0
5927/// ```
5928///
5929/// ```yaml,sandbox
5930/// title: "Round-trip with FISHER(0.5)"
5931/// formula: "=FISHERINV(0.5493061443340549)"
5932/// expected: 0.5
5933/// ```
5934#[derive(Debug)]
5935pub struct FisherInvFn;
5936/// [formualizer-docgen:schema:start]
5937/// Name: FISHERINV
5938/// Type: FisherInvFn
5939/// Min args: 1
5940/// Max args: 1
5941/// Variadic: false
5942/// Signature: FISHERINV(arg1: number@range)
5943/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
5944/// Caps: PURE, NUMERIC_ONLY
5945/// [formualizer-docgen:schema:end]
5946impl Function for FisherInvFn {
5947 func_caps!(PURE, NUMERIC_ONLY);
5948 fn name(&self) -> &'static str {
5949 "FISHERINV"
5950 }
5951 fn min_args(&self) -> usize {
5952 1
5953 }
5954 fn arg_schema(&self) -> &'static [ArgSchema] {
5955 &ARG_RANGE_NUM_LENIENT_ONE[..]
5956 }
5957 fn eval<'a, 'b, 'c>(
5958 &self,
5959 args: &'c [ArgumentHandle<'a, 'b>],
5960 _ctx: &dyn FunctionContext<'b>,
5961 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5962 let y = coerce_num(&scalar_like_value(&args[0])?)?;
5963
5964 // Inverse Fisher transformation: (e^(2y) - 1) / (e^(2y) + 1)
5965 let e2y = (2.0 * y).exp();
5966 let fisherinv = (e2y - 1.0) / (e2y + 1.0);
5967 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
5968 fisherinv,
5969 )))
5970 }
5971}
5972
5973/* ─────────────────────────── FORECAST.LINEAR ──────────────────────────── */
5974
5975/// Returns a predicted y-value at `x` from simple linear regression over known data.
5976///
5977/// `FORECAST.LINEAR` fits `y = intercept + slope * x` and evaluates that line at the requested x.
5978///
5979/// # Remarks
5980/// - Requires `known_y` and `known_x` arrays with the same numeric length.
5981/// - Returns `#N/A` when arrays are empty or lengths do not match.
5982/// - Returns `#DIV/0!` when `known_x` has zero variance.
5983/// - Alias `FORECAST` is supported.
5984///
5985/// # Examples
5986///
5987/// ```yaml,sandbox
5988/// title: "Predict next point on a perfect line"
5989/// formula: "=FORECAST.LINEAR(4,{2,4,6},{1,2,3})"
5990/// expected: 8
5991/// ```
5992///
5993/// ```yaml,sandbox
5994/// title: "Forecast with non-zero intercept"
5995/// formula: "=FORECAST.LINEAR(5,{3,5,7},{1,2,3})"
5996/// expected: 11
5997/// ```
5998#[derive(Debug)]
5999pub struct ForecastLinearFn;
6000/// [formualizer-docgen:schema:start]
6001/// Name: FORECAST.LINEAR
6002/// Type: ForecastLinearFn
6003/// Min args: 3
6004/// Max args: 1
6005/// Variadic: false
6006/// Signature: FORECAST.LINEAR(arg1: number@range)
6007/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
6008/// Caps: PURE, NUMERIC_ONLY
6009/// [formualizer-docgen:schema:end]
6010impl Function for ForecastLinearFn {
6011 func_caps!(PURE, NUMERIC_ONLY);
6012 fn name(&self) -> &'static str {
6013 "FORECAST.LINEAR"
6014 }
6015 fn aliases(&self) -> &'static [&'static str] {
6016 &["FORECAST"]
6017 }
6018 fn min_args(&self) -> usize {
6019 3
6020 }
6021 fn arg_schema(&self) -> &'static [ArgSchema] {
6022 &ARG_RANGE_NUM_LENIENT_ONE[..]
6023 }
6024 fn eval<'a, 'b, 'c>(
6025 &self,
6026 args: &'c [ArgumentHandle<'a, 'b>],
6027 _ctx: &dyn FunctionContext<'b>,
6028 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
6029 // args[0] = x value to forecast
6030 // args[1] = known_y's
6031 // args[2] = known_x's
6032 let x = match coerce_num(&scalar_like_value(&args[0])?) {
6033 Ok(n) => n,
6034 Err(_) => {
6035 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6036 ExcelError::new_value(),
6037 )));
6038 }
6039 };
6040
6041 let y_vals = collect_numeric_stats(&args[1..2])?;
6042 let x_vals = collect_numeric_stats(&args[2..3])?;
6043
6044 // Arrays must have same length
6045 if y_vals.len() != x_vals.len() {
6046 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6047 ExcelError::new_na(),
6048 )));
6049 }
6050
6051 if y_vals.is_empty() {
6052 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6053 ExcelError::new_na(),
6054 )));
6055 }
6056
6057 let n = x_vals.len() as f64;
6058 let mean_x = x_vals.iter().sum::<f64>() / n;
6059 let mean_y = y_vals.iter().sum::<f64>() / n;
6060
6061 let mut sum_xy = 0.0;
6062 let mut sum_x2 = 0.0;
6063
6064 for i in 0..x_vals.len() {
6065 let dx = x_vals[i] - mean_x;
6066 let dy = y_vals[i] - mean_y;
6067 sum_xy += dx * dy;
6068 sum_x2 += dx * dx;
6069 }
6070
6071 if sum_x2 == 0.0 {
6072 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6073 ExcelError::new_div(),
6074 )));
6075 }
6076
6077 let slope = sum_xy / sum_x2;
6078 let intercept = mean_y - slope * mean_x;
6079 let forecast = intercept + slope * x;
6080
6081 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
6082 forecast,
6083 )))
6084 }
6085}
6086
6087/* ─────────────────────────── LINEST ──────────────────────────── */
6088
6089/// Returns linear-regression coefficients and optional fit statistics.
6090///
6091/// `LINEST` fits a straight line to known y/x pairs and returns either `[slope, intercept]` or a
6092/// larger statistics matrix.
6093///
6094/// # Remarks
6095/// - `known_y` is required; `known_x` defaults to `1..n` when omitted.
6096/// - `const` controls whether an intercept is fitted (`TRUE` by default).
6097/// - `stats=TRUE` returns a `5x2` result block; otherwise it returns `1x2`.
6098/// - Returns spreadsheet errors for mismatched lengths, empty data, or degenerate x-values.
6099///
6100/// # Examples
6101///
6102/// ```yaml,sandbox
6103/// title: "Slope and intercept only"
6104/// formula: "=LINEST({2,4,6},{1,2,3})"
6105/// expected:
6106/// - [2, 0]
6107/// ```
6108///
6109/// ```yaml,sandbox
6110/// title: "Linear fit with non-zero intercept"
6111/// formula: "=LINEST({3,5,7},{1,2,3})"
6112/// expected:
6113/// - [2, 1]
6114/// ```
6115#[derive(Debug)]
6116pub struct LinestFn;
6117/// [formualizer-docgen:schema:start]
6118/// Name: LINEST
6119/// Type: LinestFn
6120/// Min args: 1
6121/// Max args: variadic
6122/// Variadic: true
6123/// Signature: LINEST(arg1...: number@range)
6124/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
6125/// Caps: PURE, NUMERIC_ONLY
6126/// [formualizer-docgen:schema:end]
6127impl Function for LinestFn {
6128 func_caps!(PURE, NUMERIC_ONLY);
6129 fn name(&self) -> &'static str {
6130 "LINEST"
6131 }
6132 fn min_args(&self) -> usize {
6133 1
6134 }
6135 fn variadic(&self) -> bool {
6136 true
6137 }
6138 fn arg_schema(&self) -> &'static [ArgSchema] {
6139 &ARG_RANGE_NUM_LENIENT_ONE[..]
6140 }
6141 fn eval<'a, 'b, 'c>(
6142 &self,
6143 args: &'c [ArgumentHandle<'a, 'b>],
6144 _ctx: &dyn FunctionContext<'b>,
6145 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
6146 // args[0] = known_y's (required)
6147 // args[1] = known_x's (optional, defaults to {1,2,3,...})
6148 // args[2] = const (optional, default TRUE - whether to compute intercept)
6149 // args[3] = stats (optional, default FALSE - whether to return additional statistics)
6150
6151 let y_vals = collect_numeric_stats(&args[0..1])?;
6152
6153 if y_vals.is_empty() {
6154 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6155 ExcelError::new_na(),
6156 )));
6157 }
6158
6159 // Get known_x's or generate default {1, 2, 3, ...}
6160 let x_vals = if args.len() >= 2 {
6161 collect_numeric_stats(&args[1..2])?
6162 } else {
6163 (1..=y_vals.len()).map(|i| i as f64).collect()
6164 };
6165
6166 // Arrays must have same length
6167 if y_vals.len() != x_vals.len() {
6168 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6169 ExcelError::new_ref(),
6170 )));
6171 }
6172
6173 // Parse const argument (default TRUE)
6174 let use_const = if args.len() >= 3 {
6175 match scalar_like_value(&args[2])? {
6176 LiteralValue::Boolean(b) => b,
6177 LiteralValue::Number(n) => n != 0.0,
6178 LiteralValue::Int(i) => i != 0,
6179 _ => true,
6180 }
6181 } else {
6182 true
6183 };
6184
6185 // Parse stats argument (default FALSE)
6186 let return_stats = if args.len() >= 4 {
6187 match scalar_like_value(&args[3])? {
6188 LiteralValue::Boolean(b) => b,
6189 LiteralValue::Number(n) => n != 0.0,
6190 LiteralValue::Int(i) => i != 0,
6191 _ => false,
6192 }
6193 } else {
6194 false
6195 };
6196
6197 let n = x_vals.len() as f64;
6198
6199 // Calculate regression coefficients
6200 let (slope, intercept) = if use_const {
6201 // Normal linear regression with intercept
6202 let mean_x = x_vals.iter().sum::<f64>() / n;
6203 let mean_y = y_vals.iter().sum::<f64>() / n;
6204
6205 let mut sum_xy = 0.0;
6206 let mut sum_x2 = 0.0;
6207
6208 for i in 0..x_vals.len() {
6209 let dx = x_vals[i] - mean_x;
6210 let dy = y_vals[i] - mean_y;
6211 sum_xy += dx * dy;
6212 sum_x2 += dx * dx;
6213 }
6214
6215 if sum_x2 == 0.0 {
6216 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6217 ExcelError::new_div(),
6218 )));
6219 }
6220
6221 let slope = sum_xy / sum_x2;
6222 let intercept = mean_y - slope * mean_x;
6223 (slope, intercept)
6224 } else {
6225 // Regression through origin (intercept = 0)
6226 let mut sum_xy = 0.0;
6227 let mut sum_x2 = 0.0;
6228
6229 for i in 0..x_vals.len() {
6230 sum_xy += x_vals[i] * y_vals[i];
6231 sum_x2 += x_vals[i] * x_vals[i];
6232 }
6233
6234 if sum_x2 == 0.0 {
6235 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6236 ExcelError::new_div(),
6237 )));
6238 }
6239
6240 let slope = sum_xy / sum_x2;
6241 (slope, 0.0)
6242 };
6243
6244 if !return_stats {
6245 // Return just slope and intercept as 1x2 array: [[slope, intercept]]
6246 let row = vec![LiteralValue::Number(slope), LiteralValue::Number(intercept)];
6247 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Array(vec![
6248 row,
6249 ])));
6250 }
6251
6252 // Calculate additional statistics for stats=TRUE
6253 // Row 1: [slope, intercept]
6254 // Row 2: [se_slope, se_intercept]
6255 // Row 3: [r_squared, se_y]
6256 // Row 4: [F_statistic, df]
6257 // Row 5: [ss_reg, ss_resid]
6258
6259 let mean_y = y_vals.iter().sum::<f64>() / n;
6260
6261 // Calculate residuals and sums of squares
6262 let mut ss_resid = 0.0; // Sum of squared residuals
6263 let mut ss_tot = 0.0; // Total sum of squares
6264
6265 for i in 0..x_vals.len() {
6266 let y_pred = slope * x_vals[i] + intercept;
6267 let residual = y_vals[i] - y_pred;
6268 ss_resid += residual * residual;
6269 let dy_tot = y_vals[i] - mean_y;
6270 ss_tot += dy_tot * dy_tot;
6271 }
6272
6273 let ss_reg = ss_tot - ss_resid; // Regression sum of squares
6274
6275 // R-squared
6276 let r_squared = if ss_tot == 0.0 {
6277 1.0 // Perfect fit or all y values are the same
6278 } else {
6279 1.0 - (ss_resid / ss_tot)
6280 };
6281
6282 // Degrees of freedom
6283 let df = if use_const {
6284 (n as i64 - 2).max(1) as f64 // n - k - 1 where k=1 (one predictor)
6285 } else {
6286 (n as i64 - 1).max(1) as f64 // n - k when no intercept
6287 };
6288
6289 // Standard error of y estimate
6290 let se_y = if df > 0.0 {
6291 (ss_resid / df).sqrt()
6292 } else {
6293 0.0
6294 };
6295
6296 // Standard errors of coefficients
6297 let mean_x = x_vals.iter().sum::<f64>() / n;
6298 let mut sum_x2_centered = 0.0;
6299 let mut sum_x2_raw = 0.0;
6300 for &xi in &x_vals {
6301 sum_x2_centered += (xi - mean_x).powi(2);
6302 sum_x2_raw += xi * xi;
6303 }
6304
6305 let se_slope = if sum_x2_centered > 0.0 && df > 0.0 {
6306 se_y / sum_x2_centered.sqrt()
6307 } else {
6308 f64::NAN
6309 };
6310
6311 let se_intercept = if use_const && sum_x2_centered > 0.0 && df > 0.0 {
6312 se_y * (sum_x2_raw / (n * sum_x2_centered)).sqrt()
6313 } else {
6314 f64::NAN
6315 };
6316
6317 // F-statistic
6318 let f_stat = if ss_resid > 0.0 && df > 0.0 {
6319 (ss_reg / 1.0) / (ss_resid / df) // MSR / MSE
6320 } else if ss_resid == 0.0 {
6321 f64::INFINITY // Perfect fit
6322 } else {
6323 f64::NAN
6324 };
6325
6326 // Build 5x2 result array
6327 let rows = vec![
6328 vec![LiteralValue::Number(slope), LiteralValue::Number(intercept)],
6329 vec![
6330 LiteralValue::Number(se_slope),
6331 LiteralValue::Number(se_intercept),
6332 ],
6333 vec![LiteralValue::Number(r_squared), LiteralValue::Number(se_y)],
6334 vec![LiteralValue::Number(f_stat), LiteralValue::Number(df)],
6335 vec![LiteralValue::Number(ss_reg), LiteralValue::Number(ss_resid)],
6336 ];
6337
6338 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Array(rows)))
6339 }
6340}
6341
6342/* ─────────────────────────── CONFIDENCE.NORM ──────────────────────────── */
6343
6344/// Returns the half-width of a confidence interval using a normal critical value.
6345///
6346/// `CONFIDENCE.NORM` computes `z_crit * standard_dev / sqrt(size)` for two-sided intervals.
6347///
6348/// # Remarks
6349/// - `alpha` must satisfy `0 < alpha < 1`.
6350/// - `standard_dev` must be greater than `0`.
6351/// - `size` must be at least `1`.
6352/// - Returns `#NUM!` when any input is outside valid bounds.
6353///
6354/// # Examples
6355///
6356/// ```yaml,sandbox
6357/// title: "95% confidence half-width"
6358/// formula: "=CONFIDENCE.NORM(0.05,2,100)"
6359/// expected: 0.3919927977622559
6360/// ```
6361///
6362/// ```yaml,sandbox
6363/// title: "90% confidence half-width"
6364/// formula: "=CONFIDENCE.NORM(0.1,5,25)"
6365/// expected: 1.644853625133699
6366/// ```
6367#[derive(Debug)]
6368pub struct ConfidenceNormFn;
6369/// [formualizer-docgen:schema:start]
6370/// Name: CONFIDENCE.NORM
6371/// Type: ConfidenceNormFn
6372/// Min args: 3
6373/// Max args: 3
6374/// Variadic: false
6375/// Signature: CONFIDENCE.NORM(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
6376/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
6377/// Caps: PURE
6378/// [formualizer-docgen:schema:end]
6379impl Function for ConfidenceNormFn {
6380 func_caps!(PURE);
6381 fn name(&self) -> &'static str {
6382 "CONFIDENCE.NORM"
6383 }
6384 fn aliases(&self) -> &'static [&'static str] {
6385 &["CONFIDENCE"]
6386 }
6387 fn min_args(&self) -> usize {
6388 3
6389 }
6390 fn arg_schema(&self) -> &'static [ArgSchema] {
6391 use std::sync::LazyLock;
6392 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
6393 vec![
6394 ArgSchema::number_lenient_scalar(),
6395 ArgSchema::number_lenient_scalar(),
6396 ArgSchema::number_lenient_scalar(),
6397 ]
6398 });
6399 &SCHEMA[..]
6400 }
6401 fn eval<'a, 'b, 'c>(
6402 &self,
6403 args: &'c [ArgumentHandle<'a, 'b>],
6404 _ctx: &dyn FunctionContext<'b>,
6405 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
6406 let alpha = coerce_num(&scalar_like_value(&args[0])?)?;
6407 let std_dev = coerce_num(&scalar_like_value(&args[1])?)?;
6408 let size = coerce_num(&scalar_like_value(&args[2])?)?;
6409
6410 // Validate inputs
6411 if alpha <= 0.0 || alpha >= 1.0 {
6412 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6413 ExcelError::new_num(),
6414 )));
6415 }
6416 if std_dev <= 0.0 {
6417 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6418 ExcelError::new_num(),
6419 )));
6420 }
6421 if size < 1.0 {
6422 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6423 ExcelError::new_num(),
6424 )));
6425 }
6426
6427 // z_crit = NORM.S.INV(1 - alpha/2)
6428 let z_crit = match std_norm_inv(1.0 - alpha / 2.0) {
6429 Some(z) => z,
6430 None => {
6431 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6432 ExcelError::new_num(),
6433 )));
6434 }
6435 };
6436
6437 let result = z_crit * std_dev / size.sqrt();
6438 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
6439 result,
6440 )))
6441 }
6442}
6443
6444/* ─────────────────────────── CONFIDENCE.T ──────────────────────────── */
6445
6446/// Returns the half-width of a confidence interval using a t critical value.
6447///
6448/// `CONFIDENCE.T` is typically used when population standard deviation is unknown and sample size
6449/// is limited.
6450///
6451/// # Remarks
6452/// - `alpha` must satisfy `0 < alpha < 1`.
6453/// - `standard_dev` must be greater than `0`.
6454/// - `size` must be at least `2` so that `df = size - 1` is valid.
6455/// - Returns `#NUM!` when inputs are outside valid bounds.
6456///
6457/// # Examples
6458///
6459/// ```yaml,sandbox
6460/// title: "95% t-interval half-width"
6461/// formula: "=CONFIDENCE.T(0.05,2,25)"
6462/// expected: 0.8256636934020788
6463/// ```
6464///
6465/// ```yaml,sandbox
6466/// title: "90% t-interval half-width"
6467/// formula: "=CONFIDENCE.T(0.1,5,10)"
6468/// expected: 2.9158049866307585
6469/// ```
6470#[derive(Debug)]
6471pub struct ConfidenceTFn;
6472/// [formualizer-docgen:schema:start]
6473/// Name: CONFIDENCE.T
6474/// Type: ConfidenceTFn
6475/// Min args: 3
6476/// Max args: 3
6477/// Variadic: false
6478/// Signature: CONFIDENCE.T(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
6479/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
6480/// Caps: PURE
6481/// [formualizer-docgen:schema:end]
6482impl Function for ConfidenceTFn {
6483 func_caps!(PURE);
6484 fn name(&self) -> &'static str {
6485 "CONFIDENCE.T"
6486 }
6487 fn min_args(&self) -> usize {
6488 3
6489 }
6490 fn arg_schema(&self) -> &'static [ArgSchema] {
6491 use std::sync::LazyLock;
6492 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
6493 vec![
6494 ArgSchema::number_lenient_scalar(),
6495 ArgSchema::number_lenient_scalar(),
6496 ArgSchema::number_lenient_scalar(),
6497 ]
6498 });
6499 &SCHEMA[..]
6500 }
6501 fn eval<'a, 'b, 'c>(
6502 &self,
6503 args: &'c [ArgumentHandle<'a, 'b>],
6504 _ctx: &dyn FunctionContext<'b>,
6505 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
6506 let alpha = coerce_num(&scalar_like_value(&args[0])?)?;
6507 let std_dev = coerce_num(&scalar_like_value(&args[1])?)?;
6508 let size = coerce_num(&scalar_like_value(&args[2])?)?;
6509
6510 // Validate inputs - size must be >= 2 for t-distribution (df = size - 1 >= 1)
6511 if alpha <= 0.0 || alpha >= 1.0 {
6512 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6513 ExcelError::new_num(),
6514 )));
6515 }
6516 if std_dev <= 0.0 {
6517 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6518 ExcelError::new_num(),
6519 )));
6520 }
6521 if size < 2.0 {
6522 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6523 ExcelError::new_num(),
6524 )));
6525 }
6526
6527 let df = size - 1.0;
6528
6529 // t_crit = T.INV(1 - alpha/2, df)
6530 let t_crit = match t_inv(1.0 - alpha / 2.0, df) {
6531 Some(t) => t,
6532 None => {
6533 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6534 ExcelError::new_num(),
6535 )));
6536 }
6537 };
6538
6539 let result = t_crit * std_dev / size.sqrt();
6540 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
6541 result,
6542 )))
6543 }
6544}
6545
6546/* ─────────────────────────── Z.TEST ──────────────────────────── */
6547
6548/// Returns the one-tailed p-value of a z-test against hypothesized mean `x`.
6549///
6550/// `Z.TEST` evaluates whether the sample mean is significantly greater than the target value.
6551///
6552/// # Remarks
6553/// - Uses provided `sigma` when supplied; otherwise computes population standard deviation.
6554/// - Returns `#NUM!` when `sigma <= 0`.
6555/// - Returns `#DIV/0!` when implied standard deviation is zero.
6556/// - Returns `#N/A` when the data array has no numeric values.
6557///
6558/// # Examples
6559///
6560/// ```yaml,sandbox
6561/// title: "Z-test with provided sigma"
6562/// formula: "=Z.TEST({1,2,3,4,5},2,1)"
6563/// expected: 0.012673659338734137
6564/// ```
6565///
6566/// ```yaml,sandbox
6567/// title: "Z-test with sigma estimated from sample"
6568/// formula: "=Z.TEST({1,2,3,4,5},2)"
6569/// expected: 0.056923149003329065
6570/// ```
6571#[derive(Debug)]
6572pub struct ZTestFn;
6573/// [formualizer-docgen:schema:start]
6574/// Name: Z.TEST
6575/// Type: ZTestFn
6576/// Min args: 2
6577/// Max args: variadic
6578/// Variadic: true
6579/// Signature: Z.TEST(arg1: number@range, arg2: number@scalar, arg3...: number@scalar)
6580/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
6581/// Caps: PURE
6582/// [formualizer-docgen:schema:end]
6583impl Function for ZTestFn {
6584 func_caps!(PURE);
6585 fn name(&self) -> &'static str {
6586 "Z.TEST"
6587 }
6588 fn aliases(&self) -> &'static [&'static str] {
6589 &["ZTEST"]
6590 }
6591 fn min_args(&self) -> usize {
6592 2
6593 }
6594 fn variadic(&self) -> bool {
6595 true
6596 }
6597 fn arg_schema(&self) -> &'static [ArgSchema] {
6598 use std::sync::LazyLock;
6599 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
6600 vec![
6601 {
6602 let mut s = ArgSchema::number_lenient_scalar();
6603 s.shape = crate::args::ShapeKind::Range;
6604 s.coercion = formualizer_common::CoercionPolicy::NumberLenientText;
6605 s
6606 },
6607 ArgSchema::number_lenient_scalar(),
6608 ArgSchema::number_lenient_scalar(), // optional sigma
6609 ]
6610 });
6611 &SCHEMA[..]
6612 }
6613 fn eval<'a, 'b, 'c>(
6614 &self,
6615 args: &'c [ArgumentHandle<'a, 'b>],
6616 _ctx: &dyn FunctionContext<'b>,
6617 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
6618 // Collect numeric values from the array argument
6619 let data = collect_numeric_stats(&args[0..1])?;
6620
6621 if data.is_empty() {
6622 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6623 ExcelError::new_na(),
6624 )));
6625 }
6626
6627 let x = coerce_num(&scalar_like_value(&args[1])?)?;
6628
6629 let n = data.len() as f64;
6630 let mean: f64 = data.iter().sum::<f64>() / n;
6631
6632 // Calculate sigma: use provided value or compute population std dev
6633 let sigma = if args.len() > 2 {
6634 let s = coerce_num(&scalar_like_value(&args[2])?)?;
6635 if s <= 0.0 {
6636 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6637 ExcelError::new_num(),
6638 )));
6639 }
6640 s
6641 } else {
6642 // Population standard deviation
6643 let variance: f64 = data.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / n;
6644 let std_dev = variance.sqrt();
6645 if std_dev == 0.0 {
6646 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6647 ExcelError::new_div(),
6648 )));
6649 }
6650 std_dev
6651 };
6652
6653 // z = (mean - x) / (sigma / sqrt(n))
6654 let z = (mean - x) / (sigma / n.sqrt());
6655
6656 // P-value = 1 - NORM.S.DIST(z, TRUE)
6657 let p_value = 1.0 - std_norm_cdf(z);
6658
6659 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
6660 p_value,
6661 )))
6662 }
6663}
6664
6665/* ─────────────────────────── TREND ──────────────────────────── */
6666
6667/// Returns fitted y-values along a linear trend derived from known data.
6668///
6669/// `TREND` performs simple linear regression and returns predictions for `new_x` (or defaults).
6670///
6671/// # Remarks
6672/// - `known_y` is required; `known_x` defaults to `1..n` when omitted.
6673/// - `new_x` defaults to `known_x` when omitted.
6674/// - `const` defaults to `TRUE`; set to `FALSE` to force a zero intercept.
6675/// - Returns spreadsheet errors for empty data, mismatched lengths, or degenerate x-variance.
6676///
6677/// # Examples
6678///
6679/// ```yaml,sandbox
6680/// title: "Predict two future points on a line"
6681/// formula: "=TREND({2,4,6},{1,2,3},{4,5})"
6682/// expected:
6683/// - [8, 10]
6684/// ```
6685///
6686/// ```yaml,sandbox
6687/// title: "Default x-values with fitted trend"
6688/// formula: "=TREND({3,5,7})"
6689/// expected:
6690/// - [3, 5, 7]
6691/// ```
6692#[derive(Debug)]
6693pub struct TrendFn;
6694/// [formualizer-docgen:schema:start]
6695/// Name: TREND
6696/// Type: TrendFn
6697/// Min args: 1
6698/// Max args: variadic
6699/// Variadic: true
6700/// Signature: TREND(arg1...: number@range)
6701/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
6702/// Caps: PURE, NUMERIC_ONLY
6703/// [formualizer-docgen:schema:end]
6704impl Function for TrendFn {
6705 func_caps!(PURE, NUMERIC_ONLY);
6706 fn name(&self) -> &'static str {
6707 "TREND"
6708 }
6709 fn min_args(&self) -> usize {
6710 1
6711 }
6712 fn variadic(&self) -> bool {
6713 true
6714 }
6715 fn arg_schema(&self) -> &'static [ArgSchema] {
6716 &ARG_RANGE_NUM_LENIENT_ONE[..]
6717 }
6718 fn eval<'a, 'b, 'c>(
6719 &self,
6720 args: &'c [ArgumentHandle<'a, 'b>],
6721 _ctx: &dyn FunctionContext<'b>,
6722 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
6723 // TREND: args[0] = known_y's (required)
6724 // args[1] = known_x's (optional, defaults to {1,2,3,...})
6725 // args[2] = new_x's (optional, defaults to known_x's)
6726 // args[3] = const (optional, default TRUE - whether to compute intercept)
6727
6728 let y_vals = collect_numeric_stats(&args[0..1])?;
6729
6730 if y_vals.is_empty() {
6731 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6732 ExcelError::new_na(),
6733 )));
6734 }
6735
6736 // Helper to check if argument is empty/omitted
6737 // Note: Empty arguments are represented as empty text strings by the parser
6738 fn is_arg_empty(arg: &ArgumentHandle) -> bool {
6739 match scalar_like_value(arg) {
6740 Ok(LiteralValue::Empty) => true,
6741 Ok(LiteralValue::Text(s)) if s.is_empty() => true,
6742 _ => false,
6743 }
6744 }
6745
6746 // Get known_x's or generate default {1, 2, 3, ...}
6747 let x_vals = if args.len() >= 2 && !is_arg_empty(&args[1]) {
6748 collect_numeric_stats(&args[1..2])?
6749 } else {
6750 (1..=y_vals.len()).map(|i| i as f64).collect()
6751 };
6752
6753 // Arrays must have same length
6754 if y_vals.len() != x_vals.len() {
6755 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6756 ExcelError::new_ref(),
6757 )));
6758 }
6759
6760 // Get new_x's or use known_x's - check if argument is empty/omitted
6761 let new_x_vals = if args.len() >= 3 && !is_arg_empty(&args[2]) {
6762 collect_numeric_stats(&args[2..3])?
6763 } else {
6764 x_vals.clone()
6765 };
6766
6767 if new_x_vals.is_empty() {
6768 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6769 ExcelError::new_na(),
6770 )));
6771 }
6772
6773 // Parse const argument (default TRUE)
6774 let use_const = if args.len() >= 4 {
6775 match scalar_like_value(&args[3])? {
6776 LiteralValue::Boolean(b) => b,
6777 LiteralValue::Number(n) => n != 0.0,
6778 LiteralValue::Int(i) => i != 0,
6779 LiteralValue::Empty => true, // empty defaults to TRUE
6780 _ => true,
6781 }
6782 } else {
6783 true
6784 };
6785
6786 let n = x_vals.len() as f64;
6787
6788 // Calculate regression coefficients
6789 let (slope, intercept) = if use_const {
6790 // Normal linear regression with intercept
6791 let mean_x = x_vals.iter().sum::<f64>() / n;
6792 let mean_y = y_vals.iter().sum::<f64>() / n;
6793
6794 let mut sum_xy = 0.0;
6795 let mut sum_x2 = 0.0;
6796
6797 for i in 0..x_vals.len() {
6798 let dx = x_vals[i] - mean_x;
6799 let dy = y_vals[i] - mean_y;
6800 sum_xy += dx * dy;
6801 sum_x2 += dx * dx;
6802 }
6803
6804 if sum_x2 == 0.0 {
6805 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6806 ExcelError::new_div(),
6807 )));
6808 }
6809
6810 let slope = sum_xy / sum_x2;
6811 let intercept = mean_y - slope * mean_x;
6812 (slope, intercept)
6813 } else {
6814 // Regression through origin (intercept = 0)
6815 let mut sum_xy = 0.0;
6816 let mut sum_x2 = 0.0;
6817
6818 for i in 0..x_vals.len() {
6819 sum_xy += x_vals[i] * y_vals[i];
6820 sum_x2 += x_vals[i] * x_vals[i];
6821 }
6822
6823 if sum_x2 == 0.0 {
6824 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6825 ExcelError::new_div(),
6826 )));
6827 }
6828
6829 let slope = sum_xy / sum_x2;
6830 (slope, 0.0)
6831 };
6832
6833 // Calculate predicted y values for new_x's
6834 let predicted: Vec<LiteralValue> = new_x_vals
6835 .iter()
6836 .map(|&x| LiteralValue::Number(slope * x + intercept))
6837 .collect();
6838
6839 // Return as 1xN array (row vector)
6840 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Array(vec![
6841 predicted,
6842 ])))
6843 }
6844}
6845
6846/* ─────────────────────────── GROWTH ──────────────────────────── */
6847
6848/// Returns fitted values from an exponential trend model.
6849///
6850/// `GROWTH` fits `y = b * m^x` by linearizing in log space, then returns predictions for `new_x`.
6851///
6852/// # Remarks
6853/// - All known y-values must be strictly greater than `0`.
6854/// - `known_x` defaults to `1..n`; `new_x` defaults to `known_x`.
6855/// - `const` defaults to `TRUE`; set to `FALSE` to force `b = 1`.
6856/// - Returns spreadsheet errors for invalid domains, mismatched lengths, or degenerate x-variance.
6857///
6858/// # Examples
6859///
6860/// ```yaml,sandbox
6861/// title: "Exponential growth forecast"
6862/// formula: "=GROWTH({2,4,8},{1,2,3},{4,5})"
6863/// expected:
6864/// - [16, 32]
6865/// ```
6866///
6867/// ```yaml,sandbox
6868/// title: "Default x-values with perfect doubling pattern"
6869/// formula: "=GROWTH({3,6,12})"
6870/// expected:
6871/// - [3, 6, 12]
6872/// ```
6873#[derive(Debug)]
6874pub struct GrowthFn;
6875/// [formualizer-docgen:schema:start]
6876/// Name: GROWTH
6877/// Type: GrowthFn
6878/// Min args: 1
6879/// Max args: variadic
6880/// Variadic: true
6881/// Signature: GROWTH(arg1...: number@range)
6882/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
6883/// Caps: PURE, NUMERIC_ONLY
6884/// [formualizer-docgen:schema:end]
6885impl Function for GrowthFn {
6886 func_caps!(PURE, NUMERIC_ONLY);
6887 fn name(&self) -> &'static str {
6888 "GROWTH"
6889 }
6890 fn min_args(&self) -> usize {
6891 1
6892 }
6893 fn variadic(&self) -> bool {
6894 true
6895 }
6896 fn arg_schema(&self) -> &'static [ArgSchema] {
6897 &ARG_RANGE_NUM_LENIENT_ONE[..]
6898 }
6899 fn eval<'a, 'b, 'c>(
6900 &self,
6901 args: &'c [ArgumentHandle<'a, 'b>],
6902 _ctx: &dyn FunctionContext<'b>,
6903 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
6904 // GROWTH: args[0] = known_y's (required)
6905 // args[1] = known_x's (optional, defaults to {1,2,3,...})
6906 // args[2] = new_x's (optional, defaults to known_x's)
6907 // args[3] = const (optional, default TRUE - whether to compute intercept)
6908
6909 let y_vals = collect_numeric_stats(&args[0..1])?;
6910
6911 if y_vals.is_empty() {
6912 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6913 ExcelError::new_na(),
6914 )));
6915 }
6916
6917 // Check that all y values are positive (required for log transformation)
6918 for &y in &y_vals {
6919 if y <= 0.0 {
6920 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6921 ExcelError::new_num(),
6922 )));
6923 }
6924 }
6925
6926 // Helper to check if argument is empty/omitted
6927 // Note: Empty arguments are represented as empty text strings by the parser
6928 fn is_arg_empty(arg: &ArgumentHandle) -> bool {
6929 match scalar_like_value(arg) {
6930 Ok(LiteralValue::Empty) => true,
6931 Ok(LiteralValue::Text(s)) if s.is_empty() => true,
6932 _ => false,
6933 }
6934 }
6935
6936 // Get known_x's or generate default {1, 2, 3, ...}
6937 let x_vals = if args.len() >= 2 && !is_arg_empty(&args[1]) {
6938 collect_numeric_stats(&args[1..2])?
6939 } else {
6940 (1..=y_vals.len()).map(|i| i as f64).collect()
6941 };
6942
6943 // Arrays must have same length
6944 if y_vals.len() != x_vals.len() {
6945 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6946 ExcelError::new_ref(),
6947 )));
6948 }
6949
6950 // Get new_x's or use known_x's - check if argument is empty/omitted
6951 let new_x_vals = if args.len() >= 3 && !is_arg_empty(&args[2]) {
6952 collect_numeric_stats(&args[2..3])?
6953 } else {
6954 x_vals.clone()
6955 };
6956
6957 if new_x_vals.is_empty() {
6958 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6959 ExcelError::new_na(),
6960 )));
6961 }
6962
6963 // Parse const argument (default TRUE)
6964 let use_const = if args.len() >= 4 {
6965 match scalar_like_value(&args[3])? {
6966 LiteralValue::Boolean(b) => b,
6967 LiteralValue::Number(n) => n != 0.0,
6968 LiteralValue::Int(i) => i != 0,
6969 LiteralValue::Empty => true, // empty defaults to TRUE
6970 _ => true,
6971 }
6972 } else {
6973 true
6974 };
6975
6976 // Transform to log space: ln(y) = ln(b) + x*ln(m)
6977 // This is linear regression on log-transformed y values
6978 let ln_y_vals: Vec<f64> = y_vals.iter().map(|&y| y.ln()).collect();
6979
6980 let n = x_vals.len() as f64;
6981
6982 // Calculate regression coefficients in log space
6983 let (ln_m, ln_b) = if use_const {
6984 // Normal linear regression with intercept
6985 let mean_x = x_vals.iter().sum::<f64>() / n;
6986 let mean_ln_y = ln_y_vals.iter().sum::<f64>() / n;
6987
6988 let mut sum_xy = 0.0;
6989 let mut sum_x2 = 0.0;
6990
6991 for i in 0..x_vals.len() {
6992 let dx = x_vals[i] - mean_x;
6993 let dy = ln_y_vals[i] - mean_ln_y;
6994 sum_xy += dx * dy;
6995 sum_x2 += dx * dx;
6996 }
6997
6998 if sum_x2 == 0.0 {
6999 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7000 ExcelError::new_div(),
7001 )));
7002 }
7003
7004 let ln_m = sum_xy / sum_x2;
7005 let ln_b = mean_ln_y - ln_m * mean_x;
7006 (ln_m, ln_b)
7007 } else {
7008 // Regression through origin in log space (ln_b = 0, so b = 1)
7009 let mut sum_xy = 0.0;
7010 let mut sum_x2 = 0.0;
7011
7012 for i in 0..x_vals.len() {
7013 sum_xy += x_vals[i] * ln_y_vals[i];
7014 sum_x2 += x_vals[i] * x_vals[i];
7015 }
7016
7017 if sum_x2 == 0.0 {
7018 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7019 ExcelError::new_div(),
7020 )));
7021 }
7022
7023 let ln_m = sum_xy / sum_x2;
7024 (ln_m, 0.0)
7025 };
7026
7027 // Convert back from log space: m = e^ln_m, b = e^ln_b
7028 let m = ln_m.exp();
7029 let b = ln_b.exp();
7030
7031 // Calculate predicted y values: y = b * m^x
7032 let predicted: Vec<LiteralValue> = new_x_vals
7033 .iter()
7034 .map(|&x| LiteralValue::Number(b * m.powf(x)))
7035 .collect();
7036
7037 // Return as 1xN array (row vector)
7038 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Array(vec![
7039 predicted,
7040 ])))
7041 }
7042}
7043
7044/* ─────────────────────────── LOGEST ──────────────────────────── */
7045
7046/// Returns parameters for an exponential model fitted to known data.
7047///
7048/// `LOGEST` fits `y = b * m^x` and returns either `[m, b]` or an expanded statistics matrix.
7049///
7050/// # Remarks
7051/// - All known y-values must be strictly greater than `0`.
7052/// - `known_x` defaults to `1..n` when omitted.
7053/// - `const` controls whether `b` is fitted (`TRUE` by default).
7054/// - `stats=TRUE` returns a `5x2` statistics block; otherwise returns `1x2`.
7055///
7056/// # Examples
7057///
7058/// ```yaml,sandbox
7059/// title: "Exponential base and intercept"
7060/// formula: "=LOGEST({2,4,8},{1,2,3})"
7061/// expected:
7062/// - [2, 1]
7063/// ```
7064///
7065/// ```yaml,sandbox
7066/// title: "Alternative growth series"
7067/// formula: "=LOGEST({3,6,12},{1,2,3})"
7068/// expected:
7069/// - [2, 1.5]
7070/// ```
7071#[derive(Debug)]
7072pub struct LogestFn;
7073/// [formualizer-docgen:schema:start]
7074/// Name: LOGEST
7075/// Type: LogestFn
7076/// Min args: 1
7077/// Max args: variadic
7078/// Variadic: true
7079/// Signature: LOGEST(arg1...: number@range)
7080/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
7081/// Caps: PURE, NUMERIC_ONLY
7082/// [formualizer-docgen:schema:end]
7083impl Function for LogestFn {
7084 func_caps!(PURE, NUMERIC_ONLY);
7085 fn name(&self) -> &'static str {
7086 "LOGEST"
7087 }
7088 fn min_args(&self) -> usize {
7089 1
7090 }
7091 fn variadic(&self) -> bool {
7092 true
7093 }
7094 fn arg_schema(&self) -> &'static [ArgSchema] {
7095 &ARG_RANGE_NUM_LENIENT_ONE[..]
7096 }
7097 fn eval<'a, 'b, 'c>(
7098 &self,
7099 args: &'c [ArgumentHandle<'a, 'b>],
7100 _ctx: &dyn FunctionContext<'b>,
7101 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
7102 // args[0] = known_y's (required)
7103 // args[1] = known_x's (optional, defaults to {1,2,3,...})
7104 // args[2] = const (optional, default TRUE - whether to compute b)
7105 // args[3] = stats (optional, default FALSE - whether to return additional statistics)
7106
7107 let y_vals = collect_numeric_stats(&args[0..1])?;
7108
7109 if y_vals.is_empty() {
7110 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7111 ExcelError::new_na(),
7112 )));
7113 }
7114
7115 // Check that all y values are positive (required for log transformation)
7116 for &y in &y_vals {
7117 if y <= 0.0 {
7118 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7119 ExcelError::new_num(),
7120 )));
7121 }
7122 }
7123
7124 // Get known_x's or generate default {1, 2, 3, ...}
7125 let x_vals = if args.len() >= 2 {
7126 collect_numeric_stats(&args[1..2])?
7127 } else {
7128 (1..=y_vals.len()).map(|i| i as f64).collect()
7129 };
7130
7131 // Arrays must have same length
7132 if y_vals.len() != x_vals.len() {
7133 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7134 ExcelError::new_ref(),
7135 )));
7136 }
7137
7138 // Parse const argument (default TRUE)
7139 let use_const = if args.len() >= 3 {
7140 match scalar_like_value(&args[2])? {
7141 LiteralValue::Boolean(b) => b,
7142 LiteralValue::Number(n) => n != 0.0,
7143 LiteralValue::Int(i) => i != 0,
7144 _ => true,
7145 }
7146 } else {
7147 true
7148 };
7149
7150 // Parse stats argument (default FALSE)
7151 let return_stats = if args.len() >= 4 {
7152 match scalar_like_value(&args[3])? {
7153 LiteralValue::Boolean(b) => b,
7154 LiteralValue::Number(n) => n != 0.0,
7155 LiteralValue::Int(i) => i != 0,
7156 _ => false,
7157 }
7158 } else {
7159 false
7160 };
7161
7162 // Transform to log space: ln(y) = ln(b) + x*ln(m)
7163 let ln_y_vals: Vec<f64> = y_vals.iter().map(|&y| y.ln()).collect();
7164
7165 let n = x_vals.len() as f64;
7166
7167 // Calculate regression coefficients in log space
7168 let (ln_m, ln_b) = if use_const {
7169 // Normal linear regression with intercept
7170 let mean_x = x_vals.iter().sum::<f64>() / n;
7171 let mean_ln_y = ln_y_vals.iter().sum::<f64>() / n;
7172
7173 let mut sum_xy = 0.0;
7174 let mut sum_x2 = 0.0;
7175
7176 for i in 0..x_vals.len() {
7177 let dx = x_vals[i] - mean_x;
7178 let dy = ln_y_vals[i] - mean_ln_y;
7179 sum_xy += dx * dy;
7180 sum_x2 += dx * dx;
7181 }
7182
7183 if sum_x2 == 0.0 {
7184 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7185 ExcelError::new_div(),
7186 )));
7187 }
7188
7189 let ln_m = sum_xy / sum_x2;
7190 let ln_b = mean_ln_y - ln_m * mean_x;
7191 (ln_m, ln_b)
7192 } else {
7193 // Regression through origin in log space (ln_b = 0, so b = 1)
7194 let mut sum_xy = 0.0;
7195 let mut sum_x2 = 0.0;
7196
7197 for i in 0..x_vals.len() {
7198 sum_xy += x_vals[i] * ln_y_vals[i];
7199 sum_x2 += x_vals[i] * x_vals[i];
7200 }
7201
7202 if sum_x2 == 0.0 {
7203 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7204 ExcelError::new_div(),
7205 )));
7206 }
7207
7208 let ln_m = sum_xy / sum_x2;
7209 (ln_m, 0.0)
7210 };
7211
7212 // Convert from log space to get m and b
7213 let m = ln_m.exp();
7214 let b = ln_b.exp();
7215
7216 if !return_stats {
7217 // Return just m and b as 1x2 array: [[m, b]]
7218 let row = vec![LiteralValue::Number(m), LiteralValue::Number(b)];
7219 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Array(vec![
7220 row,
7221 ])));
7222 }
7223
7224 // Calculate additional statistics for stats=TRUE
7225 // Statistics are computed in log space, then converted
7226 // Row 1: [m, b]
7227 // Row 2: [se_m, se_b] - standard errors (converted from log space)
7228 // Row 3: [r_squared, se_y] - R-squared and standard error of y estimate
7229 // Row 4: [F_statistic, df] - F-statistic and degrees of freedom
7230 // Row 5: [ss_reg, ss_resid] - regression sum of squares and residual sum of squares
7231
7232 let mean_ln_y = ln_y_vals.iter().sum::<f64>() / n;
7233
7234 // Calculate residuals and sums of squares in log space
7235 let mut ss_resid = 0.0;
7236 let mut ss_tot = 0.0;
7237
7238 for i in 0..x_vals.len() {
7239 let ln_y_pred = ln_m * x_vals[i] + ln_b;
7240 let residual = ln_y_vals[i] - ln_y_pred;
7241 ss_resid += residual * residual;
7242 let dy_tot = ln_y_vals[i] - mean_ln_y;
7243 ss_tot += dy_tot * dy_tot;
7244 }
7245
7246 let ss_reg = ss_tot - ss_resid;
7247
7248 // R-squared (same in both spaces for transformed regression)
7249 let r_squared = if ss_tot == 0.0 {
7250 1.0
7251 } else {
7252 1.0 - (ss_resid / ss_tot)
7253 };
7254
7255 // Degrees of freedom
7256 let df = if use_const {
7257 (n as i64 - 2).max(1) as f64
7258 } else {
7259 (n as i64 - 1).max(1) as f64
7260 };
7261
7262 // Standard error of y estimate (in log space)
7263 let se_ln_y = if df > 0.0 {
7264 (ss_resid / df).sqrt()
7265 } else {
7266 0.0
7267 };
7268
7269 // Standard errors of coefficients in log space
7270 let mean_x = x_vals.iter().sum::<f64>() / n;
7271 let mut sum_x2_centered = 0.0;
7272 let mut sum_x2_raw = 0.0;
7273 for &xi in &x_vals {
7274 sum_x2_centered += (xi - mean_x).powi(2);
7275 sum_x2_raw += xi * xi;
7276 }
7277
7278 let se_ln_m = if sum_x2_centered > 0.0 && df > 0.0 {
7279 se_ln_y / sum_x2_centered.sqrt()
7280 } else {
7281 f64::NAN
7282 };
7283
7284 let se_ln_b = if use_const && sum_x2_centered > 0.0 && df > 0.0 {
7285 se_ln_y * (sum_x2_raw / (n * sum_x2_centered)).sqrt()
7286 } else {
7287 f64::NAN
7288 };
7289
7290 // Convert standard errors: se_m = m * se_ln_m (delta method approximation)
7291 let se_m = m * se_ln_m;
7292 let se_b = b * se_ln_b;
7293
7294 // Standard error of y estimate - convert from log space
7295 // This is an approximation; for exponential models, se_y in original space varies with x
7296 let se_y = se_ln_y;
7297
7298 // F-statistic
7299 let f_stat = if ss_resid > 0.0 && df > 0.0 {
7300 (ss_reg / 1.0) / (ss_resid / df)
7301 } else if ss_resid == 0.0 {
7302 f64::INFINITY
7303 } else {
7304 f64::NAN
7305 };
7306
7307 // Build 5x2 result array
7308 let rows = vec![
7309 vec![LiteralValue::Number(m), LiteralValue::Number(b)],
7310 vec![LiteralValue::Number(se_m), LiteralValue::Number(se_b)],
7311 vec![LiteralValue::Number(r_squared), LiteralValue::Number(se_y)],
7312 vec![LiteralValue::Number(f_stat), LiteralValue::Number(df)],
7313 vec![LiteralValue::Number(ss_reg), LiteralValue::Number(ss_resid)],
7314 ];
7315
7316 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Array(rows)))
7317 }
7318}
7319
7320/* ─────────────────────────── PERCENTRANK ──────────────────────────── */
7321
7322/// Returns the inclusive percentile rank of `x` within a numeric data array.
7323///
7324/// `PERCENTRANK.INC` maps values to `[0, 1]` and interpolates linearly between data points.
7325///
7326/// # Remarks
7327/// - `x` must be within the observed min/max range; otherwise returns `#N/A`.
7328/// - Optional `significance` controls decimal truncation and defaults to `3`.
7329/// - `significance` must be at least `1`.
7330/// - Returns `#NUM!` for invalid setup such as empty numeric input.
7331///
7332/// # Examples
7333///
7334/// ```yaml,sandbox
7335/// title: "Exact inclusive percentile rank"
7336/// formula: "=PERCENTRANK.INC({1,2,3,4,5},3)"
7337/// expected: 0.5
7338/// ```
7339///
7340/// ```yaml,sandbox
7341/// title: "Interpolated inclusive percentile rank"
7342/// formula: "=PERCENTRANK.INC({1,2,3,4,5},2.5)"
7343/// expected: 0.375
7344/// ```
7345#[derive(Debug)]
7346pub struct PercentRankIncFn;
7347/// [formualizer-docgen:schema:start]
7348/// Name: PERCENTRANK.INC
7349/// Type: PercentRankIncFn
7350/// Min args: 2
7351/// Max args: variadic
7352/// Variadic: true
7353/// Signature: PERCENTRANK.INC(arg1...: number@range)
7354/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
7355/// Caps: PURE, NUMERIC_ONLY
7356/// [formualizer-docgen:schema:end]
7357impl Function for PercentRankIncFn {
7358 func_caps!(PURE, NUMERIC_ONLY);
7359 fn name(&self) -> &'static str {
7360 "PERCENTRANK.INC"
7361 }
7362 fn aliases(&self) -> &'static [&'static str] {
7363 &["PERCENTRANK"]
7364 }
7365 fn min_args(&self) -> usize {
7366 2
7367 }
7368 fn variadic(&self) -> bool {
7369 true
7370 }
7371 fn arg_schema(&self) -> &'static [ArgSchema] {
7372 &ARG_RANGE_NUM_LENIENT_ONE[..]
7373 }
7374 fn eval<'a, 'b, 'c>(
7375 &self,
7376 args: &'c [ArgumentHandle<'a, 'b>],
7377 _ctx: &dyn FunctionContext<'b>,
7378 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
7379 if args.len() < 2 {
7380 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7381 ExcelError::new_num(),
7382 )));
7383 }
7384
7385 // Get x value (the value to find the rank of)
7386 let x = match coerce_num(&scalar_like_value(&args[1])?) {
7387 Ok(n) => n,
7388 Err(_) => {
7389 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7390 ExcelError::new_num(),
7391 )));
7392 }
7393 };
7394
7395 // Get optional significance (default 3)
7396 let significance = if args.len() > 2 {
7397 match coerce_num(&scalar_like_value(&args[2])?) {
7398 Ok(n) => {
7399 let s = n as i32;
7400 if s < 1 {
7401 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7402 ExcelError::new_num(),
7403 )));
7404 }
7405 s as u32
7406 }
7407 Err(_) => 3,
7408 }
7409 } else {
7410 3
7411 };
7412
7413 // Collect and sort the data array
7414 let mut nums = collect_numeric_stats(&args[0..1])?;
7415 if nums.is_empty() {
7416 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7417 ExcelError::new_num(),
7418 )));
7419 }
7420 nums.sort_by(|a, b| a.partial_cmp(b).unwrap());
7421
7422 let n = nums.len();
7423
7424 // Check if x is outside the range
7425 if x < nums[0] || x > nums[n - 1] {
7426 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7427 ExcelError::new_na(),
7428 )));
7429 }
7430
7431 // Find the rank using linear interpolation
7432 // For PERCENTRANK.INC, the formula is: rank = (position) / (n-1)
7433 // where position is 0-based and uses linear interpolation
7434 let rank = if n == 1 {
7435 // Single element - rank is 0 (or 1.0 if we want, but Excel returns 0)
7436 0.0
7437 } else {
7438 let mut rank_val = 0.0;
7439 for i in 0..n - 1 {
7440 if (nums[i] - x).abs() < 1e-12 {
7441 // Exact match at position i
7442 rank_val = (i as f64) / ((n - 1) as f64);
7443 break;
7444 } else if nums[i] < x && x < nums[i + 1] {
7445 // Interpolate between positions i and i+1
7446 let frac = (x - nums[i]) / (nums[i + 1] - nums[i]);
7447 rank_val = ((i as f64) + frac) / ((n - 1) as f64);
7448 break;
7449 } else if i == n - 2 && (nums[n - 1] - x).abs() < 1e-12 {
7450 // Exact match at last position
7451 rank_val = 1.0;
7452 }
7453 }
7454 rank_val
7455 };
7456
7457 // Truncate to significance decimal places
7458 let multiplier = 10_f64.powi(significance as i32);
7459 let truncated = (rank * multiplier).trunc() / multiplier;
7460
7461 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
7462 truncated,
7463 )))
7464 }
7465}
7466
7467/// Returns the exclusive percentile rank of `x` within a numeric data array.
7468///
7469/// `PERCENTRANK.EXC` uses an open ranking scale that excludes exact `0` and `1` endpoints.
7470///
7471/// # Remarks
7472/// - `x` must lie within the observed min/max range; otherwise returns `#N/A`.
7473/// - Output is based on position divided by `n + 1`, with interpolation between points.
7474/// - Optional `significance` defaults to `3` and must be at least `1`.
7475/// - Returns `#NUM!` for invalid setup such as empty numeric input.
7476///
7477/// # Examples
7478///
7479/// ```yaml,sandbox
7480/// title: "Exact exclusive percentile rank"
7481/// formula: "=PERCENTRANK.EXC({1,2,3,4,5},3)"
7482/// expected: 0.5
7483/// ```
7484///
7485/// ```yaml,sandbox
7486/// title: "Interpolated exclusive percentile rank"
7487/// formula: "=PERCENTRANK.EXC({1,2,3,4,5},2.5)"
7488/// expected: 0.416
7489/// ```
7490#[derive(Debug)]
7491pub struct PercentRankExcFn;
7492/// [formualizer-docgen:schema:start]
7493/// Name: PERCENTRANK.EXC
7494/// Type: PercentRankExcFn
7495/// Min args: 2
7496/// Max args: variadic
7497/// Variadic: true
7498/// Signature: PERCENTRANK.EXC(arg1...: number@range)
7499/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
7500/// Caps: PURE, NUMERIC_ONLY
7501/// [formualizer-docgen:schema:end]
7502impl Function for PercentRankExcFn {
7503 func_caps!(PURE, NUMERIC_ONLY);
7504 fn name(&self) -> &'static str {
7505 "PERCENTRANK.EXC"
7506 }
7507 fn min_args(&self) -> usize {
7508 2
7509 }
7510 fn variadic(&self) -> bool {
7511 true
7512 }
7513 fn arg_schema(&self) -> &'static [ArgSchema] {
7514 &ARG_RANGE_NUM_LENIENT_ONE[..]
7515 }
7516 fn eval<'a, 'b, 'c>(
7517 &self,
7518 args: &'c [ArgumentHandle<'a, 'b>],
7519 _ctx: &dyn FunctionContext<'b>,
7520 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
7521 if args.len() < 2 {
7522 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7523 ExcelError::new_num(),
7524 )));
7525 }
7526
7527 // Get x value (the value to find the rank of)
7528 let x = match coerce_num(&scalar_like_value(&args[1])?) {
7529 Ok(n) => n,
7530 Err(_) => {
7531 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7532 ExcelError::new_num(),
7533 )));
7534 }
7535 };
7536
7537 // Get optional significance (default 3)
7538 let significance = if args.len() > 2 {
7539 match coerce_num(&scalar_like_value(&args[2])?) {
7540 Ok(n) => {
7541 let s = n as i32;
7542 if s < 1 {
7543 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7544 ExcelError::new_num(),
7545 )));
7546 }
7547 s as u32
7548 }
7549 Err(_) => 3,
7550 }
7551 } else {
7552 3
7553 };
7554
7555 // Collect and sort the data array
7556 let mut nums = collect_numeric_stats(&args[0..1])?;
7557 if nums.is_empty() {
7558 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7559 ExcelError::new_num(),
7560 )));
7561 }
7562 nums.sort_by(|a, b| a.partial_cmp(b).unwrap());
7563
7564 let n = nums.len();
7565
7566 // Check if x is outside the range
7567 if x < nums[0] || x > nums[n - 1] {
7568 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7569 ExcelError::new_na(),
7570 )));
7571 }
7572
7573 // For PERCENTRANK.EXC, the formula is: rank = position / (n+1)
7574 // where position is 1-based and uses linear interpolation
7575 let rank = {
7576 let mut rank_val = 0.0;
7577 for i in 0..n {
7578 if (nums[i] - x).abs() < 1e-12 {
7579 // Exact match at position i (1-based: i+1)
7580 rank_val = ((i + 1) as f64) / ((n + 1) as f64);
7581 break;
7582 } else if i < n - 1 && nums[i] < x && x < nums[i + 1] {
7583 // Interpolate between positions i and i+1 (1-based: i+1 and i+2)
7584 let frac = (x - nums[i]) / (nums[i + 1] - nums[i]);
7585 let position = ((i + 1) as f64) + frac;
7586 rank_val = position / ((n + 1) as f64);
7587 break;
7588 }
7589 }
7590 rank_val
7591 };
7592
7593 // Truncate to significance decimal places
7594 let multiplier = 10_f64.powi(significance as i32);
7595 let truncated = (rank * multiplier).trunc() / multiplier;
7596
7597 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
7598 truncated,
7599 )))
7600 }
7601}
7602
7603/* ─────────────────────────── FREQUENCY ──────────────────────────── */
7604
7605/// Returns a vertical frequency distribution for numeric data across bin cutoffs.
7606///
7607/// `FREQUENCY` counts values into `<= first bin`, intermediate right-closed bins, and an overflow
7608/// bucket above the final bin.
7609///
7610/// # Remarks
7611/// - Returns an array with `bins + 1` rows.
7612/// - Bins are sorted before counting.
7613/// - If `bins_array` has no numeric values, result is a single count of all data points.
7614/// - Non-numeric values in input ranges are ignored by statistical-collection rules.
7615///
7616/// # Examples
7617///
7618/// ```yaml,sandbox
7619/// title: "Frequency buckets with two bins"
7620/// formula: "=FREQUENCY({1,2,3,4,5},{2,4})"
7621/// expected:
7622/// - [2]
7623/// - [2]
7624/// - [1]
7625/// ```
7626///
7627/// ```yaml,sandbox
7628/// title: "Frequency with repeated values"
7629/// formula: "=FREQUENCY({1,1,2,2,3},{1,2})"
7630/// expected:
7631/// - [2]
7632/// - [2]
7633/// - [1]
7634/// ```
7635#[derive(Debug)]
7636pub struct FrequencyFn;
7637/// [formualizer-docgen:schema:start]
7638/// Name: FREQUENCY
7639/// Type: FrequencyFn
7640/// Min args: 2
7641/// Max args: 1
7642/// Variadic: false
7643/// Signature: FREQUENCY(arg1: number@range)
7644/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
7645/// Caps: PURE, NUMERIC_ONLY
7646/// [formualizer-docgen:schema:end]
7647impl Function for FrequencyFn {
7648 func_caps!(PURE, NUMERIC_ONLY);
7649 fn name(&self) -> &'static str {
7650 "FREQUENCY"
7651 }
7652 fn min_args(&self) -> usize {
7653 2
7654 }
7655 fn variadic(&self) -> bool {
7656 false
7657 }
7658 fn arg_schema(&self) -> &'static [ArgSchema] {
7659 &ARG_RANGE_NUM_LENIENT_ONE[..]
7660 }
7661 fn eval<'a, 'b, 'c>(
7662 &self,
7663 args: &'c [ArgumentHandle<'a, 'b>],
7664 _ctx: &dyn FunctionContext<'b>,
7665 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
7666 if args.len() < 2 {
7667 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7668 ExcelError::new_num(),
7669 )));
7670 }
7671
7672 // Collect data array
7673 let data = collect_numeric_stats(&args[0..1])?;
7674
7675 // Collect bins array
7676 let mut bins = collect_numeric_stats(&args[1..2])?;
7677
7678 // Handle empty bins - return single count of all data
7679 if bins.is_empty() {
7680 let rows = vec![vec![LiteralValue::Number(data.len() as f64)]];
7681 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Array(rows)));
7682 }
7683
7684 // Sort bins
7685 bins.sort_by(|a, b| a.partial_cmp(b).unwrap());
7686
7687 // Calculate frequencies
7688 // Result has bins.len() + 1 elements
7689 let mut frequencies = vec![0usize; bins.len() + 1];
7690
7691 for &value in &data {
7692 // Find which bin the value belongs to
7693 let mut found = false;
7694 for (i, &bin) in bins.iter().enumerate() {
7695 if i == 0 {
7696 // First bin: count values <= bins[0]
7697 if value <= bin {
7698 frequencies[0] += 1;
7699 found = true;
7700 break;
7701 }
7702 } else {
7703 // Intermediate bins: count values > bins[i-1] AND <= bins[i]
7704 if value <= bin {
7705 frequencies[i] += 1;
7706 found = true;
7707 break;
7708 }
7709 }
7710 }
7711 // Last bin: values > bins[last]
7712 if !found {
7713 frequencies[bins.len()] += 1;
7714 }
7715 }
7716
7717 // Return as vertical array (column vector)
7718 let rows: Vec<Vec<LiteralValue>> = frequencies
7719 .into_iter()
7720 .map(|f| vec![LiteralValue::Number(f as f64)])
7721 .collect();
7722
7723 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Array(rows)))
7724 }
7725}
7726
7727/* ─────────────────────────── T.DIST.2T ──────────────────────────── */
7728
7729/// Returns the two-tailed Student's t probability beyond `x`.
7730///
7731/// `T.DIST.2T` computes `P(|T| > x)` for the specified degrees of freedom.
7732///
7733/// # Remarks
7734/// - Requires `x >= 0` and `deg_freedom >= 1`.
7735/// - Represents a two-sided tail area.
7736/// - Returns `#NUM!` when arguments are outside valid ranges.
7737/// - Invalid numeric coercions propagate as spreadsheet errors.
7738///
7739/// # Examples
7740///
7741/// ```yaml,sandbox
7742/// title: "Two-tailed t probability at zero"
7743/// formula: "=T.DIST.2T(0,10)"
7744/// expected: 1
7745/// ```
7746///
7747/// ```yaml,sandbox
7748/// title: "Two-tailed t probability at x=2"
7749/// formula: "=T.DIST.2T(2,10)"
7750/// expected: 0.0733880342639167
7751/// ```
7752#[derive(Debug)]
7753pub struct TDist2TFn;
7754/// [formualizer-docgen:schema:start]
7755/// Name: T.DIST.2T
7756/// Type: TDist2TFn
7757/// Min args: 2
7758/// Max args: 2
7759/// Variadic: false
7760/// Signature: T.DIST.2T(arg1: number@scalar, arg2: number@scalar)
7761/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
7762/// Caps: PURE
7763/// [formualizer-docgen:schema:end]
7764impl Function for TDist2TFn {
7765 func_caps!(PURE);
7766 fn name(&self) -> &'static str {
7767 "T.DIST.2T"
7768 }
7769 fn min_args(&self) -> usize {
7770 2
7771 }
7772 fn arg_schema(&self) -> &'static [ArgSchema] {
7773 use std::sync::LazyLock;
7774 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
7775 vec![
7776 ArgSchema::number_lenient_scalar(),
7777 ArgSchema::number_lenient_scalar(),
7778 ]
7779 });
7780 &SCHEMA[..]
7781 }
7782 fn eval<'a, 'b, 'c>(
7783 &self,
7784 args: &'c [ArgumentHandle<'a, 'b>],
7785 _ctx: &dyn FunctionContext<'b>,
7786 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
7787 let x = coerce_num(&scalar_like_value(&args[0])?)?;
7788 let df = coerce_num(&scalar_like_value(&args[1])?)?;
7789
7790 // x must be non-negative for T.DIST.2T, df must be >= 1
7791 if x < 0.0 || df < 1.0 {
7792 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7793 ExcelError::new_num(),
7794 )));
7795 }
7796
7797 // Two-tailed: P(|T| > x) = 2 * (1 - t_cdf(x, df))
7798 let p = 2.0 * (1.0 - t_cdf(x, df));
7799 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(p)))
7800 }
7801}
7802
7803/* ─────────────────────────── T.INV.2T ──────────────────────────── */
7804
7805/// Returns the positive t critical value for a two-tailed probability.
7806///
7807/// `T.INV.2T` solves for `t` such that `P(|T| > t) = probability`.
7808///
7809/// # Remarks
7810/// - `probability` must satisfy `0 < probability <= 1`.
7811/// - `deg_freedom` must be at least `1`.
7812/// - Returns `#NUM!` for invalid probability or degree-of-freedom arguments.
7813/// - Alias `TINV` is supported.
7814///
7815/// # Examples
7816///
7817/// ```yaml,sandbox
7818/// title: "Maximum two-tailed probability"
7819/// formula: "=T.INV.2T(1,10)"
7820/// expected: 0
7821/// ```
7822///
7823/// ```yaml,sandbox
7824/// title: "95% two-sided critical value"
7825/// formula: "=T.INV.2T(0.05,10)"
7826/// expected: 2.228138851986273
7827/// ```
7828#[derive(Debug)]
7829pub struct TInv2TFn;
7830/// [formualizer-docgen:schema:start]
7831/// Name: T.INV.2T
7832/// Type: TInv2TFn
7833/// Min args: 2
7834/// Max args: 2
7835/// Variadic: false
7836/// Signature: T.INV.2T(arg1: number@scalar, arg2: number@scalar)
7837/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
7838/// Caps: PURE
7839/// [formualizer-docgen:schema:end]
7840impl Function for TInv2TFn {
7841 func_caps!(PURE);
7842 fn name(&self) -> &'static str {
7843 "T.INV.2T"
7844 }
7845 fn aliases(&self) -> &'static [&'static str] {
7846 &["TINV"]
7847 }
7848 fn min_args(&self) -> usize {
7849 2
7850 }
7851 fn arg_schema(&self) -> &'static [ArgSchema] {
7852 use std::sync::LazyLock;
7853 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
7854 vec![
7855 ArgSchema::number_lenient_scalar(),
7856 ArgSchema::number_lenient_scalar(),
7857 ]
7858 });
7859 &SCHEMA[..]
7860 }
7861 fn eval<'a, 'b, 'c>(
7862 &self,
7863 args: &'c [ArgumentHandle<'a, 'b>],
7864 _ctx: &dyn FunctionContext<'b>,
7865 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
7866 let p = coerce_num(&scalar_like_value(&args[0])?)?;
7867 let df = coerce_num(&scalar_like_value(&args[1])?)?;
7868
7869 // probability must be in (0, 1], df >= 1
7870 if p <= 0.0 || p > 1.0 || df < 1.0 {
7871 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7872 ExcelError::new_num(),
7873 )));
7874 }
7875
7876 // For two-tailed: we want t such that P(|T| > t) = p
7877 // P(|T| > t) = 2 * (1 - F(t)) where F is CDF
7878 // So 1 - F(t) = p/2, meaning F(t) = 1 - p/2
7879 // Thus t = t_inv(1 - p/2, df)
7880 match t_inv(1.0 - p / 2.0, df) {
7881 Some(result) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
7882 result,
7883 ))),
7884 None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7885 ExcelError::new_num(),
7886 ))),
7887 }
7888 }
7889}
7890
7891/* ─────────────────────────── T.TEST ──────────────────────────── */
7892
7893/// Returns the p-value from a Student t-test comparing two numeric samples.
7894///
7895/// `T.TEST` supports paired, equal-variance two-sample, and unequal-variance (Welch) modes.
7896///
7897/// # Remarks
7898/// - `tails` must be `1` (one-tailed) or `2` (two-tailed).
7899/// - `type` must be `1` (paired), `2` (two-sample equal variance), or `3` (Welch).
7900/// - Returns `#N/A` when paired mode arrays have different lengths.
7901/// - Returns `#NUM!` or `#DIV/0!` for invalid setup or degenerate variance conditions.
7902///
7903/// # Examples
7904///
7905/// ```yaml,sandbox
7906/// title: "Two-tailed equal-variance test with identical samples"
7907/// formula: "=T.TEST({1,2,3},{1,2,3},2,2)"
7908/// expected: 1
7909/// ```
7910///
7911/// ```yaml,sandbox
7912/// title: "One-tailed Welch test with identical samples"
7913/// formula: "=T.TEST({1,2,3},{1,2,3},1,3)"
7914/// expected: 0.5
7915/// ```
7916#[derive(Debug)]
7917pub struct TTestFn;
7918/// [formualizer-docgen:schema:start]
7919/// Name: T.TEST
7920/// Type: TTestFn
7921/// Min args: 4
7922/// Max args: 4
7923/// Variadic: false
7924/// Signature: T.TEST(arg1: number@range, arg2: number@range, arg3: number@scalar, arg4: number@scalar)
7925/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg4{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
7926/// Caps: PURE
7927/// [formualizer-docgen:schema:end]
7928impl Function for TTestFn {
7929 func_caps!(PURE);
7930 fn name(&self) -> &'static str {
7931 "T.TEST"
7932 }
7933 fn aliases(&self) -> &'static [&'static str] {
7934 &["TTEST"]
7935 }
7936 fn min_args(&self) -> usize {
7937 4
7938 }
7939 fn arg_schema(&self) -> &'static [ArgSchema] {
7940 use std::sync::LazyLock;
7941 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
7942 vec![
7943 {
7944 let mut s = ArgSchema::number_lenient_scalar();
7945 s.shape = crate::args::ShapeKind::Range;
7946 s.coercion = formualizer_common::CoercionPolicy::NumberLenientText;
7947 s
7948 },
7949 {
7950 let mut s = ArgSchema::number_lenient_scalar();
7951 s.shape = crate::args::ShapeKind::Range;
7952 s.coercion = formualizer_common::CoercionPolicy::NumberLenientText;
7953 s
7954 },
7955 ArgSchema::number_lenient_scalar(), // tails
7956 ArgSchema::number_lenient_scalar(), // type
7957 ]
7958 });
7959 &SCHEMA[..]
7960 }
7961 fn eval<'a, 'b, 'c>(
7962 &self,
7963 args: &'c [ArgumentHandle<'a, 'b>],
7964 _ctx: &dyn FunctionContext<'b>,
7965 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
7966 let array1 = collect_numeric_stats(&args[0..1])?;
7967 let array2 = collect_numeric_stats(&args[1..2])?;
7968 let tails = coerce_num(&scalar_like_value(&args[2])?)? as i32;
7969 let test_type = coerce_num(&scalar_like_value(&args[3])?)? as i32;
7970
7971 // Validate tails (1 or 2) and type (1, 2, or 3)
7972 if !(1..=2).contains(&tails) || !(1..=3).contains(&test_type) {
7973 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7974 ExcelError::new_num(),
7975 )));
7976 }
7977
7978 let n1 = array1.len();
7979 let n2 = array2.len();
7980
7981 // For paired test, arrays must have same length
7982 if test_type == 1 && n1 != n2 {
7983 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7984 ExcelError::new_na(),
7985 )));
7986 }
7987
7988 // Need at least 2 data points for meaningful t-test
7989 if n1 < 2 || n2 < 2 {
7990 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7991 ExcelError::new_num(),
7992 )));
7993 }
7994
7995 let (t_stat, df) = match test_type {
7996 1 => {
7997 // Paired t-test
7998 let n = n1 as f64;
7999 let diffs: Vec<f64> = array1
8000 .iter()
8001 .zip(array2.iter())
8002 .map(|(a, b)| a - b)
8003 .collect();
8004 let mean_diff = diffs.iter().sum::<f64>() / n;
8005 let var_diff =
8006 diffs.iter().map(|d| (d - mean_diff).powi(2)).sum::<f64>() / (n - 1.0);
8007 if var_diff == 0.0 {
8008 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8009 ExcelError::new_div(),
8010 )));
8011 }
8012 let se = (var_diff / n).sqrt();
8013 (mean_diff / se, n - 1.0)
8014 }
8015 2 => {
8016 // Two-sample equal variance (pooled)
8017 let n1f = n1 as f64;
8018 let n2f = n2 as f64;
8019 let mean1 = array1.iter().sum::<f64>() / n1f;
8020 let mean2 = array2.iter().sum::<f64>() / n2f;
8021 let var1 = array1.iter().map(|x| (x - mean1).powi(2)).sum::<f64>() / (n1f - 1.0);
8022 let var2 = array2.iter().map(|x| (x - mean2).powi(2)).sum::<f64>() / (n2f - 1.0);
8023
8024 // Pooled variance
8025 let sp2 = ((n1f - 1.0) * var1 + (n2f - 1.0) * var2) / (n1f + n2f - 2.0);
8026 if sp2 == 0.0 {
8027 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8028 ExcelError::new_div(),
8029 )));
8030 }
8031 let se = (sp2 * (1.0 / n1f + 1.0 / n2f)).sqrt();
8032 ((mean1 - mean2) / se, n1f + n2f - 2.0)
8033 }
8034 3 => {
8035 // Welch's t-test (unequal variance)
8036 let n1f = n1 as f64;
8037 let n2f = n2 as f64;
8038 let mean1 = array1.iter().sum::<f64>() / n1f;
8039 let mean2 = array2.iter().sum::<f64>() / n2f;
8040 let var1 = array1.iter().map(|x| (x - mean1).powi(2)).sum::<f64>() / (n1f - 1.0);
8041 let var2 = array2.iter().map(|x| (x - mean2).powi(2)).sum::<f64>() / (n2f - 1.0);
8042
8043 let s1_n = var1 / n1f;
8044 let s2_n = var2 / n2f;
8045 let se = (s1_n + s2_n).sqrt();
8046 if se == 0.0 {
8047 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8048 ExcelError::new_div(),
8049 )));
8050 }
8051
8052 // Welch-Satterthwaite degrees of freedom
8053 let df_num = (s1_n + s2_n).powi(2);
8054 let df_denom = s1_n.powi(2) / (n1f - 1.0) + s2_n.powi(2) / (n2f - 1.0);
8055 let df = if df_denom == 0.0 {
8056 1.0
8057 } else {
8058 df_num / df_denom
8059 };
8060 ((mean1 - mean2) / se, df)
8061 }
8062 _ => unreachable!(),
8063 };
8064
8065 // Calculate p-value based on tails
8066 let t_abs = t_stat.abs();
8067 let p = if tails == 1 {
8068 1.0 - t_cdf(t_abs, df)
8069 } else {
8070 2.0 * (1.0 - t_cdf(t_abs, df))
8071 };
8072
8073 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(p)))
8074 }
8075}
8076
8077/* ─────────────────────────── F.TEST ──────────────────────────── */
8078
8079/// Returns the two-tailed p-value from an F-test comparing sample variances.
8080///
8081/// `F.TEST` evaluates whether two samples have significantly different variances.
8082///
8083/// # Remarks
8084/// - Each array must contain at least two numeric values.
8085/// - Uses sample variances and computes a two-tailed probability.
8086/// - Returns `#DIV/0!` when either sample variance is zero.
8087/// - Alias `FTEST` is supported.
8088///
8089/// # Examples
8090///
8091/// ```yaml,sandbox
8092/// title: "Identical samples yield p-value 1"
8093/// formula: "=F.TEST({1,2,3,4},{1,2,3,4})"
8094/// expected: 1
8095/// ```
8096///
8097/// ```yaml,sandbox
8098/// title: "Different variances example"
8099/// formula: "=F.TEST({1,2,3,4},{1,1,1,5})"
8100/// expected: 0.5466810975407987
8101/// ```
8102#[derive(Debug)]
8103pub struct FTestFn;
8104/// [formualizer-docgen:schema:start]
8105/// Name: F.TEST
8106/// Type: FTestFn
8107/// Min args: 2
8108/// Max args: 2
8109/// Variadic: false
8110/// Signature: F.TEST(arg1: number@range, arg2: number@range)
8111/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
8112/// Caps: PURE
8113/// [formualizer-docgen:schema:end]
8114impl Function for FTestFn {
8115 func_caps!(PURE);
8116 fn name(&self) -> &'static str {
8117 "F.TEST"
8118 }
8119 fn aliases(&self) -> &'static [&'static str] {
8120 &["FTEST"]
8121 }
8122 fn min_args(&self) -> usize {
8123 2
8124 }
8125 fn arg_schema(&self) -> &'static [ArgSchema] {
8126 use std::sync::LazyLock;
8127 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
8128 vec![
8129 {
8130 let mut s = ArgSchema::number_lenient_scalar();
8131 s.shape = crate::args::ShapeKind::Range;
8132 s.coercion = formualizer_common::CoercionPolicy::NumberLenientText;
8133 s
8134 },
8135 {
8136 let mut s = ArgSchema::number_lenient_scalar();
8137 s.shape = crate::args::ShapeKind::Range;
8138 s.coercion = formualizer_common::CoercionPolicy::NumberLenientText;
8139 s
8140 },
8141 ]
8142 });
8143 &SCHEMA[..]
8144 }
8145 fn eval<'a, 'b, 'c>(
8146 &self,
8147 args: &'c [ArgumentHandle<'a, 'b>],
8148 _ctx: &dyn FunctionContext<'b>,
8149 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
8150 let array1 = collect_numeric_stats(&args[0..1])?;
8151 let array2 = collect_numeric_stats(&args[1..2])?;
8152
8153 let n1 = array1.len();
8154 let n2 = array2.len();
8155
8156 // Need at least 2 points in each array
8157 if n1 < 2 || n2 < 2 {
8158 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8159 ExcelError::new_div(),
8160 )));
8161 }
8162
8163 let n1f = n1 as f64;
8164 let n2f = n2 as f64;
8165
8166 let mean1 = array1.iter().sum::<f64>() / n1f;
8167 let mean2 = array2.iter().sum::<f64>() / n2f;
8168
8169 let var1 = array1.iter().map(|x| (x - mean1).powi(2)).sum::<f64>() / (n1f - 1.0);
8170 let var2 = array2.iter().map(|x| (x - mean2).powi(2)).sum::<f64>() / (n2f - 1.0);
8171
8172 // Handle zero variance
8173 if var1 == 0.0 || var2 == 0.0 {
8174 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8175 ExcelError::new_div(),
8176 )));
8177 }
8178
8179 // F-statistic: Excel's F.TEST uses var1/var2 (order matters for degrees of freedom)
8180 // and returns two-tailed p-value
8181 let f = var1 / var2;
8182 let df1 = n1f - 1.0;
8183 let df2 = n2f - 1.0;
8184
8185 // Two-tailed p-value: min(F.DIST(f), 1-F.DIST(f)) * 2
8186 let p_lower = f_cdf(f, df1, df2);
8187 let p_upper = 1.0 - p_lower;
8188 let p = 2.0 * p_lower.min(p_upper);
8189
8190 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(p)))
8191 }
8192}
8193
8194/* ─────────────────────────── CHISQ.TEST ──────────────────────────── */
8195
8196/// Returns the right-tail p-value from a chi-square goodness-of-fit style comparison.
8197///
8198/// `CHISQ.TEST` compares observed and expected values and computes `1 - CHISQ.DIST(...)`.
8199///
8200/// # Remarks
8201/// - `actual_range` and `expected_range` must contain the same number of numeric points.
8202/// - Expected values must be strictly greater than `0`.
8203/// - Requires at least two categories (`df >= 1`).
8204/// - Returns `#N/A` for length mismatches or empty inputs, and `#NUM!` for invalid expected values.
8205///
8206/// # Examples
8207///
8208/// ```yaml,sandbox
8209/// title: "Perfect match between observed and expected"
8210/// formula: "=CHISQ.TEST({20,30,50},{20,30,50})"
8211/// expected: 1
8212/// ```
8213///
8214/// ```yaml,sandbox
8215/// title: "Two-category chi-square test"
8216/// formula: "=CHISQ.TEST({18,22},{20,20})"
8217/// expected: 0.5270892568655381
8218/// ```
8219#[derive(Debug)]
8220pub struct ChisqTestFn;
8221/// [formualizer-docgen:schema:start]
8222/// Name: CHISQ.TEST
8223/// Type: ChisqTestFn
8224/// Min args: 2
8225/// Max args: 2
8226/// Variadic: false
8227/// Signature: CHISQ.TEST(arg1: number@range, arg2: number@range)
8228/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
8229/// Caps: PURE
8230/// [formualizer-docgen:schema:end]
8231impl Function for ChisqTestFn {
8232 func_caps!(PURE);
8233 fn name(&self) -> &'static str {
8234 "CHISQ.TEST"
8235 }
8236 fn aliases(&self) -> &'static [&'static str] {
8237 &["CHITEST"]
8238 }
8239 fn min_args(&self) -> usize {
8240 2
8241 }
8242 fn arg_schema(&self) -> &'static [ArgSchema] {
8243 use std::sync::LazyLock;
8244 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
8245 vec![
8246 {
8247 let mut s = ArgSchema::number_lenient_scalar();
8248 s.shape = crate::args::ShapeKind::Range;
8249 s.coercion = formualizer_common::CoercionPolicy::NumberLenientText;
8250 s
8251 },
8252 {
8253 let mut s = ArgSchema::number_lenient_scalar();
8254 s.shape = crate::args::ShapeKind::Range;
8255 s.coercion = formualizer_common::CoercionPolicy::NumberLenientText;
8256 s
8257 },
8258 ]
8259 });
8260 &SCHEMA[..]
8261 }
8262 fn eval<'a, 'b, 'c>(
8263 &self,
8264 args: &'c [ArgumentHandle<'a, 'b>],
8265 _ctx: &dyn FunctionContext<'b>,
8266 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
8267 let actual = collect_numeric_stats(&args[0..1])?;
8268 let expected = collect_numeric_stats(&args[1..2])?;
8269
8270 // Arrays must have same length
8271 if actual.len() != expected.len() {
8272 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8273 ExcelError::new_na(),
8274 )));
8275 }
8276
8277 if actual.is_empty() {
8278 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8279 ExcelError::new_na(),
8280 )));
8281 }
8282
8283 // Calculate chi-squared statistic: sum((observed - expected)^2 / expected)
8284 let mut chi_sq = 0.0;
8285 for (obs, exp) in actual.iter().zip(expected.iter()) {
8286 if *exp <= 0.0 {
8287 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8288 ExcelError::new_num(),
8289 )));
8290 }
8291 chi_sq += (obs - exp).powi(2) / exp;
8292 }
8293
8294 // Degrees of freedom = number of categories - 1
8295 let df = (actual.len() - 1) as f64;
8296
8297 if df < 1.0 {
8298 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8299 ExcelError::new_num(),
8300 )));
8301 }
8302
8303 // P-value = 1 - CHISQ.DIST(chi_sq, df, TRUE) = right-tail probability
8304 let p = 1.0 - chisq_cdf(chi_sq, df);
8305
8306 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(p)))
8307 }
8308}
8309
8310/* ═══════════════════════════════════════════════════════════════════════════
8311 FZ-PAR-01: Statistical compatibility batch
8312 AVERAGEA, MAXA, MINA, STDEVA, STDEVPA, VARA, VARPA, SKEW.P,
8313 T.DIST.RT, CHISQ.DIST.RT, CHISQ.INV.RT, F.DIST.RT, F.INV.RT,
8314 BETA.INV, BINOM.DIST.RANGE, BINOM.INV, GAMMA, GAMMA.INV,
8315 GAMMALN, GAMMALN.PRECISE
8316═══════════════════════════════════════════════════════════════════════════ */
8317
8318/// Collect numeric inputs applying Excel "A"-variant semantics:
8319/// - Range references: include numbers as-is, booleans as 0/1, text as 0. Errors propagate.
8320/// Blank cells are skipped.
8321/// - Direct scalar arguments: same coercion as standard collect_numeric_stats.
8322fn collect_numeric_a(args: &[ArgumentHandle]) -> Result<Vec<f64>, ExcelError> {
8323 let mut out = Vec::new();
8324 for a in args {
8325 if let Some(arr) = a.inline_array_literal()? {
8326 for row in arr.into_iter() {
8327 for cell in row.into_iter() {
8328 match cell {
8329 LiteralValue::Error(e) => return Err(e),
8330 LiteralValue::Number(n) => out.push(n),
8331 LiteralValue::Int(i) => out.push(i as f64),
8332 LiteralValue::Boolean(b) => out.push(if b { 1.0 } else { 0.0 }),
8333 LiteralValue::Text(_) => out.push(0.0),
8334 _ => {}
8335 }
8336 }
8337 }
8338 continue;
8339 }
8340
8341 if let Ok(view) = a.range_view() {
8342 view.for_each_cell(&mut |v| {
8343 match v {
8344 LiteralValue::Error(e) => return Err(e.clone()),
8345 LiteralValue::Number(n) => out.push(*n),
8346 LiteralValue::Int(i) => out.push(*i as f64),
8347 LiteralValue::Boolean(b) => out.push(if *b { 1.0 } else { 0.0 }),
8348 LiteralValue::Text(_) => out.push(0.0),
8349 LiteralValue::Empty => {} // skip blanks
8350 _ => {}
8351 }
8352 Ok(())
8353 })?;
8354 } else {
8355 let v = scalar_like_value(a)?;
8356 match v {
8357 LiteralValue::Error(e) => return Err(e),
8358 other => {
8359 if let Ok(n) = coerce_num(&other) {
8360 out.push(n);
8361 }
8362 }
8363 }
8364 }
8365 }
8366 Ok(out)
8367}
8368
8369/// Helper: inverse of the regularized incomplete beta function.
8370/// Given p = I_x(a,b), find x. Uses Newton-Raphson with beta_i / beta PDF.
8371fn beta_inv_helper(p: f64, a: f64, b: f64) -> Option<f64> {
8372 if p <= 0.0 {
8373 return Some(0.0);
8374 }
8375 if p >= 1.0 {
8376 return Some(1.0);
8377 }
8378 if a <= 0.0 || b <= 0.0 {
8379 return None;
8380 }
8381
8382 // Initial guess from normal approximation (Abramowitz & Stegun 26.5.22)
8383 let mut x = 0.5f64;
8384
8385 // Newton-Raphson
8386 let ln_beta_ab = ln_gamma(a) + ln_gamma(b) - ln_gamma(a + b);
8387 for _ in 0..100 {
8388 let cdf = beta_i(x, a, b);
8389 // Beta PDF: x^(a-1) * (1-x)^(b-1) / B(a,b)
8390 let pdf = if x > 0.0 && x < 1.0 {
8391 ((a - 1.0) * x.ln() + (b - 1.0) * (1.0 - x).ln() - ln_beta_ab).exp()
8392 } else {
8393 1e-30
8394 };
8395 if pdf.abs() < 1e-30 {
8396 break;
8397 }
8398 let delta = (cdf - p) / pdf;
8399 let new_x = (x - delta).clamp(1e-15, 1.0 - 1e-15);
8400 if (new_x - x).abs() < 1e-14 {
8401 x = new_x;
8402 break;
8403 }
8404 x = new_x;
8405 }
8406
8407 Some(x)
8408}
8409
8410/// Helper: inverse of GAMMA.DIST CDF. Given p = P(alpha, x/beta), find x.
8411fn gamma_inv_helper(p: f64, alpha: f64, beta: f64) -> Option<f64> {
8412 if p <= 0.0 {
8413 return Some(0.0);
8414 }
8415 if p >= 1.0 {
8416 return None;
8417 }
8418 if alpha <= 0.0 || beta <= 0.0 {
8419 return None;
8420 }
8421
8422 // Initial guess
8423 let mut x = alpha * beta;
8424 if p < 0.5 {
8425 x = x.min(beta);
8426 }
8427
8428 // Newton-Raphson on the standardized gamma CDF (gamma_p)
8429 for _ in 0..100 {
8430 let z = x / beta;
8431 let cdf = gamma_p(alpha, z);
8432 // Gamma PDF: z^(alpha-1) * e^(-z) / Gamma(alpha) / beta
8433 let pdf = if z > 0.0 {
8434 ((alpha - 1.0) * z.ln() - z - ln_gamma(alpha)).exp() / beta
8435 } else {
8436 1e-30
8437 };
8438 if pdf.abs() < 1e-30 {
8439 break;
8440 }
8441 let delta = (cdf - p) / pdf;
8442 let new_x = (x - delta).max(1e-15);
8443 if (new_x - x).abs() < 1e-12 * x.max(1e-15) {
8444 x = new_x;
8445 break;
8446 }
8447 x = new_x;
8448 }
8449
8450 Some(x)
8451}
8452
8453/* ─────────────────────────── AVERAGEA ──────────────────────────── */
8454
8455/// Returns the arithmetic mean while treating logical values and text as numeric inputs.
8456///
8457/// # Formula example
8458/// ```excel
8459/// # returns: 1
8460/// =AVERAGEA(TRUE,2,"x")
8461/// ```
8462///
8463/// ```yaml,sandbox
8464/// title: "Average with logical and text coercion"
8465/// formula: '=AVERAGEA(TRUE,2,"x")'
8466/// expected: 1
8467/// ```
8468///
8469/// ```yaml,docs
8470/// related:
8471/// - AVERAGE
8472/// - MAXA
8473/// - MINA
8474/// faq:
8475/// - q: "How does AVERAGEA treat text and booleans?"
8476/// a: "TRUE counts as 1, FALSE and text count as 0, and blanks are ignored."
8477/// ```
8478#[derive(Debug)]
8479pub struct AverageAFn;
8480/// [formualizer-docgen:schema:start]
8481/// Name: AVERAGEA
8482/// Type: AverageAFn
8483/// Min args: 1
8484/// Max args: variadic
8485/// Variadic: true
8486/// Signature: AVERAGEA(arg1...: number@range)
8487/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
8488/// Caps: PURE, REDUCTION
8489/// [formualizer-docgen:schema:end]
8490impl Function for AverageAFn {
8491 func_caps!(PURE, REDUCTION);
8492 fn name(&self) -> &'static str {
8493 "AVERAGEA"
8494 }
8495 fn min_args(&self) -> usize {
8496 1
8497 }
8498 fn variadic(&self) -> bool {
8499 true
8500 }
8501 fn arg_schema(&self) -> &'static [ArgSchema] {
8502 &ARG_RANGE_NUM_LENIENT_ONE[..]
8503 }
8504 fn eval<'a, 'b, 'c>(
8505 &self,
8506 args: &'c [ArgumentHandle<'a, 'b>],
8507 _ctx: &dyn FunctionContext<'b>,
8508 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
8509 let nums = collect_numeric_a(args)?;
8510 if nums.is_empty() {
8511 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8512 ExcelError::new_div(),
8513 )));
8514 }
8515 let avg = nums.iter().sum::<f64>() / nums.len() as f64;
8516 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(avg)))
8517 }
8518}
8519
8520/* ─────────────────────────── MAXA ──────────────────────────── */
8521
8522/// Returns the largest value after applying A-variant coercion rules.
8523///
8524/// # Formula example
8525/// ```excel
8526/// # returns: 1
8527/// =MAXA(TRUE,-2,"x")
8528/// ```
8529///
8530/// ```yaml,sandbox
8531/// title: "Maximum with logical and text coercion"
8532/// formula: '=MAXA(TRUE,-2,"x")'
8533/// expected: 1
8534/// ```
8535///
8536/// ```yaml,docs
8537/// related:
8538/// - MAX
8539/// - MINA
8540/// - AVERAGEA
8541/// faq:
8542/// - q: "What do text values contribute to MAXA?"
8543/// a: "Text contributes 0, so negative numeric inputs can still be smaller than text in the aggregate."
8544/// ```
8545#[derive(Debug)]
8546pub struct MaxAFn;
8547/// [formualizer-docgen:schema:start]
8548/// Name: MAXA
8549/// Type: MaxAFn
8550/// Min args: 1
8551/// Max args: variadic
8552/// Variadic: true
8553/// Signature: MAXA(arg1...: number@range)
8554/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
8555/// Caps: PURE, REDUCTION
8556/// [formualizer-docgen:schema:end]
8557impl Function for MaxAFn {
8558 func_caps!(PURE, REDUCTION);
8559 fn name(&self) -> &'static str {
8560 "MAXA"
8561 }
8562 fn min_args(&self) -> usize {
8563 1
8564 }
8565 fn variadic(&self) -> bool {
8566 true
8567 }
8568 fn arg_schema(&self) -> &'static [ArgSchema] {
8569 &ARG_RANGE_NUM_LENIENT_ONE[..]
8570 }
8571 fn eval<'a, 'b, 'c>(
8572 &self,
8573 args: &'c [ArgumentHandle<'a, 'b>],
8574 _ctx: &dyn FunctionContext<'b>,
8575 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
8576 let nums = collect_numeric_a(args)?;
8577 if nums.is_empty() {
8578 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0)));
8579 }
8580 let mx = nums.iter().copied().fold(f64::NEG_INFINITY, f64::max);
8581 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(mx)))
8582 }
8583}
8584
8585/* ─────────────────────────── MINA ──────────────────────────── */
8586
8587/// Returns the smallest value after applying A-variant coercion rules.
8588///
8589/// # Formula example
8590/// ```excel
8591/// # returns: -2
8592/// =MINA(TRUE,-2,"x")
8593/// ```
8594///
8595/// ```yaml,sandbox
8596/// title: "Minimum with logical and text coercion"
8597/// formula: '=MINA(TRUE,-2,"x")'
8598/// expected: -2
8599/// ```
8600///
8601/// ```yaml,docs
8602/// related:
8603/// - MIN
8604/// - MAXA
8605/// - AVERAGEA
8606/// faq:
8607/// - q: "Do text values affect MINA?"
8608/// a: "Yes. Text is coerced to 0, so it can become the minimum when all numeric inputs are positive."
8609/// ```
8610#[derive(Debug)]
8611pub struct MinAFn;
8612/// [formualizer-docgen:schema:start]
8613/// Name: MINA
8614/// Type: MinAFn
8615/// Min args: 1
8616/// Max args: variadic
8617/// Variadic: true
8618/// Signature: MINA(arg1...: number@range)
8619/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
8620/// Caps: PURE, REDUCTION
8621/// [formualizer-docgen:schema:end]
8622impl Function for MinAFn {
8623 func_caps!(PURE, REDUCTION);
8624 fn name(&self) -> &'static str {
8625 "MINA"
8626 }
8627 fn min_args(&self) -> usize {
8628 1
8629 }
8630 fn variadic(&self) -> bool {
8631 true
8632 }
8633 fn arg_schema(&self) -> &'static [ArgSchema] {
8634 &ARG_RANGE_NUM_LENIENT_ONE[..]
8635 }
8636 fn eval<'a, 'b, 'c>(
8637 &self,
8638 args: &'c [ArgumentHandle<'a, 'b>],
8639 _ctx: &dyn FunctionContext<'b>,
8640 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
8641 let nums = collect_numeric_a(args)?;
8642 if nums.is_empty() {
8643 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0)));
8644 }
8645 let mn = nums.iter().copied().fold(f64::INFINITY, f64::min);
8646 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(mn)))
8647 }
8648}
8649
8650/* ─────────────────────────── STDEVA ──────────────────────────── */
8651
8652/// Returns the sample standard deviation using A-variant coercion semantics.
8653///
8654/// # Formula example
8655/// ```excel
8656/// # returns: 1
8657/// =STDEVA(TRUE,2,"x")
8658/// ```
8659///
8660/// ```yaml,sandbox
8661/// title: "Sample deviation with coerced values"
8662/// formula: '=STDEVA(TRUE,2,"x")'
8663/// expected: 1
8664/// ```
8665///
8666/// ```yaml,docs
8667/// related:
8668/// - STDEV.P
8669/// - STDEVPA
8670/// - VARA
8671/// faq:
8672/// - q: "When does STDEVA return #DIV/0!?"
8673/// a: "It returns #DIV/0! when fewer than two coerced values remain after evaluation."
8674/// ```
8675#[derive(Debug)]
8676pub struct StdevAFn;
8677/// [formualizer-docgen:schema:start]
8678/// Name: STDEVA
8679/// Type: StdevAFn
8680/// Min args: 1
8681/// Max args: variadic
8682/// Variadic: true
8683/// Signature: STDEVA(arg1...: number@range)
8684/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
8685/// Caps: PURE, REDUCTION
8686/// [formualizer-docgen:schema:end]
8687impl Function for StdevAFn {
8688 func_caps!(PURE, REDUCTION);
8689 fn name(&self) -> &'static str {
8690 "STDEVA"
8691 }
8692 fn min_args(&self) -> usize {
8693 1
8694 }
8695 fn variadic(&self) -> bool {
8696 true
8697 }
8698 fn arg_schema(&self) -> &'static [ArgSchema] {
8699 &ARG_RANGE_NUM_LENIENT_ONE[..]
8700 }
8701 fn eval<'a, 'b, 'c>(
8702 &self,
8703 args: &'c [ArgumentHandle<'a, 'b>],
8704 _ctx: &dyn FunctionContext<'b>,
8705 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
8706 let nums = collect_numeric_a(args)?;
8707 let n = nums.len();
8708 if n < 2 {
8709 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8710 ExcelError::new_div(),
8711 )));
8712 }
8713 let mean = nums.iter().sum::<f64>() / n as f64;
8714 let ss: f64 = nums.iter().map(|v| (v - mean).powi(2)).sum();
8715 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
8716 (ss / (n - 1) as f64).sqrt(),
8717 )))
8718 }
8719}
8720
8721/* ─────────────────────────── STDEVPA ──────────────────────────── */
8722
8723/// Returns the population standard deviation using A-variant coercion semantics.
8724///
8725/// # Formula example
8726/// ```excel
8727/// # returns: 0.816496580927726
8728/// =STDEVPA(TRUE,2,"x")
8729/// ```
8730///
8731/// ```yaml,sandbox
8732/// title: "Population deviation with coerced values"
8733/// formula: '=STDEVPA(TRUE,2,"x")'
8734/// expected: 0.816496580927726
8735/// ```
8736///
8737/// ```yaml,docs
8738/// related:
8739/// - STDEVA
8740/// - VARPA
8741/// - STDEV.P
8742/// faq:
8743/// - q: "What is the difference between STDEVA and STDEVPA?"
8744/// a: "STDEVA uses the sample denominator n-1, while STDEVPA uses the population denominator n."
8745/// ```
8746#[derive(Debug)]
8747pub struct StdevPAFn;
8748/// [formualizer-docgen:schema:start]
8749/// Name: STDEVPA
8750/// Type: StdevPAFn
8751/// Min args: 1
8752/// Max args: variadic
8753/// Variadic: true
8754/// Signature: STDEVPA(arg1...: number@range)
8755/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
8756/// Caps: PURE, REDUCTION
8757/// [formualizer-docgen:schema:end]
8758impl Function for StdevPAFn {
8759 func_caps!(PURE, REDUCTION);
8760 fn name(&self) -> &'static str {
8761 "STDEVPA"
8762 }
8763 fn min_args(&self) -> usize {
8764 1
8765 }
8766 fn variadic(&self) -> bool {
8767 true
8768 }
8769 fn arg_schema(&self) -> &'static [ArgSchema] {
8770 &ARG_RANGE_NUM_LENIENT_ONE[..]
8771 }
8772 fn eval<'a, 'b, 'c>(
8773 &self,
8774 args: &'c [ArgumentHandle<'a, 'b>],
8775 _ctx: &dyn FunctionContext<'b>,
8776 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
8777 let nums = collect_numeric_a(args)?;
8778 let n = nums.len();
8779 if n == 0 {
8780 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8781 ExcelError::new_div(),
8782 )));
8783 }
8784 let mean = nums.iter().sum::<f64>() / n as f64;
8785 let ss: f64 = nums.iter().map(|v| (v - mean).powi(2)).sum();
8786 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
8787 (ss / n as f64).sqrt(),
8788 )))
8789 }
8790}
8791
8792/* ─────────────────────────── VARA ──────────────────────────── */
8793
8794/// Returns the sample variance using A-variant coercion semantics.
8795///
8796/// # Formula example
8797/// ```excel
8798/// # returns: 1
8799/// =VARA(TRUE,2,"x")
8800/// ```
8801///
8802/// ```yaml,sandbox
8803/// title: "Sample variance with coerced values"
8804/// formula: '=VARA(TRUE,2,"x")'
8805/// expected: 1
8806/// ```
8807///
8808/// ```yaml,docs
8809/// related:
8810/// - VARPA
8811/// - STDEVA
8812/// - AVERAGEA
8813/// faq:
8814/// - q: "How are blanks handled in VARA?"
8815/// a: "Blanks are ignored, while booleans and text are coerced under A-variant rules."
8816/// ```
8817#[derive(Debug)]
8818pub struct VarAFn;
8819/// [formualizer-docgen:schema:start]
8820/// Name: VARA
8821/// Type: VarAFn
8822/// Min args: 1
8823/// Max args: variadic
8824/// Variadic: true
8825/// Signature: VARA(arg1...: number@range)
8826/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
8827/// Caps: PURE, REDUCTION
8828/// [formualizer-docgen:schema:end]
8829impl Function for VarAFn {
8830 func_caps!(PURE, REDUCTION);
8831 fn name(&self) -> &'static str {
8832 "VARA"
8833 }
8834 fn min_args(&self) -> usize {
8835 1
8836 }
8837 fn variadic(&self) -> bool {
8838 true
8839 }
8840 fn arg_schema(&self) -> &'static [ArgSchema] {
8841 &ARG_RANGE_NUM_LENIENT_ONE[..]
8842 }
8843 fn eval<'a, 'b, 'c>(
8844 &self,
8845 args: &'c [ArgumentHandle<'a, 'b>],
8846 _ctx: &dyn FunctionContext<'b>,
8847 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
8848 let nums = collect_numeric_a(args)?;
8849 let n = nums.len();
8850 if n < 2 {
8851 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8852 ExcelError::new_div(),
8853 )));
8854 }
8855 let mean = nums.iter().sum::<f64>() / n as f64;
8856 let ss: f64 = nums.iter().map(|v| (v - mean).powi(2)).sum();
8857 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
8858 ss / (n - 1) as f64,
8859 )))
8860 }
8861}
8862
8863/* ─────────────────────────── VARPA ──────────────────────────── */
8864
8865/// Returns the population variance using A-variant coercion semantics.
8866///
8867/// # Formula example
8868/// ```excel
8869/// # returns: 0.6666666666666666
8870/// =VARPA(TRUE,2,"x")
8871/// ```
8872///
8873/// ```yaml,sandbox
8874/// title: "Population variance with coerced values"
8875/// formula: '=VARPA(TRUE,2,"x")'
8876/// expected: 0.6666666666666666
8877/// ```
8878///
8879/// ```yaml,docs
8880/// related:
8881/// - VARA
8882/// - STDEVPA
8883/// - STDEV.P
8884/// faq:
8885/// - q: "When does VARPA return #DIV/0!?"
8886/// a: "It returns #DIV/0! when no coerced values remain after evaluation."
8887/// ```
8888#[derive(Debug)]
8889pub struct VarPAFn;
8890/// [formualizer-docgen:schema:start]
8891/// Name: VARPA
8892/// Type: VarPAFn
8893/// Min args: 1
8894/// Max args: variadic
8895/// Variadic: true
8896/// Signature: VARPA(arg1...: number@range)
8897/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
8898/// Caps: PURE, REDUCTION
8899/// [formualizer-docgen:schema:end]
8900impl Function for VarPAFn {
8901 func_caps!(PURE, REDUCTION);
8902 fn name(&self) -> &'static str {
8903 "VARPA"
8904 }
8905 fn min_args(&self) -> usize {
8906 1
8907 }
8908 fn variadic(&self) -> bool {
8909 true
8910 }
8911 fn arg_schema(&self) -> &'static [ArgSchema] {
8912 &ARG_RANGE_NUM_LENIENT_ONE[..]
8913 }
8914 fn eval<'a, 'b, 'c>(
8915 &self,
8916 args: &'c [ArgumentHandle<'a, 'b>],
8917 _ctx: &dyn FunctionContext<'b>,
8918 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
8919 let nums = collect_numeric_a(args)?;
8920 let n = nums.len();
8921 if n == 0 {
8922 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8923 ExcelError::new_div(),
8924 )));
8925 }
8926 let mean = nums.iter().sum::<f64>() / n as f64;
8927 let ss: f64 = nums.iter().map(|v| (v - mean).powi(2)).sum();
8928 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
8929 ss / n as f64,
8930 )))
8931 }
8932}
8933
8934/* ─────────────────────────── SKEW.P ──────────────────────────── */
8935
8936/// Returns the population skewness of a numeric data set.
8937///
8938/// # Formula example
8939/// ```excel
8940/// # returns: 0
8941/// =SKEW.P(1,2,3)
8942/// ```
8943///
8944/// ```yaml,sandbox
8945/// title: "Symmetric data has zero skew"
8946/// formula: '=SKEW.P(1,2,3)'
8947/// expected: 0
8948/// ```
8949///
8950/// ```yaml,docs
8951/// related:
8952/// - SKEW
8953/// - KURT
8954/// - AVERAGE
8955/// faq:
8956/// - q: "When does SKEW.P return #DIV/0!?"
8957/// a: "It returns #DIV/0! when fewer than three numeric values are available or the population standard deviation is zero."
8958/// ```
8959#[derive(Debug)]
8960pub struct SkewPFn;
8961/// [formualizer-docgen:schema:start]
8962/// Name: SKEW.P
8963/// Type: SkewPFn
8964/// Min args: 1
8965/// Max args: variadic
8966/// Variadic: true
8967/// Signature: SKEW.P(arg1...: number@range)
8968/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
8969/// Caps: PURE, REDUCTION, NUMERIC_ONLY
8970/// [formualizer-docgen:schema:end]
8971impl Function for SkewPFn {
8972 func_caps!(PURE, REDUCTION, NUMERIC_ONLY);
8973 fn name(&self) -> &'static str {
8974 "SKEW.P"
8975 }
8976 fn min_args(&self) -> usize {
8977 1
8978 }
8979 fn variadic(&self) -> bool {
8980 true
8981 }
8982 fn arg_schema(&self) -> &'static [ArgSchema] {
8983 &ARG_RANGE_NUM_LENIENT_ONE[..]
8984 }
8985 fn eval<'a, 'b, 'c>(
8986 &self,
8987 args: &'c [ArgumentHandle<'a, 'b>],
8988 _ctx: &dyn FunctionContext<'b>,
8989 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
8990 let nums = collect_numeric_stats(args)?;
8991 let n = nums.len();
8992 if n < 3 {
8993 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8994 ExcelError::new_div(),
8995 )));
8996 }
8997 let n_f = n as f64;
8998 let mean = nums.iter().sum::<f64>() / n_f;
8999 let mut sum_sq = 0.0;
9000 for &v in &nums {
9001 sum_sq += (v - mean).powi(2);
9002 }
9003 let stdev_pop = (sum_sq / n_f).sqrt();
9004 if stdev_pop == 0.0 {
9005 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9006 ExcelError::new_div(),
9007 )));
9008 }
9009 let mut sum_cubed = 0.0;
9010 for &v in &nums {
9011 sum_cubed += ((v - mean) / stdev_pop).powi(3);
9012 }
9013 // Population skewness: (1/n) * sum((xi - mean)/sigma)^3
9014 let skew = sum_cubed / n_f;
9015 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(skew)))
9016 }
9017}
9018
9019/* ─────────────────────────── T.DIST.RT ──────────────────────────── */
9020
9021/// Returns the right-tailed Student's t-distribution probability.
9022///
9023/// # Formula example
9024/// ```excel
9025/// # returns: 0.5
9026/// =T.DIST.RT(0,10)
9027/// ```
9028///
9029/// ```yaml,sandbox
9030/// title: "Zero lies at the midpoint of the t distribution"
9031/// formula: '=T.DIST.RT(0,10)'
9032/// expected: 0.5
9033/// ```
9034///
9035/// ```yaml,docs
9036/// related:
9037/// - T.DIST.2T
9038/// - T.INV
9039/// - T.INV.2T
9040/// faq:
9041/// - q: "When does T.DIST.RT return #NUM!?"
9042/// a: "It returns #NUM! when the degrees of freedom are less than 1."
9043/// ```
9044#[derive(Debug)]
9045pub struct TDistRtFn;
9046/// [formualizer-docgen:schema:start]
9047/// Name: T.DIST.RT
9048/// Type: TDistRtFn
9049/// Min args: 2
9050/// Max args: 2
9051/// Variadic: false
9052/// Signature: T.DIST.RT(arg1: number@scalar, arg2: number@scalar)
9053/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
9054/// Caps: PURE
9055/// [formualizer-docgen:schema:end]
9056impl Function for TDistRtFn {
9057 func_caps!(PURE);
9058 fn name(&self) -> &'static str {
9059 "T.DIST.RT"
9060 }
9061 fn min_args(&self) -> usize {
9062 2
9063 }
9064 fn arg_schema(&self) -> &'static [ArgSchema] {
9065 use std::sync::LazyLock;
9066 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
9067 vec![
9068 ArgSchema::number_lenient_scalar(),
9069 ArgSchema::number_lenient_scalar(),
9070 ]
9071 });
9072 &SCHEMA[..]
9073 }
9074 fn eval<'a, 'b, 'c>(
9075 &self,
9076 args: &'c [ArgumentHandle<'a, 'b>],
9077 _ctx: &dyn FunctionContext<'b>,
9078 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
9079 let x = coerce_num(&scalar_like_value(&args[0])?)?;
9080 let df = coerce_num(&scalar_like_value(&args[1])?)?;
9081 if df < 1.0 {
9082 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9083 ExcelError::new_num(),
9084 )));
9085 }
9086 let result = 1.0 - t_cdf(x, df);
9087 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
9088 result,
9089 )))
9090 }
9091}
9092
9093/* ─────────────────────────── CHISQ.DIST.RT ──────────────────────────── */
9094
9095/// Returns the right-tailed chi-squared distribution probability.
9096///
9097/// # Formula example
9098/// ```excel
9099/// # returns: 0.36787944117144233
9100/// =CHISQ.DIST.RT(2,2)
9101/// ```
9102///
9103/// ```yaml,sandbox
9104/// title: "Right-tail chi-squared probability"
9105/// formula: '=CHISQ.DIST.RT(2,2)'
9106/// expected: 0.36787944117144233
9107/// ```
9108///
9109/// ```yaml,docs
9110/// related:
9111/// - CHISQ.INV.RT
9112/// - CHISQ.TEST
9113/// - CHISQ.DIST
9114/// faq:
9115/// - q: "Which inputs return #NUM!?"
9116/// a: "Negative x values and degrees of freedom below 1 return #NUM!."
9117/// ```
9118#[derive(Debug)]
9119pub struct ChisqDistRtFn;
9120/// [formualizer-docgen:schema:start]
9121/// Name: CHISQ.DIST.RT
9122/// Type: ChisqDistRtFn
9123/// Min args: 2
9124/// Max args: 2
9125/// Variadic: false
9126/// Signature: CHISQ.DIST.RT(arg1: number@scalar, arg2: number@scalar)
9127/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
9128/// Caps: PURE
9129/// [formualizer-docgen:schema:end]
9130impl Function for ChisqDistRtFn {
9131 func_caps!(PURE);
9132 fn name(&self) -> &'static str {
9133 "CHISQ.DIST.RT"
9134 }
9135 fn min_args(&self) -> usize {
9136 2
9137 }
9138 fn arg_schema(&self) -> &'static [ArgSchema] {
9139 use std::sync::LazyLock;
9140 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
9141 vec![
9142 ArgSchema::number_lenient_scalar(),
9143 ArgSchema::number_lenient_scalar(),
9144 ]
9145 });
9146 &SCHEMA[..]
9147 }
9148 fn eval<'a, 'b, 'c>(
9149 &self,
9150 args: &'c [ArgumentHandle<'a, 'b>],
9151 _ctx: &dyn FunctionContext<'b>,
9152 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
9153 let x = coerce_num(&scalar_like_value(&args[0])?)?;
9154 let df = coerce_num(&scalar_like_value(&args[1])?)?;
9155 if df < 1.0 || x < 0.0 {
9156 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9157 ExcelError::new_num(),
9158 )));
9159 }
9160 let result = 1.0 - chisq_cdf(x, df);
9161 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
9162 result,
9163 )))
9164 }
9165}
9166
9167/* ─────────────────────────── CHISQ.INV.RT ──────────────────────────── */
9168
9169/// Returns the inverse of the right-tailed chi-squared distribution.
9170///
9171/// # Formula example
9172/// ```excel
9173/// # returns: 1.3862943611198906
9174/// =CHISQ.INV.RT(0.5,2)
9175/// ```
9176///
9177/// ```yaml,sandbox
9178/// title: "Median right-tail inverse for 2 degrees of freedom"
9179/// formula: '=CHISQ.INV.RT(0.5,2)'
9180/// expected: 1.3862943611198906
9181/// ```
9182///
9183/// ```yaml,docs
9184/// related:
9185/// - CHISQ.DIST.RT
9186/// - CHISQ.INV
9187/// - CHISQ.TEST
9188/// faq:
9189/// - q: "What p-values are valid for CHISQ.INV.RT?"
9190/// a: "p must lie in the range 0 to 1, and p=0 returns #NUM! because the right-tail inverse diverges."
9191/// ```
9192#[derive(Debug)]
9193pub struct ChisqInvRtFn;
9194/// [formualizer-docgen:schema:start]
9195/// Name: CHISQ.INV.RT
9196/// Type: ChisqInvRtFn
9197/// Min args: 2
9198/// Max args: 2
9199/// Variadic: false
9200/// Signature: CHISQ.INV.RT(arg1: number@scalar, arg2: number@scalar)
9201/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
9202/// Caps: PURE
9203/// [formualizer-docgen:schema:end]
9204impl Function for ChisqInvRtFn {
9205 func_caps!(PURE);
9206 fn name(&self) -> &'static str {
9207 "CHISQ.INV.RT"
9208 }
9209 fn min_args(&self) -> usize {
9210 2
9211 }
9212 fn arg_schema(&self) -> &'static [ArgSchema] {
9213 use std::sync::LazyLock;
9214 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
9215 vec![
9216 ArgSchema::number_lenient_scalar(),
9217 ArgSchema::number_lenient_scalar(),
9218 ]
9219 });
9220 &SCHEMA[..]
9221 }
9222 fn eval<'a, 'b, 'c>(
9223 &self,
9224 args: &'c [ArgumentHandle<'a, 'b>],
9225 _ctx: &dyn FunctionContext<'b>,
9226 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
9227 let p = coerce_num(&scalar_like_value(&args[0])?)?;
9228 let df = coerce_num(&scalar_like_value(&args[1])?)?;
9229 if df < 1.0 || !(0.0..=1.0).contains(&p) {
9230 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9231 ExcelError::new_num(),
9232 )));
9233 }
9234 // Right-tail: CHISQ.INV.RT(p, df) = CHISQ.INV(1-p, df)
9235 if p == 0.0 {
9236 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9237 ExcelError::new_num(),
9238 )));
9239 }
9240 match chisq_inv(1.0 - p, df) {
9241 Some(result) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
9242 result,
9243 ))),
9244 None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9245 ExcelError::new_num(),
9246 ))),
9247 }
9248 }
9249}
9250
9251/* ─────────────────────────── F.DIST.RT ──────────────────────────── */
9252
9253/// Returns the right-tailed F-distribution probability.
9254///
9255/// # Formula example
9256/// ```excel
9257/// # returns: 1
9258/// =F.DIST.RT(0,5,10)
9259/// ```
9260///
9261/// ```yaml,sandbox
9262/// title: "Zero leaves the entire right tail"
9263/// formula: '=F.DIST.RT(0,5,10)'
9264/// expected: 1
9265/// ```
9266///
9267/// ```yaml,docs
9268/// related:
9269/// - F.INV.RT
9270/// - F.TEST
9271/// - F.DIST
9272/// faq:
9273/// - q: "Which inputs return #NUM!?"
9274/// a: "Negative x values or degrees of freedom below 1 return #NUM!."
9275/// ```
9276#[derive(Debug)]
9277pub struct FDistRtFn;
9278/// [formualizer-docgen:schema:start]
9279/// Name: F.DIST.RT
9280/// Type: FDistRtFn
9281/// Min args: 3
9282/// Max args: 3
9283/// Variadic: false
9284/// Signature: F.DIST.RT(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
9285/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
9286/// Caps: PURE
9287/// [formualizer-docgen:schema:end]
9288impl Function for FDistRtFn {
9289 func_caps!(PURE);
9290 fn name(&self) -> &'static str {
9291 "F.DIST.RT"
9292 }
9293 fn min_args(&self) -> usize {
9294 3
9295 }
9296 fn arg_schema(&self) -> &'static [ArgSchema] {
9297 use std::sync::LazyLock;
9298 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
9299 vec![
9300 ArgSchema::number_lenient_scalar(),
9301 ArgSchema::number_lenient_scalar(),
9302 ArgSchema::number_lenient_scalar(),
9303 ]
9304 });
9305 &SCHEMA[..]
9306 }
9307 fn eval<'a, 'b, 'c>(
9308 &self,
9309 args: &'c [ArgumentHandle<'a, 'b>],
9310 _ctx: &dyn FunctionContext<'b>,
9311 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
9312 let x = coerce_num(&scalar_like_value(&args[0])?)?;
9313 let d1 = coerce_num(&scalar_like_value(&args[1])?)?;
9314 let d2 = coerce_num(&scalar_like_value(&args[2])?)?;
9315 if d1 < 1.0 || d2 < 1.0 || x < 0.0 {
9316 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9317 ExcelError::new_num(),
9318 )));
9319 }
9320 let result = 1.0 - f_cdf(x, d1, d2);
9321 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
9322 result,
9323 )))
9324 }
9325}
9326
9327/* ─────────────────────────── F.INV.RT ──────────────────────────── */
9328
9329/// Returns the inverse of the right-tailed F-distribution.
9330///
9331/// # Formula example
9332/// ```excel
9333/// # returns: 0
9334/// =F.INV.RT(1,5,10)
9335/// ```
9336///
9337/// ```yaml,sandbox
9338/// title: "A full right tail maps to zero"
9339/// formula: '=F.INV.RT(1,5,10)'
9340/// expected: 0
9341/// ```
9342///
9343/// ```yaml,docs
9344/// related:
9345/// - F.DIST.RT
9346/// - F.INV
9347/// - F.TEST
9348/// faq:
9349/// - q: "What p-values are valid for F.INV.RT?"
9350/// a: "p must lie in the range 0 to 1, and p=0 returns #NUM! because the inverse diverges."
9351/// ```
9352#[derive(Debug)]
9353pub struct FInvRtFn;
9354/// [formualizer-docgen:schema:start]
9355/// Name: F.INV.RT
9356/// Type: FInvRtFn
9357/// Min args: 3
9358/// Max args: 3
9359/// Variadic: false
9360/// Signature: F.INV.RT(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
9361/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
9362/// Caps: PURE
9363/// [formualizer-docgen:schema:end]
9364impl Function for FInvRtFn {
9365 func_caps!(PURE);
9366 fn name(&self) -> &'static str {
9367 "F.INV.RT"
9368 }
9369 fn min_args(&self) -> usize {
9370 3
9371 }
9372 fn arg_schema(&self) -> &'static [ArgSchema] {
9373 use std::sync::LazyLock;
9374 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
9375 vec![
9376 ArgSchema::number_lenient_scalar(),
9377 ArgSchema::number_lenient_scalar(),
9378 ArgSchema::number_lenient_scalar(),
9379 ]
9380 });
9381 &SCHEMA[..]
9382 }
9383 fn eval<'a, 'b, 'c>(
9384 &self,
9385 args: &'c [ArgumentHandle<'a, 'b>],
9386 _ctx: &dyn FunctionContext<'b>,
9387 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
9388 let p = coerce_num(&scalar_like_value(&args[0])?)?;
9389 let d1 = coerce_num(&scalar_like_value(&args[1])?)?;
9390 let d2 = coerce_num(&scalar_like_value(&args[2])?)?;
9391 if d1 < 1.0 || d2 < 1.0 || !(0.0..=1.0).contains(&p) {
9392 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9393 ExcelError::new_num(),
9394 )));
9395 }
9396 if p == 0.0 {
9397 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9398 ExcelError::new_num(),
9399 )));
9400 }
9401 // F.INV.RT(1, d1, d2) = 0 (entire right tail)
9402 if p == 1.0 {
9403 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0)));
9404 }
9405 // F.INV.RT(p, d1, d2) = F.INV(1-p, d1, d2)
9406 match f_inv(1.0 - p, d1, d2) {
9407 Some(result) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
9408 result,
9409 ))),
9410 None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9411 ExcelError::new_num(),
9412 ))),
9413 }
9414 }
9415}
9416
9417/* ─────────────────────────── BETA.INV ──────────────────────────── */
9418
9419/// Returns the inverse cumulative beta distribution, optionally scaled to custom bounds.
9420///
9421/// # Formula example
9422/// ```excel
9423/// # returns: 0.5
9424/// =BETA.INV(0.5,2,2)
9425/// ```
9426///
9427/// ```yaml,sandbox
9428/// title: "Symmetric beta inverse at the median"
9429/// formula: '=BETA.INV(0.5,2,2)'
9430/// expected: 0.5
9431/// ```
9432///
9433/// ```yaml,docs
9434/// related:
9435/// - BETA.DIST
9436/// - GAMMA.INV
9437/// - NORM.INV
9438/// faq:
9439/// - q: "When does BETA.INV return #NUM!?"
9440/// a: "It returns #NUM! for non-positive alpha or beta, invalid bounds, or probabilities outside 0..1."
9441/// ```
9442#[derive(Debug)]
9443pub struct BetaInvFn;
9444/// [formualizer-docgen:schema:start]
9445/// Name: BETA.INV
9446/// Type: BetaInvFn
9447/// Min args: 3
9448/// Max args: variadic
9449/// Variadic: true
9450/// Signature: BETA.INV(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar, arg5...: number@scalar)
9451/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg4{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg5{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
9452/// Caps: PURE
9453/// [formualizer-docgen:schema:end]
9454impl Function for BetaInvFn {
9455 func_caps!(PURE);
9456 fn name(&self) -> &'static str {
9457 "BETA.INV"
9458 }
9459 fn min_args(&self) -> usize {
9460 3
9461 }
9462 fn variadic(&self) -> bool {
9463 true
9464 }
9465 fn arg_schema(&self) -> &'static [ArgSchema] {
9466 use std::sync::LazyLock;
9467 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
9468 vec![
9469 ArgSchema::number_lenient_scalar(),
9470 ArgSchema::number_lenient_scalar(),
9471 ArgSchema::number_lenient_scalar(),
9472 ArgSchema::number_lenient_scalar(),
9473 ArgSchema::number_lenient_scalar(),
9474 ]
9475 });
9476 &SCHEMA[..]
9477 }
9478 fn eval<'a, 'b, 'c>(
9479 &self,
9480 args: &'c [ArgumentHandle<'a, 'b>],
9481 _ctx: &dyn FunctionContext<'b>,
9482 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
9483 let p = coerce_num(&scalar_like_value(&args[0])?)?;
9484 let alpha = coerce_num(&scalar_like_value(&args[1])?)?;
9485 let beta_param = coerce_num(&scalar_like_value(&args[2])?)?;
9486 let a_bound = if args.len() > 3 {
9487 coerce_num(&scalar_like_value(&args[3])?)?
9488 } else {
9489 0.0
9490 };
9491 let b_bound = if args.len() > 4 {
9492 coerce_num(&scalar_like_value(&args[4])?)?
9493 } else {
9494 1.0
9495 };
9496
9497 if alpha <= 0.0 || beta_param <= 0.0 || a_bound >= b_bound || !(0.0..=1.0).contains(&p) {
9498 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9499 ExcelError::new_num(),
9500 )));
9501 }
9502
9503 match beta_inv_helper(p, alpha, beta_param) {
9504 Some(x_std) => {
9505 let result = a_bound + x_std * (b_bound - a_bound);
9506 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
9507 result,
9508 )))
9509 }
9510 None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9511 ExcelError::new_num(),
9512 ))),
9513 }
9514 }
9515}
9516
9517/* ─────────────────────────── BINOM.DIST.RANGE ──────────────────────────── */
9518
9519/// Returns the probability that a binomial random variable falls within a range of successes.
9520///
9521/// # Formula example
9522/// ```excel
9523/// # returns: 0.1171875
9524/// =BINOM.DIST.RANGE(10,0.5,3,3)
9525/// ```
9526///
9527/// ```yaml,sandbox
9528/// title: "Probability of exactly three successes"
9529/// formula: '=BINOM.DIST.RANGE(10,0.5,3,3)'
9530/// expected: 0.1171875
9531/// ```
9532///
9533/// ```yaml,docs
9534/// related:
9535/// - BINOM.DIST
9536/// - BINOM.INV
9537/// - POISSON.DIST
9538/// faq:
9539/// - q: "What happens if the upper bound is omitted?"
9540/// a: "The function treats the lower and upper bounds as the same value, yielding the probability of exactly that many successes."
9541/// ```
9542#[derive(Debug)]
9543pub struct BinomDistRangeFn;
9544/// [formualizer-docgen:schema:start]
9545/// Name: BINOM.DIST.RANGE
9546/// Type: BinomDistRangeFn
9547/// Min args: 3
9548/// Max args: variadic
9549/// Variadic: true
9550/// Signature: BINOM.DIST.RANGE(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4...: number@scalar)
9551/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg4{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
9552/// Caps: PURE
9553/// [formualizer-docgen:schema:end]
9554impl Function for BinomDistRangeFn {
9555 func_caps!(PURE);
9556 fn name(&self) -> &'static str {
9557 "BINOM.DIST.RANGE"
9558 }
9559 fn min_args(&self) -> usize {
9560 3
9561 }
9562 fn variadic(&self) -> bool {
9563 true
9564 }
9565 fn arg_schema(&self) -> &'static [ArgSchema] {
9566 use std::sync::LazyLock;
9567 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
9568 vec![
9569 ArgSchema::number_lenient_scalar(),
9570 ArgSchema::number_lenient_scalar(),
9571 ArgSchema::number_lenient_scalar(),
9572 ArgSchema::number_lenient_scalar(),
9573 ]
9574 });
9575 &SCHEMA[..]
9576 }
9577 fn eval<'a, 'b, 'c>(
9578 &self,
9579 args: &'c [ArgumentHandle<'a, 'b>],
9580 _ctx: &dyn FunctionContext<'b>,
9581 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
9582 let n = coerce_num(&scalar_like_value(&args[0])?)?.trunc() as i64;
9583 let p = coerce_num(&scalar_like_value(&args[1])?)?;
9584 let s = coerce_num(&scalar_like_value(&args[2])?)?.trunc() as i64;
9585 let s2 = if args.len() > 3 {
9586 coerce_num(&scalar_like_value(&args[3])?)?.trunc() as i64
9587 } else {
9588 s
9589 };
9590
9591 if n < 0 || !(0.0..=1.0).contains(&p) || s < 0 || s > n || s2 < s || s2 > n {
9592 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9593 ExcelError::new_num(),
9594 )));
9595 }
9596
9597 let mut sum = 0.0;
9598 for k in s..=s2 {
9599 let ln_prob = ln_binom(n, k) + (k as f64) * p.ln() + ((n - k) as f64) * (1.0 - p).ln();
9600 sum += ln_prob.exp();
9601 }
9602 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(sum)))
9603 }
9604}
9605
9606/* ─────────────────────────── BINOM.INV ──────────────────────────── */
9607
9608/// Returns the smallest number of successes whose cumulative binomial probability meets a threshold.
9609///
9610/// # Formula example
9611/// ```excel
9612/// # returns: 5
9613/// =BINOM.INV(10,0.5,0.5)
9614/// ```
9615///
9616/// ```yaml,sandbox
9617/// title: "Median success threshold"
9618/// formula: '=BINOM.INV(10,0.5,0.5)'
9619/// expected: 5
9620/// ```
9621///
9622/// ```yaml,docs
9623/// related:
9624/// - BINOM.DIST
9625/// - BINOM.DIST.RANGE
9626/// - CRITBINOM
9627/// faq:
9628/// - q: "Is CRITBINOM supported?"
9629/// a: "Yes. CRITBINOM is registered as an alias of BINOM.INV."
9630/// ```
9631#[derive(Debug)]
9632pub struct BinomInvFn;
9633/// [formualizer-docgen:schema:start]
9634/// Name: BINOM.INV
9635/// Type: BinomInvFn
9636/// Min args: 3
9637/// Max args: 3
9638/// Variadic: false
9639/// Signature: BINOM.INV(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
9640/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
9641/// Caps: PURE
9642/// [formualizer-docgen:schema:end]
9643impl Function for BinomInvFn {
9644 func_caps!(PURE);
9645 fn name(&self) -> &'static str {
9646 "BINOM.INV"
9647 }
9648 fn aliases(&self) -> &'static [&'static str] {
9649 &["CRITBINOM"]
9650 }
9651 fn min_args(&self) -> usize {
9652 3
9653 }
9654 fn arg_schema(&self) -> &'static [ArgSchema] {
9655 use std::sync::LazyLock;
9656 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
9657 vec![
9658 ArgSchema::number_lenient_scalar(),
9659 ArgSchema::number_lenient_scalar(),
9660 ArgSchema::number_lenient_scalar(),
9661 ]
9662 });
9663 &SCHEMA[..]
9664 }
9665 fn eval<'a, 'b, 'c>(
9666 &self,
9667 args: &'c [ArgumentHandle<'a, 'b>],
9668 _ctx: &dyn FunctionContext<'b>,
9669 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
9670 let n = coerce_num(&scalar_like_value(&args[0])?)?.trunc() as i64;
9671 let p = coerce_num(&scalar_like_value(&args[1])?)?;
9672 let alpha = coerce_num(&scalar_like_value(&args[2])?)?;
9673
9674 if n < 0 || !(0.0..=1.0).contains(&p) || !(0.0..=1.0).contains(&alpha) {
9675 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9676 ExcelError::new_num(),
9677 )));
9678 }
9679
9680 // Find smallest k such that BINOM.DIST(k, n, p, TRUE) >= alpha
9681 let mut cum = 0.0;
9682 for k in 0..=n {
9683 let ln_prob = ln_binom(n, k) + (k as f64) * p.ln() + ((n - k) as f64) * (1.0 - p).ln();
9684 cum += ln_prob.exp();
9685 if cum >= alpha {
9686 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
9687 k as f64,
9688 )));
9689 }
9690 }
9691 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
9692 n as f64,
9693 )))
9694 }
9695}
9696
9697/* ─────────────────────────── GAMMA ──────────────────────────── */
9698
9699/// Returns the value of the gamma function.
9700///
9701/// # Formula example
9702/// ```excel
9703/// # returns: 24
9704/// =GAMMA(5)
9705/// ```
9706///
9707/// ```yaml,sandbox
9708/// title: "Gamma extends factorials"
9709/// formula: '=GAMMA(5)'
9710/// expected: 24
9711/// ```
9712///
9713/// ```yaml,docs
9714/// related:
9715/// - GAMMALN
9716/// - GAMMA.INV
9717/// - FACT
9718/// faq:
9719/// - q: "When does GAMMA return #NUM!?"
9720/// a: "It returns #NUM! for zero and negative integers, where the gamma function has poles."
9721/// ```
9722#[derive(Debug)]
9723pub struct GammaFn;
9724/// [formualizer-docgen:schema:start]
9725/// Name: GAMMA
9726/// Type: GammaFn
9727/// Min args: 1
9728/// Max args: 1
9729/// Variadic: false
9730/// Signature: GAMMA(arg1: number@scalar)
9731/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
9732/// Caps: PURE
9733/// [formualizer-docgen:schema:end]
9734impl Function for GammaFn {
9735 func_caps!(PURE);
9736 fn name(&self) -> &'static str {
9737 "GAMMA"
9738 }
9739 fn min_args(&self) -> usize {
9740 1
9741 }
9742 fn arg_schema(&self) -> &'static [ArgSchema] {
9743 use std::sync::LazyLock;
9744 static SCHEMA: LazyLock<Vec<ArgSchema>> =
9745 LazyLock::new(|| vec![ArgSchema::number_lenient_scalar()]);
9746 &SCHEMA[..]
9747 }
9748 fn eval<'a, 'b, 'c>(
9749 &self,
9750 args: &'c [ArgumentHandle<'a, 'b>],
9751 _ctx: &dyn FunctionContext<'b>,
9752 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
9753 let x = coerce_num(&scalar_like_value(&args[0])?)?;
9754 // GAMMA(0) and negative integers are #NUM!
9755 if x == 0.0 || (x < 0.0 && x == x.trunc()) {
9756 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9757 ExcelError::new_num(),
9758 )));
9759 }
9760 let result = ln_gamma(x).exp();
9761 if result.is_infinite() || result.is_nan() {
9762 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9763 ExcelError::new_num(),
9764 )));
9765 }
9766 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
9767 result,
9768 )))
9769 }
9770}
9771
9772/* ─────────────────────────── GAMMA.INV ──────────────────────────── */
9773
9774/// Returns the inverse cumulative gamma distribution.
9775///
9776/// # Formula example
9777/// ```excel
9778/// # returns: 0.6931471805599453
9779/// =GAMMA.INV(0.5,1,1)
9780/// ```
9781///
9782/// ```yaml,sandbox
9783/// title: "Exponential special case"
9784/// formula: '=GAMMA.INV(0.5,1,1)'
9785/// expected: 0.6931471805599453
9786/// ```
9787///
9788/// ```yaml,docs
9789/// related:
9790/// - GAMMA.DIST
9791/// - GAMMA
9792/// - BETA.INV
9793/// faq:
9794/// - q: "Is GAMMAINV supported?"
9795/// a: "Yes. GAMMAINV is registered as an alias of GAMMA.INV."
9796/// ```
9797#[derive(Debug)]
9798pub struct GammaInvFn;
9799/// [formualizer-docgen:schema:start]
9800/// Name: GAMMA.INV
9801/// Type: GammaInvFn
9802/// Min args: 3
9803/// Max args: 3
9804/// Variadic: false
9805/// Signature: GAMMA.INV(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
9806/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
9807/// Caps: PURE
9808/// [formualizer-docgen:schema:end]
9809impl Function for GammaInvFn {
9810 func_caps!(PURE);
9811 fn name(&self) -> &'static str {
9812 "GAMMA.INV"
9813 }
9814 fn aliases(&self) -> &'static [&'static str] {
9815 &["GAMMAINV"]
9816 }
9817 fn min_args(&self) -> usize {
9818 3
9819 }
9820 fn arg_schema(&self) -> &'static [ArgSchema] {
9821 use std::sync::LazyLock;
9822 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
9823 vec![
9824 ArgSchema::number_lenient_scalar(),
9825 ArgSchema::number_lenient_scalar(),
9826 ArgSchema::number_lenient_scalar(),
9827 ]
9828 });
9829 &SCHEMA[..]
9830 }
9831 fn eval<'a, 'b, 'c>(
9832 &self,
9833 args: &'c [ArgumentHandle<'a, 'b>],
9834 _ctx: &dyn FunctionContext<'b>,
9835 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
9836 let p = coerce_num(&scalar_like_value(&args[0])?)?;
9837 let alpha = coerce_num(&scalar_like_value(&args[1])?)?;
9838 let beta = coerce_num(&scalar_like_value(&args[2])?)?;
9839
9840 if alpha <= 0.0 || beta <= 0.0 || !(0.0..=1.0).contains(&p) {
9841 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9842 ExcelError::new_num(),
9843 )));
9844 }
9845
9846 match gamma_inv_helper(p, alpha, beta) {
9847 Some(result) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
9848 result,
9849 ))),
9850 None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9851 ExcelError::new_num(),
9852 ))),
9853 }
9854 }
9855}
9856
9857/* ─────────────────────────── GAMMALN ──────────────────────────── */
9858
9859/// Returns the natural logarithm of the gamma function.
9860///
9861/// # Formula example
9862/// ```excel
9863/// # returns: 3.1780538303479458
9864/// =GAMMALN(5)
9865/// ```
9866///
9867/// ```yaml,sandbox
9868/// title: "Log gamma at 5"
9869/// formula: '=GAMMALN(5)'
9870/// expected: 3.1780538303479458
9871/// ```
9872///
9873/// ```yaml,docs
9874/// related:
9875/// - GAMMALN.PRECISE
9876/// - GAMMA
9877/// - LN
9878/// faq:
9879/// - q: "When does GAMMALN return #NUM!?"
9880/// a: "It returns #NUM! for zero or negative inputs."
9881/// ```
9882#[derive(Debug)]
9883pub struct GammaLnFn;
9884/// [formualizer-docgen:schema:start]
9885/// Name: GAMMALN
9886/// Type: GammaLnFn
9887/// Min args: 1
9888/// Max args: 1
9889/// Variadic: false
9890/// Signature: GAMMALN(arg1: number@scalar)
9891/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
9892/// Caps: PURE
9893/// [formualizer-docgen:schema:end]
9894impl Function for GammaLnFn {
9895 func_caps!(PURE);
9896 fn name(&self) -> &'static str {
9897 "GAMMALN"
9898 }
9899 fn min_args(&self) -> usize {
9900 1
9901 }
9902 fn arg_schema(&self) -> &'static [ArgSchema] {
9903 use std::sync::LazyLock;
9904 static SCHEMA: LazyLock<Vec<ArgSchema>> =
9905 LazyLock::new(|| vec![ArgSchema::number_lenient_scalar()]);
9906 &SCHEMA[..]
9907 }
9908 fn eval<'a, 'b, 'c>(
9909 &self,
9910 args: &'c [ArgumentHandle<'a, 'b>],
9911 _ctx: &dyn FunctionContext<'b>,
9912 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
9913 let x = coerce_num(&scalar_like_value(&args[0])?)?;
9914 if x <= 0.0 {
9915 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9916 ExcelError::new_num(),
9917 )));
9918 }
9919 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
9920 ln_gamma(x),
9921 )))
9922 }
9923}
9924
9925/* ─────────────────────────── GAMMALN.PRECISE ──────────────────────────── */
9926
9927/// Returns the natural logarithm of the gamma function using Excel's precise naming variant.
9928///
9929/// # Formula example
9930/// ```excel
9931/// # returns: 3.1780538303479458
9932/// =GAMMALN.PRECISE(5)
9933/// ```
9934///
9935/// ```yaml,sandbox
9936/// title: "Precise log gamma at 5"
9937/// formula: '=GAMMALN.PRECISE(5)'
9938/// expected: 3.1780538303479458
9939/// ```
9940///
9941/// ```yaml,docs
9942/// related:
9943/// - GAMMALN
9944/// - GAMMA
9945/// - LN
9946/// faq:
9947/// - q: "How does GAMMALN.PRECISE differ here?"
9948/// a: "This implementation uses the same core log-gamma calculation as GAMMALN, matching Excel's function naming split."
9949/// ```
9950#[derive(Debug)]
9951pub struct GammaLnPreciseFn;
9952/// [formualizer-docgen:schema:start]
9953/// Name: GAMMALN.PRECISE
9954/// Type: GammaLnPreciseFn
9955/// Min args: 1
9956/// Max args: 1
9957/// Variadic: false
9958/// Signature: GAMMALN.PRECISE(arg1: number@scalar)
9959/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
9960/// Caps: PURE
9961/// [formualizer-docgen:schema:end]
9962impl Function for GammaLnPreciseFn {
9963 func_caps!(PURE);
9964 fn name(&self) -> &'static str {
9965 "GAMMALN.PRECISE"
9966 }
9967 fn min_args(&self) -> usize {
9968 1
9969 }
9970 fn arg_schema(&self) -> &'static [ArgSchema] {
9971 use std::sync::LazyLock;
9972 static SCHEMA: LazyLock<Vec<ArgSchema>> =
9973 LazyLock::new(|| vec![ArgSchema::number_lenient_scalar()]);
9974 &SCHEMA[..]
9975 }
9976 fn eval<'a, 'b, 'c>(
9977 &self,
9978 args: &'c [ArgumentHandle<'a, 'b>],
9979 _ctx: &dyn FunctionContext<'b>,
9980 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
9981 let x = coerce_num(&scalar_like_value(&args[0])?)?;
9982 if x <= 0.0 {
9983 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9984 ExcelError::new_num(),
9985 )));
9986 }
9987 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
9988 ln_gamma(x),
9989 )))
9990 }
9991}
9992
9993pub fn register_builtins() {
9994 use std::sync::Arc;
9995 crate::function_registry::register_function(Arc::new(ForecastLinearFn));
9996 crate::function_registry::register_function(Arc::new(LinestFn));
9997 crate::function_registry::register_function(Arc::new(LARGE));
9998 crate::function_registry::register_function(Arc::new(SMALL));
9999 crate::function_registry::register_function(Arc::new(MEDIAN));
10000 crate::function_registry::register_function(Arc::new(StdevSample));
10001 crate::function_registry::register_function(Arc::new(StdevPop));
10002 crate::function_registry::register_function(Arc::new(VarSample));
10003 crate::function_registry::register_function(Arc::new(VarPop));
10004 crate::function_registry::register_function(Arc::new(PercentileInc));
10005 crate::function_registry::register_function(Arc::new(PercentileExc));
10006 crate::function_registry::register_function(Arc::new(QuartileInc));
10007 crate::function_registry::register_function(Arc::new(QuartileExc));
10008 crate::function_registry::register_function(Arc::new(RankEqFn));
10009 crate::function_registry::register_function(Arc::new(RankAvgFn));
10010 crate::function_registry::register_function(Arc::new(ModeSingleFn));
10011 crate::function_registry::register_function(Arc::new(ModeMultiFn));
10012 crate::function_registry::register_function(Arc::new(ProductFn));
10013 crate::function_registry::register_function(Arc::new(GeomeanFn));
10014 crate::function_registry::register_function(Arc::new(HarmeanFn));
10015 crate::function_registry::register_function(Arc::new(AvedevFn));
10016 crate::function_registry::register_function(Arc::new(DevsqFn));
10017 crate::function_registry::register_function(Arc::new(MaxIfsFn));
10018 crate::function_registry::register_function(Arc::new(MinIfsFn));
10019 crate::function_registry::register_function(Arc::new(TrimmeanFn));
10020 crate::function_registry::register_function(Arc::new(CorrelFn));
10021 crate::function_registry::register_function(Arc::new(SlopeFn));
10022 crate::function_registry::register_function(Arc::new(InterceptFn));
10023 // Covariance and correlation functions
10024 crate::function_registry::register_function(Arc::new(CovariancePFn));
10025 crate::function_registry::register_function(Arc::new(CovarianceSFn));
10026 crate::function_registry::register_function(Arc::new(PearsonFn));
10027 crate::function_registry::register_function(Arc::new(RsqFn));
10028 crate::function_registry::register_function(Arc::new(SteyxFn));
10029 crate::function_registry::register_function(Arc::new(SkewFn));
10030 crate::function_registry::register_function(Arc::new(KurtFn));
10031 crate::function_registry::register_function(Arc::new(FisherFn));
10032 crate::function_registry::register_function(Arc::new(FisherInvFn));
10033 // Statistical distributions
10034 crate::function_registry::register_function(Arc::new(NormSDistFn));
10035 crate::function_registry::register_function(Arc::new(NormSInvFn));
10036 crate::function_registry::register_function(Arc::new(NormDistFn));
10037 crate::function_registry::register_function(Arc::new(NormInvFn));
10038 crate::function_registry::register_function(Arc::new(LognormDistFn));
10039 crate::function_registry::register_function(Arc::new(LognormInvFn));
10040 crate::function_registry::register_function(Arc::new(PhiFn));
10041 crate::function_registry::register_function(Arc::new(GaussFn));
10042 crate::function_registry::register_function(Arc::new(StandardizeFn));
10043 crate::function_registry::register_function(Arc::new(TDistFn));
10044 crate::function_registry::register_function(Arc::new(TInvFn));
10045 crate::function_registry::register_function(Arc::new(ChisqDistFn));
10046 crate::function_registry::register_function(Arc::new(ChisqInvFn));
10047 crate::function_registry::register_function(Arc::new(FDistFn));
10048 crate::function_registry::register_function(Arc::new(FInvFn));
10049 // Discrete distributions
10050 crate::function_registry::register_function(Arc::new(BinomDistFn));
10051 crate::function_registry::register_function(Arc::new(PoissonDistFn));
10052 crate::function_registry::register_function(Arc::new(ExponDistFn));
10053 crate::function_registry::register_function(Arc::new(GammaDistFn));
10054 // Additional distributions
10055 crate::function_registry::register_function(Arc::new(WeibullDistFn));
10056 crate::function_registry::register_function(Arc::new(BetaDistFn));
10057 crate::function_registry::register_function(Arc::new(NegbinomDistFn));
10058 crate::function_registry::register_function(Arc::new(HypgeomDistFn));
10059 // Confidence intervals and hypothesis testing
10060 crate::function_registry::register_function(Arc::new(ConfidenceNormFn));
10061 crate::function_registry::register_function(Arc::new(ConfidenceTFn));
10062 crate::function_registry::register_function(Arc::new(ZTestFn));
10063 // Regression and trend functions
10064 crate::function_registry::register_function(Arc::new(TrendFn));
10065 crate::function_registry::register_function(Arc::new(GrowthFn));
10066 crate::function_registry::register_function(Arc::new(LogestFn));
10067 // Percent rank and frequency functions
10068 crate::function_registry::register_function(Arc::new(PercentRankIncFn));
10069 crate::function_registry::register_function(Arc::new(PercentRankExcFn));
10070 crate::function_registry::register_function(Arc::new(FrequencyFn));
10071 // Hypothesis testing functions
10072 crate::function_registry::register_function(Arc::new(TDist2TFn));
10073 crate::function_registry::register_function(Arc::new(TInv2TFn));
10074 crate::function_registry::register_function(Arc::new(TTestFn));
10075 crate::function_registry::register_function(Arc::new(FTestFn));
10076 crate::function_registry::register_function(Arc::new(ChisqTestFn));
10077 // FZ-PAR-01 batch
10078 crate::function_registry::register_function(Arc::new(AverageAFn));
10079 crate::function_registry::register_function(Arc::new(MaxAFn));
10080 crate::function_registry::register_function(Arc::new(MinAFn));
10081 crate::function_registry::register_function(Arc::new(StdevAFn));
10082 crate::function_registry::register_function(Arc::new(StdevPAFn));
10083 crate::function_registry::register_function(Arc::new(VarAFn));
10084 crate::function_registry::register_function(Arc::new(VarPAFn));
10085 crate::function_registry::register_function(Arc::new(SkewPFn));
10086 crate::function_registry::register_function(Arc::new(TDistRtFn));
10087 crate::function_registry::register_function(Arc::new(ChisqDistRtFn));
10088 crate::function_registry::register_function(Arc::new(ChisqInvRtFn));
10089 crate::function_registry::register_function(Arc::new(FDistRtFn));
10090 crate::function_registry::register_function(Arc::new(FInvRtFn));
10091 crate::function_registry::register_function(Arc::new(BetaInvFn));
10092 crate::function_registry::register_function(Arc::new(BinomDistRangeFn));
10093 crate::function_registry::register_function(Arc::new(BinomInvFn));
10094 crate::function_registry::register_function(Arc::new(GammaFn));
10095 crate::function_registry::register_function(Arc::new(GammaInvFn));
10096 crate::function_registry::register_function(Arc::new(GammaLnFn));
10097 crate::function_registry::register_function(Arc::new(GammaLnPreciseFn));
10098}
10099
10100#[cfg(test)]
10101mod tests_basic_stats {
10102 use super::*;
10103 use crate::test_workbook::TestWorkbook;
10104 use crate::traits::ArgumentHandle;
10105 use formualizer_common::LiteralValue;
10106 use formualizer_parse::parser::{ASTNode, ASTNodeType};
10107 fn interp(wb: &TestWorkbook) -> crate::interpreter::Interpreter<'_> {
10108 wb.interpreter()
10109 }
10110 fn arr(vals: Vec<f64>) -> ASTNode {
10111 ASTNode::new(
10112 ASTNodeType::Literal(LiteralValue::Array(vec![
10113 vals.into_iter().map(LiteralValue::Number).collect(),
10114 ])),
10115 None,
10116 )
10117 }
10118 #[test]
10119 fn median_even() {
10120 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(MEDIAN));
10121 let ctx = interp(&wb);
10122 let node = arr(vec![1.0, 3.0, 5.0, 7.0]);
10123 let f = ctx.context.get_function("", "MEDIAN").unwrap();
10124 let out = f
10125 .dispatch(
10126 &[ArgumentHandle::new(&node, &ctx)],
10127 &ctx.function_context(None),
10128 )
10129 .unwrap();
10130 assert_eq!(out, LiteralValue::Number(4.0));
10131 }
10132 #[test]
10133 fn median_odd() {
10134 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(MEDIAN));
10135 let ctx = interp(&wb);
10136 let node = arr(vec![1.0, 9.0, 5.0]);
10137 let f = ctx.context.get_function("", "MEDIAN").unwrap();
10138 let out = f
10139 .dispatch(
10140 &[ArgumentHandle::new(&node, &ctx)],
10141 &ctx.function_context(None),
10142 )
10143 .unwrap();
10144 assert_eq!(out, LiteralValue::Number(5.0));
10145 }
10146 #[test]
10147 fn large_basic() {
10148 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(LARGE));
10149 let ctx = interp(&wb);
10150 let nums = arr(vec![10.0, 20.0, 30.0]);
10151 let k = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(2.0)), None);
10152 let f = ctx.context.get_function("", "LARGE").unwrap();
10153 let out = f
10154 .dispatch(
10155 &[
10156 ArgumentHandle::new(&nums, &ctx),
10157 ArgumentHandle::new(&k, &ctx),
10158 ],
10159 &ctx.function_context(None),
10160 )
10161 .unwrap();
10162 assert_eq!(out, LiteralValue::Number(20.0));
10163 }
10164 #[test]
10165 fn small_basic() {
10166 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(SMALL));
10167 let ctx = interp(&wb);
10168 let nums = arr(vec![10.0, 20.0, 30.0]);
10169 let k = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(2.0)), None);
10170 let f = ctx.context.get_function("", "SMALL").unwrap();
10171 let out = f
10172 .dispatch(
10173 &[
10174 ArgumentHandle::new(&nums, &ctx),
10175 ArgumentHandle::new(&k, &ctx),
10176 ],
10177 &ctx.function_context(None),
10178 )
10179 .unwrap();
10180 assert_eq!(out, LiteralValue::Number(20.0));
10181 }
10182 #[test]
10183 fn percentile_inc_quarter() {
10184 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(PercentileInc));
10185 let ctx = interp(&wb);
10186 let nums = arr(vec![1.0, 2.0, 3.0, 4.0]);
10187 let p = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(0.25)), None);
10188 let f = ctx.context.get_function("", "PERCENTILE.INC").unwrap();
10189 match f
10190 .dispatch(
10191 &[
10192 ArgumentHandle::new(&nums, &ctx),
10193 ArgumentHandle::new(&p, &ctx),
10194 ],
10195 &ctx.function_context(None),
10196 )
10197 .unwrap()
10198 .into_literal()
10199 {
10200 LiteralValue::Number(v) => assert!((v - 1.75).abs() < 1e-9),
10201 other => panic!("unexpected {other:?}"),
10202 }
10203 }
10204 #[test]
10205 fn rank_eq_descending() {
10206 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(RankEqFn));
10207 let ctx = interp(&wb);
10208 // target 5 among {10,5,1} descending => ranks 1,2,3 => expect 2
10209 let target = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(5.0)), None);
10210 let arr_node = arr(vec![10.0, 5.0, 1.0]);
10211 let f = ctx.context.get_function("", "RANK.EQ").unwrap();
10212 let out = f
10213 .dispatch(
10214 &[
10215 ArgumentHandle::new(&target, &ctx),
10216 ArgumentHandle::new(&arr_node, &ctx),
10217 ],
10218 &ctx.function_context(None),
10219 )
10220 .unwrap();
10221 assert_eq!(out, LiteralValue::Number(2.0));
10222 }
10223 #[test]
10224 fn rank_eq_ascending_order_arg() {
10225 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(RankEqFn));
10226 let ctx = interp(&wb);
10227 // ascending order=1: array {1,5,10}; target 5 => rank 2
10228 let target = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(5.0)), None);
10229 let arr_node = arr(vec![1.0, 5.0, 10.0]);
10230 let order = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(1.0)), None);
10231 let f = ctx.context.get_function("", "RANK.EQ").unwrap();
10232 let out = f
10233 .dispatch(
10234 &[
10235 ArgumentHandle::new(&target, &ctx),
10236 ArgumentHandle::new(&arr_node, &ctx),
10237 ArgumentHandle::new(&order, &ctx),
10238 ],
10239 &ctx.function_context(None),
10240 )
10241 .unwrap();
10242 assert_eq!(out, LiteralValue::Number(2.0));
10243 }
10244 #[test]
10245 fn rank_avg_ties() {
10246 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(RankAvgFn));
10247 let ctx = interp(&wb);
10248 // descending array {5,5,1} target 5 positions 1 and 2 avg -> 1.5
10249 let target = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(5.0)), None);
10250 let arr_node = arr(vec![5.0, 5.0, 1.0]);
10251 let f = ctx.context.get_function("", "RANK.AVG").unwrap();
10252 let out = f
10253 .dispatch(
10254 &[
10255 ArgumentHandle::new(&target, &ctx),
10256 ArgumentHandle::new(&arr_node, &ctx),
10257 ],
10258 &ctx.function_context(None),
10259 )
10260 .unwrap()
10261 .into_literal();
10262 match out {
10263 LiteralValue::Number(v) => assert!((v - 1.5).abs() < 1e-12),
10264 other => panic!("expected number got {other:?}"),
10265 }
10266 }
10267 #[test]
10268 fn stdev_var_sample_population() {
10269 let wb = TestWorkbook::new()
10270 .with_function(std::sync::Arc::new(StdevSample))
10271 .with_function(std::sync::Arc::new(StdevPop))
10272 .with_function(std::sync::Arc::new(VarSample))
10273 .with_function(std::sync::Arc::new(VarPop));
10274 let ctx = interp(&wb);
10275 let arr_node = arr(vec![2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0]); // variance population = 4, sample = 4.571428...
10276 let stdev_p = ctx.context.get_function("", "STDEV.P").unwrap();
10277 let stdev_s = ctx.context.get_function("", "STDEV.S").unwrap();
10278 let var_p = ctx.context.get_function("", "VAR.P").unwrap();
10279 let var_s = ctx.context.get_function("", "VAR.S").unwrap();
10280 let args = [ArgumentHandle::new(&arr_node, &ctx)];
10281 match var_p
10282 .dispatch(&args, &ctx.function_context(None))
10283 .unwrap()
10284 .into_literal()
10285 {
10286 LiteralValue::Number(v) => assert!((v - 4.0).abs() < 1e-12),
10287 other => panic!("unexpected {other:?}"),
10288 }
10289 match var_s
10290 .dispatch(&args, &ctx.function_context(None))
10291 .unwrap()
10292 .into_literal()
10293 {
10294 LiteralValue::Number(v) => assert!((v - 4.571428571428571).abs() < 1e-9),
10295 other => panic!("unexpected {other:?}"),
10296 }
10297 match stdev_p
10298 .dispatch(&args, &ctx.function_context(None))
10299 .unwrap()
10300 .into_literal()
10301 {
10302 LiteralValue::Number(v) => assert!((v - 2.0).abs() < 1e-12),
10303 other => panic!("unexpected {other:?}"),
10304 }
10305 match stdev_s
10306 .dispatch(&args, &ctx.function_context(None))
10307 .unwrap()
10308 .into_literal()
10309 {
10310 LiteralValue::Number(v) => assert!((v - 2.138089935).abs() < 1e-9),
10311 other => panic!("unexpected {other:?}"),
10312 }
10313 }
10314 #[test]
10315 fn quartile_inc_exc() {
10316 let wb = TestWorkbook::new()
10317 .with_function(std::sync::Arc::new(QuartileInc))
10318 .with_function(std::sync::Arc::new(QuartileExc));
10319 let ctx = interp(&wb);
10320 let arr_node = arr(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
10321 let q1 = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(1.0)), None);
10322 let q2 = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(2.0)), None);
10323 let f_inc = ctx.context.get_function("", "QUARTILE.INC").unwrap();
10324 let f_exc = ctx.context.get_function("", "QUARTILE.EXC").unwrap();
10325 let arg_inc_q1 = [
10326 ArgumentHandle::new(&arr_node, &ctx),
10327 ArgumentHandle::new(&q1, &ctx),
10328 ];
10329 let arg_inc_q2 = [
10330 ArgumentHandle::new(&arr_node, &ctx),
10331 ArgumentHandle::new(&q2, &ctx),
10332 ];
10333 match f_inc
10334 .dispatch(&arg_inc_q1, &ctx.function_context(None))
10335 .unwrap()
10336 .into_literal()
10337 {
10338 LiteralValue::Number(v) => assert!((v - 2.0).abs() < 1e-12),
10339 other => panic!("unexpected {other:?}"),
10340 }
10341 match f_inc
10342 .dispatch(&arg_inc_q2, &ctx.function_context(None))
10343 .unwrap()
10344 .into_literal()
10345 {
10346 LiteralValue::Number(v) => assert!((v - 3.0).abs() < 1e-12),
10347 other => panic!("unexpected {other:?}"),
10348 }
10349 // QUARTILE.EXC Q1 for 5-point set uses exclusive percentile => 1.5
10350 match f_exc
10351 .dispatch(&arg_inc_q1, &ctx.function_context(None))
10352 .unwrap()
10353 .into_literal()
10354 {
10355 LiteralValue::Number(v) => assert!((v - 1.5).abs() < 1e-12),
10356 other => panic!("unexpected {other:?}"),
10357 }
10358 match f_exc
10359 .dispatch(&arg_inc_q2, &ctx.function_context(None))
10360 .unwrap()
10361 .into_literal()
10362 {
10363 LiteralValue::Number(v) => assert!((v - 3.0).abs() < 1e-12),
10364 other => panic!("unexpected {other:?}"),
10365 }
10366 }
10367
10368 // --- eval()/dispatch equivalence tests for variance / stdev ---
10369 #[test]
10370 fn fold_equivalence_var_stdev() {
10371 use crate::function::Function as _; // trait import
10372 let wb = TestWorkbook::new()
10373 .with_function(std::sync::Arc::new(VarSample))
10374 .with_function(std::sync::Arc::new(VarPop))
10375 .with_function(std::sync::Arc::new(StdevSample))
10376 .with_function(std::sync::Arc::new(StdevPop));
10377 let ctx = interp(&wb);
10378 let arr_node = arr(vec![1.0, 2.0, 5.0, 5.0, 9.0]);
10379 let args = [ArgumentHandle::new(&arr_node, &ctx)];
10380
10381 let var_s_fn = VarSample; // concrete instance to call eval()
10382 let var_p_fn = VarPop;
10383 let stdev_s_fn = StdevSample;
10384 let stdev_p_fn = StdevPop;
10385
10386 let fctx = ctx.function_context(None);
10387 // Dispatch results (will use fold path)
10388 let disp_var_s = ctx
10389 .context
10390 .get_function("", "VAR.S")
10391 .unwrap()
10392 .dispatch(&args, &fctx)
10393 .unwrap()
10394 .into_literal();
10395 let disp_var_p = ctx
10396 .context
10397 .get_function("", "VAR.P")
10398 .unwrap()
10399 .dispatch(&args, &fctx)
10400 .unwrap()
10401 .into_literal();
10402 let disp_stdev_s = ctx
10403 .context
10404 .get_function("", "STDEV.S")
10405 .unwrap()
10406 .dispatch(&args, &fctx)
10407 .unwrap()
10408 .into_literal();
10409 let disp_stdev_p = ctx
10410 .context
10411 .get_function("", "STDEV.P")
10412 .unwrap()
10413 .dispatch(&args, &fctx)
10414 .unwrap()
10415 .into_literal();
10416
10417 // Scalar path results
10418 let scalar_var_s = var_s_fn.eval(&args, &fctx).unwrap().into_literal();
10419 let scalar_var_p = var_p_fn.eval(&args, &fctx).unwrap().into_literal();
10420 let scalar_stdev_s = stdev_s_fn.eval(&args, &fctx).unwrap().into_literal();
10421 let scalar_stdev_p = stdev_p_fn.eval(&args, &fctx).unwrap().into_literal();
10422
10423 fn assert_close(a: &LiteralValue, b: &LiteralValue) {
10424 match (a, b) {
10425 (LiteralValue::Number(x), LiteralValue::Number(y)) => {
10426 assert!((x - y).abs() < 1e-12, "mismatch {x} vs {y}")
10427 }
10428 _ => assert_eq!(a, b),
10429 }
10430 }
10431 assert_close(&disp_var_s, &scalar_var_s);
10432 assert_close(&disp_var_p, &scalar_var_p);
10433 assert_close(&disp_stdev_s, &scalar_stdev_s);
10434 assert_close(&disp_stdev_p, &scalar_stdev_p);
10435 }
10436
10437 #[test]
10438 fn fold_equivalence_edge_cases() {
10439 use crate::function::Function as _;
10440 let wb = TestWorkbook::new()
10441 .with_function(std::sync::Arc::new(VarSample))
10442 .with_function(std::sync::Arc::new(VarPop))
10443 .with_function(std::sync::Arc::new(StdevSample))
10444 .with_function(std::sync::Arc::new(StdevPop));
10445 let ctx = interp(&wb);
10446 // Single numeric element -> sample variance/div0, population variance 0
10447 let single = arr(vec![42.0]);
10448 let args_single = [ArgumentHandle::new(&single, &ctx)];
10449 let fctx = ctx.function_context(None);
10450 let disp_var_s = ctx
10451 .context
10452 .get_function("", "VAR.S")
10453 .unwrap()
10454 .dispatch(&args_single, &fctx)
10455 .unwrap();
10456 let scalar_var_s = VarSample.eval(&args_single, &fctx).unwrap().into_literal();
10457 assert_eq!(disp_var_s, scalar_var_s);
10458 let disp_var_p = ctx
10459 .context
10460 .get_function("", "VAR.P")
10461 .unwrap()
10462 .dispatch(&args_single, &fctx)
10463 .unwrap();
10464 let scalar_var_p = VarPop.eval(&args_single, &fctx).unwrap().into_literal();
10465 assert_eq!(disp_var_p, scalar_var_p);
10466 let disp_stdev_p = ctx
10467 .context
10468 .get_function("", "STDEV.P")
10469 .unwrap()
10470 .dispatch(&args_single, &fctx)
10471 .unwrap();
10472 let scalar_stdev_p = StdevPop.eval(&args_single, &fctx).unwrap().into_literal();
10473 assert_eq!(disp_stdev_p, scalar_stdev_p);
10474 let disp_stdev_s = ctx
10475 .context
10476 .get_function("", "STDEV.S")
10477 .unwrap()
10478 .dispatch(&args_single, &fctx)
10479 .unwrap();
10480 let scalar_stdev_s = StdevSample
10481 .eval(&args_single, &fctx)
10482 .unwrap()
10483 .into_literal();
10484 assert_eq!(disp_stdev_s, scalar_stdev_s);
10485 }
10486
10487 #[test]
10488 fn legacy_aliases_match_modern() {
10489 let wb = TestWorkbook::new()
10490 .with_function(std::sync::Arc::new(PercentileInc))
10491 .with_function(std::sync::Arc::new(QuartileInc))
10492 .with_function(std::sync::Arc::new(RankEqFn));
10493 let ctx = interp(&wb);
10494 let arr_node = arr(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
10495 let p = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(0.4)), None);
10496 let q2 = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(2.0)), None);
10497 let target = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(4.0)), None);
10498 let args_p = [
10499 ArgumentHandle::new(&arr_node, &ctx),
10500 ArgumentHandle::new(&p, &ctx),
10501 ];
10502 let args_q = [
10503 ArgumentHandle::new(&arr_node, &ctx),
10504 ArgumentHandle::new(&q2, &ctx),
10505 ];
10506 let args_rank = [
10507 ArgumentHandle::new(&target, &ctx),
10508 ArgumentHandle::new(&arr_node, &ctx),
10509 ];
10510 let modern_p = ctx
10511 .context
10512 .get_function("", "PERCENTILE.INC")
10513 .unwrap()
10514 .dispatch(&args_p, &ctx.function_context(None))
10515 .unwrap()
10516 .into_literal();
10517 let legacy_p = ctx
10518 .context
10519 .get_function("", "PERCENTILE")
10520 .unwrap()
10521 .dispatch(&args_p, &ctx.function_context(None))
10522 .unwrap()
10523 .into_literal();
10524 assert_eq!(modern_p, legacy_p);
10525 let modern_q = ctx
10526 .context
10527 .get_function("", "QUARTILE.INC")
10528 .unwrap()
10529 .dispatch(&args_q, &ctx.function_context(None))
10530 .unwrap()
10531 .into_literal();
10532 let legacy_q = ctx
10533 .context
10534 .get_function("", "QUARTILE")
10535 .unwrap()
10536 .dispatch(&args_q, &ctx.function_context(None))
10537 .unwrap()
10538 .into_literal();
10539 assert_eq!(modern_q, legacy_q);
10540 let modern_rank = ctx
10541 .context
10542 .get_function("", "RANK.EQ")
10543 .unwrap()
10544 .dispatch(&args_rank, &ctx.function_context(None))
10545 .unwrap()
10546 .into_literal();
10547 let legacy_rank = ctx
10548 .context
10549 .get_function("", "RANK")
10550 .unwrap()
10551 .dispatch(&args_rank, &ctx.function_context(None))
10552 .unwrap()
10553 .into_literal();
10554 assert_eq!(modern_rank, legacy_rank);
10555 }
10556
10557 #[test]
10558 fn mode_single_basic_and_alias() {
10559 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(ModeSingleFn));
10560 let ctx = interp(&wb);
10561 let arr_node = arr(vec![5.0, 2.0, 2.0, 3.0, 3.0, 3.0]);
10562 let args = [ArgumentHandle::new(&arr_node, &ctx)];
10563 let mode_sngl = ctx
10564 .context
10565 .get_function("", "MODE.SNGL")
10566 .unwrap()
10567 .dispatch(&args, &ctx.function_context(None))
10568 .unwrap()
10569 .into_literal();
10570 assert_eq!(mode_sngl, LiteralValue::Number(3.0));
10571 let mode_alias = ctx
10572 .context
10573 .get_function("", "MODE")
10574 .unwrap()
10575 .dispatch(&args, &ctx.function_context(None))
10576 .unwrap()
10577 .into_literal();
10578 assert_eq!(mode_alias, mode_sngl);
10579 }
10580
10581 #[test]
10582 fn mode_single_no_duplicates() {
10583 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(ModeSingleFn));
10584 let ctx = interp(&wb);
10585 let arr_node = arr(vec![1.0, 2.0, 3.0]);
10586 let args = [ArgumentHandle::new(&arr_node, &ctx)];
10587 let out = ctx
10588 .context
10589 .get_function("", "MODE.SNGL")
10590 .unwrap()
10591 .dispatch(&args, &ctx.function_context(None))
10592 .unwrap()
10593 .into_literal();
10594 match out {
10595 LiteralValue::Error(e) => assert!(e.to_string().contains("#N/A")),
10596 _ => panic!("expected #N/A"),
10597 }
10598 }
10599
10600 #[test]
10601 fn mode_multi_basic() {
10602 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(ModeMultiFn));
10603 let ctx = interp(&wb);
10604 let arr_node = arr(vec![2.0, 3.0, 2.0, 4.0, 3.0, 5.0, 2.0, 3.0]);
10605 let args = [ArgumentHandle::new(&arr_node, &ctx)];
10606 let out = ctx
10607 .context
10608 .get_function("", "MODE.MULT")
10609 .unwrap()
10610 .dispatch(&args, &ctx.function_context(None))
10611 .unwrap()
10612 .into_literal();
10613 let expected = LiteralValue::Array(vec![
10614 vec![LiteralValue::Number(2.0)],
10615 vec![LiteralValue::Number(3.0)],
10616 ]);
10617 assert_eq!(out, expected);
10618 }
10619
10620 #[test]
10621 fn large_small_fold_vs_scalar() {
10622 let wb = TestWorkbook::new()
10623 .with_function(std::sync::Arc::new(LARGE))
10624 .with_function(std::sync::Arc::new(SMALL));
10625 let ctx = interp(&wb);
10626 let arr_node = arr(vec![10.0, 5.0, 7.0, 12.0, 9.0]);
10627 let k_node = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(2.0)), None);
10628 let args = [
10629 ArgumentHandle::new(&arr_node, &ctx),
10630 ArgumentHandle::new(&k_node, &ctx),
10631 ];
10632 let f_large = ctx.context.get_function("", "LARGE").unwrap();
10633 let disp_large = f_large
10634 .dispatch(&args, &ctx.function_context(None))
10635 .unwrap()
10636 .into_literal();
10637 let scalar_large = LARGE
10638 .eval(&args, &ctx.function_context(None))
10639 .unwrap()
10640 .into_literal();
10641 assert_eq!(disp_large, scalar_large);
10642 let f_small = ctx.context.get_function("", "SMALL").unwrap();
10643 let disp_small = f_small
10644 .dispatch(&args, &ctx.function_context(None))
10645 .unwrap()
10646 .into_literal();
10647 let scalar_small = SMALL
10648 .eval(&args, &ctx.function_context(None))
10649 .unwrap()
10650 .into_literal();
10651 assert_eq!(disp_small, scalar_small);
10652 }
10653
10654 #[test]
10655 fn mode_fold_vs_scalar() {
10656 let wb = TestWorkbook::new()
10657 .with_function(std::sync::Arc::new(ModeSingleFn))
10658 .with_function(std::sync::Arc::new(ModeMultiFn));
10659 let ctx = interp(&wb);
10660 let arr_node = arr(vec![2.0, 3.0, 2.0, 4.0, 3.0, 3.0, 2.0]);
10661 let args = [ArgumentHandle::new(&arr_node, &ctx)];
10662 let f_single = ctx.context.get_function("", "MODE.SNGL").unwrap();
10663 let disp_single = f_single
10664 .dispatch(&args, &ctx.function_context(None))
10665 .unwrap()
10666 .into_literal();
10667 let scalar_single = ModeSingleFn
10668 .eval(&args, &ctx.function_context(None))
10669 .unwrap()
10670 .into_literal();
10671 assert_eq!(disp_single, scalar_single);
10672 let f_multi = ctx.context.get_function("", "MODE.MULT").unwrap();
10673 let disp_multi = f_multi
10674 .dispatch(&args, &ctx.function_context(None))
10675 .unwrap()
10676 .into_literal();
10677 let scalar_multi = ModeMultiFn
10678 .eval(&args, &ctx.function_context(None))
10679 .unwrap()
10680 .into_literal();
10681 assert_eq!(disp_multi, scalar_multi);
10682 }
10683
10684 #[test]
10685 fn median_fold_vs_scalar_even() {
10686 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(MEDIAN));
10687 let ctx = interp(&wb);
10688 let arr_node = arr(vec![7.0, 1.0, 9.0, 5.0]); // sorted: 1,5,7,9 median=(5+7)/2=6
10689 let args = [ArgumentHandle::new(&arr_node, &ctx)];
10690 let f_med = ctx.context.get_function("", "MEDIAN").unwrap();
10691 let disp = f_med
10692 .dispatch(&args, &ctx.function_context(None))
10693 .unwrap()
10694 .into_literal();
10695 let scalar = MEDIAN
10696 .eval(&args, &ctx.function_context(None))
10697 .unwrap()
10698 .into_literal();
10699 assert_eq!(disp, scalar);
10700 assert_eq!(disp, LiteralValue::Number(6.0));
10701 }
10702
10703 #[test]
10704 fn median_fold_vs_scalar_odd() {
10705 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(MEDIAN));
10706 let ctx = interp(&wb);
10707 let arr_node = arr(vec![9.0, 2.0, 5.0]); // sorted 2,5,9 median=5
10708 let args = [ArgumentHandle::new(&arr_node, &ctx)];
10709 let f_med = ctx.context.get_function("", "MEDIAN").unwrap();
10710 let disp = f_med
10711 .dispatch(&args, &ctx.function_context(None))
10712 .unwrap()
10713 .into_literal();
10714 let scalar = MEDIAN
10715 .eval(&args, &ctx.function_context(None))
10716 .unwrap()
10717 .into_literal();
10718 assert_eq!(disp, scalar);
10719 assert_eq!(disp, LiteralValue::Number(5.0));
10720 }
10721
10722 // Newly added edge case tests for statistical semantics.
10723 #[test]
10724 fn percentile_inc_edges() {
10725 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(PercentileInc));
10726 let ctx = interp(&wb);
10727 let arr_node = arr(vec![10.0, 20.0, 30.0, 40.0]);
10728 let p0 = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(0.0)), None);
10729 let p1 = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(1.0)), None);
10730 let f = ctx.context.get_function("", "PERCENTILE.INC").unwrap();
10731 let args0 = [
10732 ArgumentHandle::new(&arr_node, &ctx),
10733 ArgumentHandle::new(&p0, &ctx),
10734 ];
10735 let args1 = [
10736 ArgumentHandle::new(&arr_node, &ctx),
10737 ArgumentHandle::new(&p1, &ctx),
10738 ];
10739 assert_eq!(
10740 f.dispatch(&args0, &ctx.function_context(None))
10741 .unwrap()
10742 .into_literal(),
10743 LiteralValue::Number(10.0)
10744 );
10745 assert_eq!(
10746 f.dispatch(&args1, &ctx.function_context(None))
10747 .unwrap()
10748 .into_literal(),
10749 LiteralValue::Number(40.0)
10750 );
10751 }
10752
10753 #[test]
10754 fn percentile_exc_invalid() {
10755 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(PercentileExc));
10756 let ctx = interp(&wb);
10757 let arr_node = arr(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
10758 let p_bad0 = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(0.0)), None);
10759 let p_bad1 = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(1.0)), None);
10760 let f = ctx.context.get_function("", "PERCENTILE.EXC").unwrap();
10761 for bad in [&p_bad0, &p_bad1] {
10762 let args = [
10763 ArgumentHandle::new(&arr_node, &ctx),
10764 ArgumentHandle::new(bad, &ctx),
10765 ];
10766 match f
10767 .dispatch(&args, &ctx.function_context(None))
10768 .unwrap()
10769 .into_literal()
10770 {
10771 LiteralValue::Error(e) => assert!(e.to_string().contains("#NUM!")),
10772 other => panic!("expected #NUM! got {other:?}"),
10773 }
10774 }
10775 }
10776
10777 #[test]
10778 fn quartile_invalids() {
10779 let wb = TestWorkbook::new()
10780 .with_function(std::sync::Arc::new(QuartileInc))
10781 .with_function(std::sync::Arc::new(QuartileExc));
10782 let ctx = interp(&wb);
10783 let arr_node = arr(vec![1.0, 2.0, 3.0]);
10784 // QUARTILE.INC invalid q=5
10785 let q5 = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(5.0)), None);
10786 let args_bad_inc = [
10787 ArgumentHandle::new(&arr_node, &ctx),
10788 ArgumentHandle::new(&q5, &ctx),
10789 ];
10790 match ctx
10791 .context
10792 .get_function("", "QUARTILE.INC")
10793 .unwrap()
10794 .dispatch(&args_bad_inc, &ctx.function_context(None))
10795 .unwrap()
10796 .into_literal()
10797 {
10798 LiteralValue::Error(e) => assert!(e.to_string().contains("#NUM!")),
10799 other => panic!("expected #NUM! {other:?}"),
10800 }
10801 // QUARTILE.EXC invalid q=0
10802 let q0 = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(0.0)), None);
10803 let args_bad_exc = [
10804 ArgumentHandle::new(&arr_node, &ctx),
10805 ArgumentHandle::new(&q0, &ctx),
10806 ];
10807 match ctx
10808 .context
10809 .get_function("", "QUARTILE.EXC")
10810 .unwrap()
10811 .dispatch(&args_bad_exc, &ctx.function_context(None))
10812 .unwrap()
10813 .into_literal()
10814 {
10815 LiteralValue::Error(e) => assert!(e.to_string().contains("#NUM!")),
10816 other => panic!("expected #NUM! {other:?}"),
10817 }
10818 }
10819
10820 #[test]
10821 fn rank_target_not_found() {
10822 let wb = TestWorkbook::new()
10823 .with_function(std::sync::Arc::new(RankEqFn))
10824 .with_function(std::sync::Arc::new(RankAvgFn));
10825 let ctx = interp(&wb);
10826 let arr_node = arr(vec![1.0, 2.0, 3.0]);
10827 let target = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(4.0)), None);
10828 let args = [
10829 ArgumentHandle::new(&target, &ctx),
10830 ArgumentHandle::new(&arr_node, &ctx),
10831 ];
10832 for name in ["RANK.EQ", "RANK.AVG"] {
10833 match ctx
10834 .context
10835 .get_function("", name)
10836 .unwrap()
10837 .dispatch(&args, &ctx.function_context(None))
10838 .unwrap()
10839 .into_literal()
10840 {
10841 LiteralValue::Error(e) => assert!(e.to_string().contains("#N/A")),
10842 other => panic!("expected #N/A {other:?}"),
10843 }
10844 }
10845 }
10846
10847 #[test]
10848 fn mode_mult_ordering() {
10849 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(ModeMultiFn));
10850 let ctx = interp(&wb);
10851 // Two modes with same frequency; ensure ascending ordering in array result
10852 let arr_node = arr(vec![5.0, 2.0, 2.0, 5.0, 3.0, 7.0, 5.0, 2.0]); // 2 and 5 appear 4 times each
10853 let args = [ArgumentHandle::new(&arr_node, &ctx)];
10854 let out = ctx
10855 .context
10856 .get_function("", "MODE.MULT")
10857 .unwrap()
10858 .dispatch(&args, &ctx.function_context(None))
10859 .unwrap()
10860 .into_literal();
10861 match out {
10862 LiteralValue::Array(rows) => {
10863 let vals: Vec<f64> = rows
10864 .into_iter()
10865 .map(|r| {
10866 if let LiteralValue::Number(n) = r[0] {
10867 n
10868 } else {
10869 panic!("expected number")
10870 }
10871 })
10872 .collect();
10873 assert_eq!(vals, vec![2.0, 5.0]);
10874 }
10875 other => panic!("expected array {other:?}"),
10876 }
10877 }
10878
10879 #[test]
10880 fn boolean_and_text_in_range_are_ignored() {
10881 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(StdevPop));
10882 let ctx = interp(&wb);
10883 let mixed = ASTNode::new(
10884 ASTNodeType::Literal(LiteralValue::Array(vec![vec![
10885 LiteralValue::Number(1.0),
10886 LiteralValue::Text("ABC".into()),
10887 LiteralValue::Boolean(true),
10888 LiteralValue::Number(4.0),
10889 ]])),
10890 None,
10891 );
10892 let f = ctx.context.get_function("", "STDEV.P").unwrap();
10893 let out = f
10894 .dispatch(
10895 &[ArgumentHandle::new(&mixed, &ctx)],
10896 &ctx.function_context(None),
10897 )
10898 .unwrap()
10899 .into_literal();
10900 // NOTE: Inline array literal is treated as a direct scalar argument (not a range reference),
10901 // so boolean TRUE is coerced to 1. Dataset becomes {1,1,4}; population stdev = sqrt(6/3)=sqrt(2).
10902 match out {
10903 LiteralValue::Number(v) => {
10904 assert!((v - 2f64.sqrt()).abs() < 1e-12, "expected sqrt(2) got {v}")
10905 }
10906 other => panic!("unexpected {other:?}"),
10907 }
10908 }
10909
10910 #[test]
10911 fn boolean_direct_arg_coerces() {
10912 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(StdevPop));
10913 let ctx = interp(&wb);
10914 let one = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(1.0)), None);
10915 let t = ASTNode::new(ASTNodeType::Literal(LiteralValue::Boolean(true)), None);
10916 let f = ctx.context.get_function("", "STDEV.P").unwrap();
10917 let args = [
10918 ArgumentHandle::new(&one, &ctx),
10919 ArgumentHandle::new(&t, &ctx),
10920 ];
10921 let out = f
10922 .dispatch(&args, &ctx.function_context(None))
10923 .unwrap()
10924 .into_literal();
10925 assert_eq!(out, LiteralValue::Number(0.0));
10926 }
10927}
10928
10929#[cfg(test)]
10930mod tests_selection_vs_sort_oracle {
10931 //! Property tests: the quickselect paths must be bit-identical to the
10932 //! previous full-sort implementations (the "oracle" below reproduces the
10933 //! removed sort-based code exactly).
10934
10935 use super::*;
10936 use rand::rngs::SmallRng;
10937 use rand::{Rng, SeedableRng};
10938
10939 /* ───── full-sort reference oracle (the pre-quickselect implementation) ───── */
10940
10941 fn sorted_asc(data: &[f64]) -> Vec<f64> {
10942 let mut s = data.to_vec();
10943 s.sort_by(|a, b| a.partial_cmp(b).unwrap());
10944 s
10945 }
10946
10947 fn oracle_large(data: &[f64], k: usize) -> f64 {
10948 let mut s = data.to_vec();
10949 s.sort_by(|a, b| b.partial_cmp(a).unwrap());
10950 s[k - 1]
10951 }
10952
10953 fn oracle_small(data: &[f64], k: usize) -> f64 {
10954 sorted_asc(data)[k - 1]
10955 }
10956
10957 fn oracle_median(data: &[f64]) -> f64 {
10958 let s = sorted_asc(data);
10959 let n = s.len();
10960 let mid = n / 2;
10961 if n % 2 == 1 {
10962 s[mid]
10963 } else {
10964 (s[mid - 1] + s[mid]) / 2.0
10965 }
10966 }
10967
10968 fn oracle_percentile_inc(data: &[f64], p: f64) -> Result<f64, ExcelError> {
10969 let sorted = sorted_asc(data);
10970 if sorted.is_empty() || !(0.0..=1.0).contains(&p) {
10971 return Err(ExcelError::new_num());
10972 }
10973 if sorted.len() == 1 {
10974 return Ok(sorted[0]);
10975 }
10976 let n = sorted.len() as f64;
10977 let rank = p * (n - 1.0);
10978 let lo = rank.floor() as usize;
10979 let hi = rank.ceil() as usize;
10980 if lo == hi {
10981 return Ok(sorted[lo]);
10982 }
10983 let frac = rank - (lo as f64);
10984 Ok(sorted[lo] + (sorted[hi] - sorted[lo]) * frac)
10985 }
10986
10987 fn oracle_percentile_exc(data: &[f64], p: f64) -> Result<f64, ExcelError> {
10988 let sorted = sorted_asc(data);
10989 if sorted.is_empty() || !(0.0..=1.0).contains(&p) || p <= 0.0 || p >= 1.0 {
10990 return Err(ExcelError::new_num());
10991 }
10992 let n = sorted.len() as f64;
10993 let rank = p * (n + 1.0);
10994 if rank < 1.0 || rank > n {
10995 return Err(ExcelError::new_num());
10996 }
10997 let lo = rank.floor();
10998 let hi = rank.ceil();
10999 if (lo - hi).abs() < f64::EPSILON {
11000 return Ok(sorted[(lo as usize) - 1]);
11001 }
11002 let frac = rank - lo;
11003 let lo_idx = (lo as usize) - 1;
11004 let hi_idx = (hi as usize) - 1;
11005 Ok(sorted[lo_idx] + (sorted[hi_idx] - sorted[lo_idx]) * frac)
11006 }
11007
11008 fn assert_bits_eq(new: f64, old: f64, what: &str, data: &[f64]) {
11009 assert_eq!(
11010 new.to_bits(),
11011 old.to_bits(),
11012 "{what}: selection {new:?} != sort oracle {old:?} for {data:?}"
11013 );
11014 }
11015
11016 /// 1000 seeded random vectors (mixed continuous values and a small grid
11017 /// to force exact-duplicate ties) x random k / percentile.
11018 #[test]
11019 fn quickselect_paths_match_full_sort_oracle_bitwise() {
11020 let mut rng = SmallRng::seed_from_u64(0x5EED_57A7_u64);
11021 for _case in 0..1000 {
11022 let n = rng.gen_range(1..=64);
11023 let data: Vec<f64> = (0..n)
11024 .map(|_| {
11025 if rng.gen_bool(0.35) {
11026 // Tie-heavy grid: exact duplicates are common.
11027 f64::from(rng.gen_range(-4i32..=4)) * 0.5
11028 } else {
11029 rng.gen_range(-1.0e6..1.0e6)
11030 }
11031 })
11032 .collect();
11033
11034 // LARGE / SMALL across every k (includes k == 1 and k == n).
11035 for k in 1..=n {
11036 let mut buf = data.clone();
11037 let idx = buf.len() - k;
11038 assert_bits_eq(
11039 nth_smallest(&mut buf, idx),
11040 oracle_large(&data, k),
11041 "LARGE",
11042 &data,
11043 );
11044 let mut buf = data.clone();
11045 assert_bits_eq(
11046 nth_smallest(&mut buf, k - 1),
11047 oracle_small(&data, k),
11048 "SMALL",
11049 &data,
11050 );
11051 }
11052
11053 // MEDIAN (odd and even n both covered across cases).
11054 {
11055 let mut buf = data.clone();
11056 let mid = buf.len() / 2;
11057 let med = if buf.len() % 2 == 1 {
11058 nth_smallest(&mut buf, mid)
11059 } else if buf.len() >= 2 {
11060 let (lo, hi) = adjacent_smallest(&mut buf, mid - 1);
11061 (lo + hi) / 2.0
11062 } else {
11063 buf[0]
11064 };
11065 assert_bits_eq(med, oracle_median(&data), "MEDIAN", &data);
11066 }
11067
11068 // PERCENTILE.INC / .EXC with random p, plus the exact endpoints
11069 // and quartile points (0.25/0.5/0.75 also covers QUARTILE.*).
11070 let ps = [
11071 rng.r#gen::<f64>(),
11072 0.0,
11073 0.25,
11074 0.5,
11075 0.75,
11076 1.0,
11077 rng.gen_range(-0.5..1.5), // sometimes out of range
11078 ];
11079 for p in ps {
11080 let mut buf = data.clone();
11081 match (percentile_inc(&mut buf, p), oracle_percentile_inc(&data, p)) {
11082 (Ok(a), Ok(b)) => assert_bits_eq(a, b, "PERCENTILE.INC", &data),
11083 (Err(ea), Err(eb)) => assert_eq!(ea.kind, eb.kind),
11084 (a, b) => panic!("PERCENTILE.INC divergence p={p}: {a:?} vs {b:?}"),
11085 }
11086 let mut buf = data.clone();
11087 match (percentile_exc(&mut buf, p), oracle_percentile_exc(&data, p)) {
11088 (Ok(a), Ok(b)) => assert_bits_eq(a, b, "PERCENTILE.EXC", &data),
11089 (Err(ea), Err(eb)) => assert_eq!(ea.kind, eb.kind),
11090 (a, b) => panic!("PERCENTILE.EXC divergence p={p}: {a:?} vs {b:?}"),
11091 }
11092 }
11093 }
11094 }
11095
11096 /// Empty input keeps the error contract.
11097 #[test]
11098 fn percentile_empty_input_still_num_error() {
11099 let mut empty: Vec<f64> = vec![];
11100 assert!(percentile_inc(&mut empty, 0.5).is_err());
11101 assert!(percentile_exc(&mut empty, 0.5).is_err());
11102 }
11103
11104 /// Perf probe (run explicitly):
11105 /// `cargo test -p formualizer-eval --release --lib large_quickselect_perf_probe -- --ignored --nocapture`
11106 #[test]
11107 #[ignore = "perf probe; run manually with --release --nocapture"]
11108 fn large_quickselect_perf_probe() {
11109 let mut rng = SmallRng::seed_from_u64(42);
11110 let data: Vec<f64> = (0..100_000).map(|_| rng.gen_range(-1.0e9..1.0e9)).collect();
11111 let k = 50usize;
11112 let rounds = 50u32;
11113
11114 let t = std::time::Instant::now();
11115 let mut old_v = 0.0;
11116 for _ in 0..rounds {
11117 let mut s = data.clone();
11118 s.sort_by(|a, b| b.partial_cmp(a).unwrap());
11119 old_v = s[k - 1];
11120 }
11121 let old_t = t.elapsed();
11122
11123 let t = std::time::Instant::now();
11124 let mut new_v = 0.0;
11125 for _ in 0..rounds {
11126 let mut s = data.clone();
11127 let idx = s.len() - k;
11128 new_v = nth_smallest(&mut s, idx);
11129 }
11130 let new_t = t.elapsed();
11131
11132 println!(
11133 "LARGE over 100k x{rounds}: full-sort {:?} ({:?}/op), quickselect {:?} ({:?}/op)",
11134 old_t,
11135 old_t / rounds,
11136 new_t,
11137 new_t / rounds
11138 );
11139 assert_eq!(new_v.to_bits(), old_v.to_bits());
11140 assert!(
11141 new_t < old_t,
11142 "quickselect should beat a full sort on 100k values"
11143 );
11144 }
11145}