formualizer_eval/builtins/stats/mod.rs
1//! Statistical basic functions (Sprint 6)
2//!
3//! Implementations target Excel semantic parity for:
4//! LARGE, SMALL, RANK.EQ, RANK.AVG, MEDIAN, STDEV.S, STDEV.P, VAR.S, VAR.P,
5//! PERCENTILE.INC, PERCENTILE.EXC, QUARTILE.INC, QUARTILE.EXC.
6//!
7//! Notes:
8//! - We currently materialize numeric values into a Vec<f64>. For large ranges this could be
9//! optimized with streaming selection algorithms (nth_element / partial sort). TODO(perf).
10//! - Text/boolean coercion nuance: For Excel statistical functions, values coming from range
11//! references should ignore text and logical values (they are skipped), while direct scalar
12//! arguments still coerce (e.g. =STDEV(1,TRUE) treats TRUE as 1). This file now implements that
13//! distinction. TODO(excel-nuance): refine numeric text literal vs non‑numeric text handling.
14//! - Errors encountered in any argument propagate immediately.
15//! - Empty numeric sets produce Excel-specific errors (#NUM! for LARGE/SMALL, #N/A for rank target
16//! out of range, #DIV/0! for STDEV/VAR sample with n < 2, etc.).
17
18use super::super::builtins::utils::{ARG_RANGE_NUM_LENIENT_ONE, coerce_num};
19use crate::args::ArgSchema;
20use crate::function::Function;
21use crate::function_contract::FunctionDependencyContract;
22use crate::traits::{ArgumentHandle, FunctionContext};
23use formualizer_common::{ExcelError, LiteralValue};
24// use std::collections::BTreeMap; // removed unused import
25use formualizer_macros::func_caps;
26
27fn scalar_like_value(arg: &ArgumentHandle<'_, '_>) -> Result<LiteralValue, ExcelError> {
28 Ok(match arg.value()? {
29 crate::traits::CalcValue::Scalar(v) => v,
30 crate::traits::CalcValue::Range(rv) => rv.get_cell(0, 0),
31 crate::traits::CalcValue::Callable(_) => LiteralValue::Error(
32 ExcelError::new(formualizer_common::ExcelErrorKind::Calc)
33 .with_message("LAMBDA value must be invoked"),
34 ),
35 })
36}
37
38/// Collect numeric inputs applying Excel statistical semantics:
39/// - Range references: include only numeric cells; skip text, logical, blank. Errors propagate.
40/// - Direct scalar arguments: attempt numeric coercion (so TRUE/FALSE, numeric text are included if
41/// coerce_num succeeds). Non-numeric text is ignored (Excel would treat a direct non-numeric text
42/// argument as #VALUE! in some contexts; covered by TODO for finer parity).
43fn collect_numeric_stats(args: &[ArgumentHandle]) -> Result<Vec<f64>, ExcelError> {
44 let mut out = Vec::new();
45 for a in args {
46 // Special-case: inline array literal argument should be treated like a list of direct scalar
47 // arguments (not a by-ref range). This allows boolean/text coercion per element akin to
48 // passing multiple scalars to the function.
49 if let Some(arr) = a.inline_array_literal()? {
50 for row in arr.into_iter() {
51 for cell in row.into_iter() {
52 match cell {
53 LiteralValue::Error(e) => return Err(e),
54 other => {
55 if let Ok(n) = coerce_num(&other) {
56 out.push(n);
57 }
58 }
59 }
60 }
61 }
62 continue;
63 }
64
65 if let Ok(view) = a.range_view() {
66 view.for_each_cell(&mut |v| {
67 match v {
68 LiteralValue::Error(e) => return Err(e.clone()),
69 LiteralValue::Number(n) => out.push(*n),
70 LiteralValue::Int(i) => out.push(*i as f64),
71 _ => {}
72 }
73 Ok(())
74 })?;
75 } else {
76 let v = scalar_like_value(a)?;
77 match v {
78 LiteralValue::Error(e) => return Err(e),
79 other => {
80 if let Ok(n) = coerce_num(&other) {
81 out.push(n);
82 }
83 }
84 }
85 }
86 }
87 Ok(out)
88}
89
90fn percentile_inc(sorted: &[f64], p: f64) -> Result<f64, ExcelError> {
91 if sorted.is_empty() {
92 return Err(ExcelError::new_num());
93 }
94 if !(0.0..=1.0).contains(&p) {
95 return Err(ExcelError::new_num());
96 }
97 if sorted.len() == 1 {
98 return Ok(sorted[0]);
99 }
100 let n = sorted.len() as f64;
101 let rank = p * (n - 1.0); // 0-based rank
102 let lo = rank.floor() as usize;
103 let hi = rank.ceil() as usize;
104 if lo == hi {
105 return Ok(sorted[lo]);
106 }
107 let frac = rank - (lo as f64);
108 Ok(sorted[lo] + (sorted[hi] - sorted[lo]) * frac)
109}
110
111fn percentile_exc(sorted: &[f64], p: f64) -> Result<f64, ExcelError> {
112 // Excel PERCENTILE.EXC requires 0 < p < 1 and uses (n+1) basis; invalid if rank<1 or >n
113 if sorted.is_empty() {
114 return Err(ExcelError::new_num());
115 }
116 if !(0.0..=1.0).contains(&p) || p <= 0.0 || p >= 1.0 {
117 return Err(ExcelError::new_num());
118 }
119 let n = sorted.len() as f64;
120 let rank = p * (n + 1.0); // 1..n domain
121 if rank < 1.0 || rank > n {
122 return Err(ExcelError::new_num());
123 }
124 let lo = rank.floor();
125 let hi = rank.ceil();
126 if (lo - hi).abs() < f64::EPSILON {
127 return Ok(sorted[(lo as usize) - 1]);
128 }
129 let frac = rank - lo;
130 let lo_idx = (lo as usize) - 1;
131 let hi_idx = (hi as usize) - 1;
132 Ok(sorted[lo_idx] + (sorted[hi_idx] - sorted[lo_idx]) * frac)
133}
134
135/// Returns the rank position of a number within a data set, with ties sharing the same rank.
136///
137/// `RANK.EQ` defaults to descending order (largest value is rank 1), and can switch to ascending
138/// order when `order` is non-zero.
139///
140/// # Remarks
141/// - Omitting `order`, or setting `order` to `0`, ranks values in descending order.
142/// - Any non-zero `order` ranks values in ascending order.
143/// - Tied values receive the same rank (the first matching position in the sorted list).
144/// - Returns `#N/A` if `number` is not found in `ref`.
145///
146/// # Examples
147///
148/// ```yaml,sandbox
149/// title: "Descending rank with direct values"
150/// formula: "=RANK.EQ(7,{10,7,4,2})"
151/// expected: 2
152/// ```
153///
154/// ```yaml,sandbox
155/// title: "Ascending rank with ties in a range"
156/// grid:
157/// A1: 50
158/// A2: 20
159/// A3: 20
160/// A4: 10
161/// A5: 5
162/// formula: "=RANK.EQ(A2,A1:A5,1)"
163/// expected: 3
164/// ```
165///
166/// ```yaml,docs
167/// related:
168/// - RANK.AVG
169/// - LARGE
170/// - SMALL
171/// faq:
172/// - q: "When does RANK.EQ return #N/A?"
173/// a: "It returns #N/A when the target number does not appear in the reference set."
174/// ```
175#[derive(Debug)]
176pub struct RankEqFn;
177/// [formualizer-docgen:schema:start]
178/// Name: RANK.EQ
179/// Type: RankEqFn
180/// Min args: 2
181/// Max args: variadic
182/// Variadic: true
183/// Signature: RANK.EQ(arg1...: number@range)
184/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
185/// Caps: PURE, NUMERIC_ONLY
186/// [formualizer-docgen:schema:end]
187impl Function for RankEqFn {
188 func_caps!(PURE, NUMERIC_ONLY);
189 fn name(&self) -> &'static str {
190 "RANK.EQ"
191 }
192 fn aliases(&self) -> &'static [&'static str] {
193 &["RANK"]
194 }
195 fn min_args(&self) -> usize {
196 2
197 }
198 fn variadic(&self) -> bool {
199 true
200 } // allow optional order
201 fn arg_schema(&self) -> &'static [ArgSchema] {
202 &ARG_RANGE_NUM_LENIENT_ONE[..]
203 }
204 fn eval<'a, 'b, 'c>(
205 &self,
206 args: &'c [ArgumentHandle<'a, 'b>],
207 _ctx: &dyn FunctionContext<'b>,
208 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
209 if args.len() < 2 {
210 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
211 ExcelError::new_na(),
212 )));
213 }
214 let target = match coerce_num(&args[0].value()?.into_literal()) {
215 Ok(n) => n,
216 Err(_) => {
217 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
218 ExcelError::new_na(),
219 )));
220 }
221 };
222 // optional order arg at end if 3 args
223 let order = if args.len() >= 3 {
224 coerce_num(&args[2].value()?.into_literal()).unwrap_or(0.0)
225 } else {
226 0.0
227 };
228 let nums = collect_numeric_stats(&args[1..2])?; // only one ref range per Excel spec
229 if nums.is_empty() {
230 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
231 ExcelError::new_na(),
232 )));
233 }
234 let mut sorted = nums; // copy
235 if order.abs() < 1e-12 {
236 // descending
237 sorted.sort_by(|a, b| b.partial_cmp(a).unwrap());
238 } else {
239 // ascending
240 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
241 }
242 for (i, &v) in sorted.iter().enumerate() {
243 if (v - target).abs() < 1e-12 {
244 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
245 (i + 1) as f64,
246 )));
247 }
248 }
249 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
250 ExcelError::new_na(),
251 )))
252 }
253}
254
255/// Returns the rank position of a number, averaging the rank positions for ties.
256///
257/// Use `RANK.AVG` when tied values should share the average of their occupied rank positions.
258///
259/// # Remarks
260/// - Omitting `order`, or setting `order` to `0`, ranks values in descending order.
261/// - Any non-zero `order` ranks values in ascending order.
262/// - If `number` appears multiple times, the function returns the mean of those rank positions.
263/// - Returns `#N/A` if `number` is not found in `ref`.
264///
265/// # Examples
266///
267/// ```yaml,sandbox
268/// title: "Average rank for tied values"
269/// formula: "=RANK.AVG(20,{30,20,20,10})"
270/// expected: 2.5
271/// ```
272///
273/// ```yaml,sandbox
274/// title: "Ascending average rank from a range"
275/// grid:
276/// A1: 50
277/// A2: 20
278/// A3: 20
279/// A4: 10
280/// A5: 5
281/// formula: "=RANK.AVG(A2,A1:A5,1)"
282/// expected: 3.5
283/// ```
284///
285/// ```yaml,docs
286/// related:
287/// - RANK.EQ
288/// - LARGE
289/// - SMALL
290/// faq:
291/// - q: "How are ties handled by RANK.AVG?"
292/// a: "All tied occurrences share the average of their rank positions."
293/// ```
294#[derive(Debug)]
295pub struct RankAvgFn;
296/// [formualizer-docgen:schema:start]
297/// Name: RANK.AVG
298/// Type: RankAvgFn
299/// Min args: 2
300/// Max args: variadic
301/// Variadic: true
302/// Signature: RANK.AVG(arg1...: number@range)
303/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
304/// Caps: PURE, NUMERIC_ONLY
305/// [formualizer-docgen:schema:end]
306impl Function for RankAvgFn {
307 func_caps!(PURE, NUMERIC_ONLY);
308 fn name(&self) -> &'static str {
309 "RANK.AVG"
310 }
311 fn min_args(&self) -> usize {
312 2
313 }
314 fn variadic(&self) -> bool {
315 true
316 }
317 fn arg_schema(&self) -> &'static [ArgSchema] {
318 &ARG_RANGE_NUM_LENIENT_ONE[..]
319 }
320 fn eval<'a, 'b, 'c>(
321 &self,
322 args: &'c [ArgumentHandle<'a, 'b>],
323 _ctx: &dyn FunctionContext<'b>,
324 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
325 if args.len() < 2 {
326 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
327 ExcelError::new_na(),
328 )));
329 }
330 let t0 = scalar_like_value(&args[0])?;
331 let target = match coerce_num(&t0) {
332 Ok(n) => n,
333 Err(_) => {
334 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
335 ExcelError::new_na(),
336 )));
337 }
338 };
339 let order = if args.len() >= 3 {
340 let ord = scalar_like_value(&args[2])?;
341 coerce_num(&ord).unwrap_or(0.0)
342 } else {
343 0.0
344 };
345 let nums = collect_numeric_stats(&args[1..2])?;
346 if nums.is_empty() {
347 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
348 ExcelError::new_na(),
349 )));
350 }
351 let mut sorted = nums;
352 if order.abs() < 1e-12 {
353 sorted.sort_by(|a, b| b.partial_cmp(a).unwrap());
354 } else {
355 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
356 }
357 let mut positions = Vec::new();
358 for (i, &v) in sorted.iter().enumerate() {
359 if (v - target).abs() < 1e-12 {
360 positions.push(i + 1);
361 }
362 }
363 if positions.is_empty() {
364 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
365 ExcelError::new_na(),
366 )));
367 }
368 let avg = positions.iter().copied().sum::<usize>() as f64 / positions.len() as f64;
369 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(avg)))
370 }
371}
372
373/// Returns the k-th largest value in a data set.
374///
375/// `LARGE` is useful for top-N analysis, such as highest score, second-highest sale, or third-best
376/// result.
377///
378/// # Remarks
379/// - `k` must be at least `1`.
380/// - Returns `#NUM!` if `k` is greater than the count of numeric values.
381/// - Non-numeric values in referenced ranges are ignored.
382///
383/// # Examples
384///
385/// ```yaml,sandbox
386/// title: "Second-largest from direct values"
387/// formula: "=LARGE({4,9,1,7},2)"
388/// expected: 7
389/// ```
390///
391/// ```yaml,sandbox
392/// title: "Third-largest from a range"
393/// grid:
394/// A1: 3
395/// A2: 12
396/// A3: 8
397/// A4: 5
398/// formula: "=LARGE(A1:A4,3)"
399/// expected: 5
400/// ```
401///
402/// ```yaml,docs
403/// related:
404/// - SMALL
405/// - MAX
406/// - RANK.EQ
407/// faq:
408/// - q: "When does LARGE return #NUM!?"
409/// a: "It returns #NUM! when k < 1, k exceeds numeric count, or no numeric values exist."
410/// ```
411#[derive(Debug)]
412pub struct LARGE;
413/// [formualizer-docgen:schema:start]
414/// Name: LARGE
415/// Type: LARGE
416/// Min args: 2
417/// Max args: variadic
418/// Variadic: true
419/// Signature: LARGE(arg1...: number@range)
420/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
421/// Caps: PURE, REDUCTION, NUMERIC_ONLY
422/// [formualizer-docgen:schema:end]
423impl Function for LARGE {
424 func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
425 fn name(&self) -> &'static str {
426 "LARGE"
427 }
428 fn min_args(&self) -> usize {
429 2
430 }
431 fn variadic(&self) -> bool {
432 true
433 }
434 fn arg_schema(&self) -> &'static [ArgSchema] {
435 &ARG_RANGE_NUM_LENIENT_ONE[..]
436 }
437 fn eval<'a, 'b, 'c>(
438 &self,
439 args: &'c [ArgumentHandle<'a, 'b>],
440 _ctx: &dyn FunctionContext<'b>,
441 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
442 if args.len() < 2 {
443 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
444 ExcelError::new_num(),
445 )));
446 }
447 let k = match coerce_num(&args.last().unwrap().value()?.into_literal()) {
448 Ok(n) => n,
449 Err(_) => {
450 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
451 ExcelError::new_num(),
452 )));
453 }
454 };
455 let k = k as i64;
456 if k < 1 {
457 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
458 ExcelError::new_num(),
459 )));
460 }
461 let mut nums = collect_numeric_stats(&args[..args.len() - 1])?;
462 if nums.is_empty() || k as usize > nums.len() {
463 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
464 ExcelError::new_num(),
465 )));
466 }
467 nums.sort_by(|a, b| b.partial_cmp(a).unwrap());
468 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
469 nums[(k as usize) - 1],
470 )))
471 }
472}
473
474/// Returns the k-th smallest value in a data set.
475///
476/// `SMALL` is often used to find low outliers, minimum thresholds, or bottom-N values.
477///
478/// # Remarks
479/// - `k` must be at least `1`.
480/// - Returns `#NUM!` if `k` is greater than the count of numeric values.
481/// - Non-numeric values in referenced ranges are ignored.
482///
483/// # Examples
484///
485/// ```yaml,sandbox
486/// title: "Second-smallest from direct values"
487/// formula: "=SMALL({4,9,1,7},2)"
488/// expected: 4
489/// ```
490///
491/// ```yaml,sandbox
492/// title: "Third-smallest from a range"
493/// grid:
494/// A1: 3
495/// A2: 12
496/// A3: 8
497/// A4: 5
498/// formula: "=SMALL(A1:A4,3)"
499/// expected: 8
500/// ```
501///
502/// ```yaml,docs
503/// related:
504/// - LARGE
505/// - MIN
506/// - RANK.EQ
507/// faq:
508/// - q: "Does SMALL include text in referenced ranges?"
509/// a: "No. Non-numeric range values are ignored when selecting the k-th smallest value."
510/// ```
511#[derive(Debug)]
512pub struct SMALL;
513/// [formualizer-docgen:schema:start]
514/// Name: SMALL
515/// Type: SMALL
516/// Min args: 2
517/// Max args: variadic
518/// Variadic: true
519/// Signature: SMALL(arg1...: number@range)
520/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
521/// Caps: PURE, REDUCTION, NUMERIC_ONLY
522/// [formualizer-docgen:schema:end]
523impl Function for SMALL {
524 func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
525 fn name(&self) -> &'static str {
526 "SMALL"
527 }
528 fn min_args(&self) -> usize {
529 2
530 }
531 fn variadic(&self) -> bool {
532 true
533 }
534 fn arg_schema(&self) -> &'static [ArgSchema] {
535 &ARG_RANGE_NUM_LENIENT_ONE[..]
536 }
537 fn eval<'a, 'b, 'c>(
538 &self,
539 args: &'c [ArgumentHandle<'a, 'b>],
540 _ctx: &dyn FunctionContext<'b>,
541 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
542 if args.len() < 2 {
543 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
544 ExcelError::new_num(),
545 )));
546 }
547 let k = match coerce_num(&args.last().unwrap().value()?.into_literal()) {
548 Ok(n) => n,
549 Err(_) => {
550 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
551 ExcelError::new_num(),
552 )));
553 }
554 };
555 let k = k as i64;
556 if k < 1 {
557 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
558 ExcelError::new_num(),
559 )));
560 }
561 let mut nums = collect_numeric_stats(&args[..args.len() - 1])?;
562 if nums.is_empty() || k as usize > nums.len() {
563 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
564 ExcelError::new_num(),
565 )));
566 }
567 nums.sort_by(|a, b| a.partial_cmp(b).unwrap());
568 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
569 nums[(k as usize) - 1],
570 )))
571 }
572}
573
574/// Returns the middle value of a numeric data set.
575///
576/// For an even number of values, `MEDIAN` returns the average of the two center values.
577///
578/// # Remarks
579/// - Ignores non-numeric values in referenced ranges.
580/// - Returns `#NUM!` when no numeric values are available.
581/// - Supports both scalar arguments and range inputs.
582///
583/// # Examples
584///
585/// ```yaml,sandbox
586/// title: "Median of an odd-sized set"
587/// formula: "=MEDIAN(1,3,8)"
588/// expected: 3
589/// ```
590///
591/// ```yaml,sandbox
592/// title: "Median of an even-sized range"
593/// grid:
594/// A1: 1
595/// A2: 2
596/// A3: 10
597/// A4: 12
598/// formula: "=MEDIAN(A1:A4)"
599/// expected: 6
600/// ```
601///
602/// ```yaml,docs
603/// related:
604/// - AVERAGE
605/// - MODE.SNGL
606/// - QUARTILE.INC
607/// faq:
608/// - q: "When does MEDIAN return #NUM!?"
609/// a: "MEDIAN returns #NUM! when no numeric values are available after filtering/coercion."
610/// ```
611#[derive(Debug)]
612pub struct MEDIAN;
613/// [formualizer-docgen:schema:start]
614/// Name: MEDIAN
615/// Type: MEDIAN
616/// Min args: 1
617/// Max args: variadic
618/// Variadic: true
619/// Signature: MEDIAN(arg1...: number@range)
620/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
621/// Caps: PURE, REDUCTION, NUMERIC_ONLY
622/// [formualizer-docgen:schema:end]
623impl Function for MEDIAN {
624 func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
625 fn name(&self) -> &'static str {
626 "MEDIAN"
627 }
628 fn min_args(&self) -> usize {
629 1
630 }
631 fn variadic(&self) -> bool {
632 true
633 }
634 fn arg_schema(&self) -> &'static [ArgSchema] {
635 &ARG_RANGE_NUM_LENIENT_ONE[..]
636 }
637 fn eval<'a, 'b, 'c>(
638 &self,
639 args: &'c [ArgumentHandle<'a, 'b>],
640 _ctx: &dyn FunctionContext<'b>,
641 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
642 let mut nums = collect_numeric_stats(args)?;
643 if nums.is_empty() {
644 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
645 ExcelError::new_num(),
646 )));
647 }
648 nums.sort_by(|a, b| a.partial_cmp(b).unwrap());
649 let n = nums.len();
650 let mid = n / 2;
651 let med = if n % 2 == 1 {
652 nums[mid]
653 } else {
654 (nums[mid - 1] + nums[mid]) / 2.0
655 };
656 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(med)))
657 }
658}
659
660/// Estimates sample standard deviation using `n-1` in the denominator.
661///
662/// `STDEV.S` measures spread when your values represent a sample of a larger population.
663///
664/// # Remarks
665/// - Requires at least two numeric values.
666/// - Returns `#DIV/0!` when fewer than two numeric values are provided.
667/// - Non-numeric values in referenced ranges are ignored.
668///
669/// # Examples
670///
671/// ```yaml,sandbox
672/// title: "Sample standard deviation from scalar arguments"
673/// formula: "=STDEV.S(2,4,6)"
674/// expected: 2
675/// ```
676///
677/// ```yaml,sandbox
678/// title: "Sample standard deviation from a range"
679/// grid:
680/// A1: 5
681/// A2: 7
682/// A3: 9
683/// formula: "=STDEV.S(A1:A3)"
684/// expected: 2
685/// ```
686///
687/// ```yaml,docs
688/// related:
689/// - STDEV.P
690/// - VAR.S
691/// - VAR.P
692/// faq:
693/// - q: "Why does STDEV.S return #DIV/0!?"
694/// a: "Sample standard deviation needs at least two numeric values."
695/// ```
696#[derive(Debug)]
697pub struct StdevSample; // sample
698/// [formualizer-docgen:schema:start]
699/// Name: STDEV.S
700/// Type: StdevSample
701/// Min args: 1
702/// Max args: variadic
703/// Variadic: true
704/// Signature: STDEV.S(arg1...: number@range)
705/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
706/// Caps: PURE, REDUCTION, NUMERIC_ONLY, STREAM_OK
707/// [formualizer-docgen:schema:end]
708impl Function for StdevSample {
709 func_caps!(PURE, NUMERIC_ONLY, REDUCTION, STREAM_OK);
710 fn name(&self) -> &'static str {
711 "STDEV.S"
712 }
713 fn aliases(&self) -> &'static [&'static str] {
714 &["STDEV"]
715 }
716 fn min_args(&self) -> usize {
717 1
718 }
719 fn variadic(&self) -> bool {
720 true
721 }
722 fn arg_schema(&self) -> &'static [ArgSchema] {
723 &ARG_RANGE_NUM_LENIENT_ONE[..]
724 }
725 fn eval<'a, 'b, 'c>(
726 &self,
727 args: &'c [ArgumentHandle<'a, 'b>],
728 _ctx: &dyn FunctionContext<'b>,
729 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
730 let nums = collect_numeric_stats(args)?;
731 let n = nums.len();
732 if n < 2 {
733 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
734 ExcelError::from_error_string("#DIV/0!"),
735 )));
736 }
737 let mean = nums.iter().sum::<f64>() / (n as f64);
738 let mut ss = 0.0;
739 for &v in &nums {
740 let d = v - mean;
741 ss += d * d;
742 }
743 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
744 (ss / ((n - 1) as f64)).sqrt(),
745 )))
746 }
747}
748
749/// Returns population standard deviation using `n` in the denominator.
750///
751/// Use `STDEV.P` when your values represent the entire population, not a sample.
752///
753/// # Remarks
754/// - Requires at least one numeric value.
755/// - Returns `#DIV/0!` when no numeric values are provided.
756/// - Non-numeric values in referenced ranges are ignored.
757///
758/// # Examples
759///
760/// ```yaml,sandbox
761/// title: "Population standard deviation from scalar arguments"
762/// formula: "=STDEV.P(2,4,6)"
763/// expected: 1.632993161855452
764/// ```
765///
766/// ```yaml,sandbox
767/// title: "Population standard deviation from a range"
768/// grid:
769/// A1: 1
770/// A2: 2
771/// A3: 3
772/// formula: "=STDEV.P(A1:A3)"
773/// expected: 0.816496580927726
774/// ```
775///
776/// ```yaml,docs
777/// related:
778/// - STDEV.S
779/// - VAR.P
780/// - VAR.S
781/// faq:
782/// - q: "When does STDEV.P return #DIV/0!?"
783/// a: "It returns #DIV/0! when no numeric values are provided."
784/// ```
785#[derive(Debug)]
786pub struct StdevPop; // population
787/// [formualizer-docgen:schema:start]
788/// Name: STDEV.P
789/// Type: StdevPop
790/// Min args: 1
791/// Max args: variadic
792/// Variadic: true
793/// Signature: STDEV.P(arg1...: number@range)
794/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
795/// Caps: PURE, REDUCTION, NUMERIC_ONLY, STREAM_OK
796/// [formualizer-docgen:schema:end]
797impl Function for StdevPop {
798 func_caps!(PURE, NUMERIC_ONLY, REDUCTION, STREAM_OK);
799 fn name(&self) -> &'static str {
800 "STDEV.P"
801 }
802 fn aliases(&self) -> &'static [&'static str] {
803 &["STDEVP"]
804 }
805 fn min_args(&self) -> usize {
806 1
807 }
808 fn variadic(&self) -> bool {
809 true
810 }
811 fn arg_schema(&self) -> &'static [ArgSchema] {
812 &ARG_RANGE_NUM_LENIENT_ONE[..]
813 }
814 fn eval<'a, 'b, 'c>(
815 &self,
816 args: &'c [ArgumentHandle<'a, 'b>],
817 _ctx: &dyn FunctionContext<'b>,
818 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
819 let nums = collect_numeric_stats(args)?;
820 let n = nums.len();
821 if n == 0 {
822 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
823 ExcelError::from_error_string("#DIV/0!"),
824 )));
825 }
826 let mean = nums.iter().sum::<f64>() / (n as f64);
827 let mut ss = 0.0;
828 for &v in &nums {
829 let d = v - mean;
830 ss += d * d;
831 }
832 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
833 (ss / (n as f64)).sqrt(),
834 )))
835 }
836}
837
838/// Estimates sample variance using `n-1` in the denominator.
839///
840/// `VAR.S` is the squared counterpart of `STDEV.S` for sample-based variability.
841///
842/// # Remarks
843/// - Requires at least two numeric values.
844/// - Returns `#DIV/0!` when fewer than two numeric values are provided.
845/// - Non-numeric values in referenced ranges are ignored.
846///
847/// # Examples
848///
849/// ```yaml,sandbox
850/// title: "Sample variance from scalar arguments"
851/// formula: "=VAR.S(2,4,6)"
852/// expected: 4
853/// ```
854///
855/// ```yaml,sandbox
856/// title: "Sample variance from a range"
857/// grid:
858/// A1: 1
859/// A2: 2
860/// A3: 3
861/// formula: "=VAR.S(A1:A3)"
862/// expected: 1
863/// ```
864///
865/// ```yaml,docs
866/// related:
867/// - VAR.P
868/// - STDEV.S
869/// - STDEV.P
870/// faq:
871/// - q: "Why does VAR.S return #DIV/0!?"
872/// a: "Sample variance requires at least two numeric observations."
873/// ```
874#[derive(Debug)]
875pub struct VarSample; // sample variance
876/// [formualizer-docgen:schema:start]
877/// Name: VAR.S
878/// Type: VarSample
879/// Min args: 1
880/// Max args: variadic
881/// Variadic: true
882/// Signature: VAR.S(arg1...: number@range)
883/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
884/// Caps: PURE, REDUCTION, NUMERIC_ONLY, STREAM_OK
885/// [formualizer-docgen:schema:end]
886impl Function for VarSample {
887 func_caps!(PURE, NUMERIC_ONLY, REDUCTION, STREAM_OK);
888 fn name(&self) -> &'static str {
889 "VAR.S"
890 }
891 fn aliases(&self) -> &'static [&'static str] {
892 &["VAR"]
893 }
894 fn min_args(&self) -> usize {
895 1
896 }
897 fn variadic(&self) -> bool {
898 true
899 }
900 fn arg_schema(&self) -> &'static [ArgSchema] {
901 &ARG_RANGE_NUM_LENIENT_ONE[..]
902 }
903 fn eval<'a, 'b, 'c>(
904 &self,
905 args: &'c [ArgumentHandle<'a, 'b>],
906 _ctx: &dyn FunctionContext<'b>,
907 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
908 let nums = collect_numeric_stats(args)?;
909 let n = nums.len();
910 if n < 2 {
911 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
912 ExcelError::from_error_string("#DIV/0!"),
913 )));
914 }
915 let mean = nums.iter().sum::<f64>() / (n as f64);
916 let mut ss = 0.0;
917 for &v in &nums {
918 let d = v - mean;
919 ss += d * d;
920 }
921 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
922 ss / ((n - 1) as f64),
923 )))
924 }
925}
926
927/// Returns population variance using `n` in the denominator.
928///
929/// `VAR.P` describes dispersion for a complete population of numeric values.
930///
931/// # Remarks
932/// - Requires at least one numeric value.
933/// - Returns `#DIV/0!` when no numeric values are provided.
934/// - Non-numeric values in referenced ranges are ignored.
935///
936/// # Examples
937///
938/// ```yaml,sandbox
939/// title: "Population variance from scalar arguments"
940/// formula: "=VAR.P(2,4,6)"
941/// expected: 2.6666666666666665
942/// ```
943///
944/// ```yaml,sandbox
945/// title: "Population variance from a range"
946/// grid:
947/// A1: 1
948/// A2: 2
949/// A3: 3
950/// formula: "=VAR.P(A1:A3)"
951/// expected: 0.6666666666666666
952/// ```
953///
954/// ```yaml,docs
955/// related:
956/// - VAR.S
957/// - STDEV.P
958/// - STDEV.S
959/// faq:
960/// - q: "What is the denominator difference vs VAR.S?"
961/// a: "VAR.P divides by n, while VAR.S divides by n-1."
962/// ```
963#[derive(Debug)]
964pub struct VarPop; // population variance
965/// [formualizer-docgen:schema:start]
966/// Name: VAR.P
967/// Type: VarPop
968/// Min args: 1
969/// Max args: variadic
970/// Variadic: true
971/// Signature: VAR.P(arg1...: number@range)
972/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
973/// Caps: PURE, REDUCTION, NUMERIC_ONLY, STREAM_OK
974/// [formualizer-docgen:schema:end]
975impl Function for VarPop {
976 func_caps!(PURE, NUMERIC_ONLY, REDUCTION, STREAM_OK);
977 fn name(&self) -> &'static str {
978 "VAR.P"
979 }
980 fn aliases(&self) -> &'static [&'static str] {
981 &["VARP"]
982 }
983 fn min_args(&self) -> usize {
984 1
985 }
986 fn variadic(&self) -> bool {
987 true
988 }
989 fn arg_schema(&self) -> &'static [ArgSchema] {
990 &ARG_RANGE_NUM_LENIENT_ONE[..]
991 }
992 fn eval<'a, 'b, 'c>(
993 &self,
994 args: &'c [ArgumentHandle<'a, 'b>],
995 _ctx: &dyn FunctionContext<'b>,
996 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
997 let nums = collect_numeric_stats(args)?;
998 let n = nums.len();
999 if n == 0 {
1000 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1001 ExcelError::from_error_string("#DIV/0!"),
1002 )));
1003 }
1004 let mean = nums.iter().sum::<f64>() / (n as f64);
1005 let mut ss = 0.0;
1006 for &v in &nums {
1007 let d = v - mean;
1008 ss += d * d;
1009 }
1010 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
1011 ss / (n as f64),
1012 )))
1013 }
1014}
1015
1016// MODE.SNGL (alias MODE) and MODE.MULT
1017/// Returns the most frequently occurring value in a data set.
1018///
1019/// `MODE.SNGL` returns a single mode value and reports `#N/A` if no value repeats.
1020///
1021/// # Remarks
1022/// - Returns the first mode encountered after sorting when frequencies tie.
1023/// - Returns `#N/A` when every numeric value appears only once.
1024/// - Alias `MODE` is supported.
1025///
1026/// # Examples
1027///
1028/// ```yaml,sandbox
1029/// title: "Single mode from scalar arguments"
1030/// formula: "=MODE.SNGL(1,2,2,3)"
1031/// expected: 2
1032/// ```
1033///
1034/// ```yaml,sandbox
1035/// title: "Single mode from a range"
1036/// grid:
1037/// A1: 4
1038/// A2: 4
1039/// A3: 6
1040/// A4: 6
1041/// A5: 6
1042/// formula: "=MODE.SNGL(A1:A5)"
1043/// expected: 6
1044/// ```
1045///
1046/// ```yaml,docs
1047/// related:
1048/// - MODE.MULT
1049/// - MEDIAN
1050/// - AVERAGE
1051/// faq:
1052/// - q: "When does MODE.SNGL return #N/A?"
1053/// a: "It returns #N/A when no value repeats in the numeric dataset."
1054/// ```
1055#[derive(Debug)]
1056pub struct ModeSingleFn;
1057/// [formualizer-docgen:schema:start]
1058/// Name: MODE.SNGL
1059/// Type: ModeSingleFn
1060/// Min args: 1
1061/// Max args: variadic
1062/// Variadic: true
1063/// Signature: MODE.SNGL(arg1...: number@range)
1064/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1065/// Caps: PURE, REDUCTION, NUMERIC_ONLY
1066/// [formualizer-docgen:schema:end]
1067impl Function for ModeSingleFn {
1068 func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
1069 fn name(&self) -> &'static str {
1070 "MODE.SNGL"
1071 }
1072 fn aliases(&self) -> &'static [&'static str] {
1073 &["MODE"]
1074 }
1075 fn min_args(&self) -> usize {
1076 1
1077 }
1078 fn variadic(&self) -> bool {
1079 true
1080 }
1081 fn arg_schema(&self) -> &'static [ArgSchema] {
1082 &ARG_RANGE_NUM_LENIENT_ONE[..]
1083 }
1084 fn eval<'a, 'b, 'c>(
1085 &self,
1086 args: &'c [ArgumentHandle<'a, 'b>],
1087 _ctx: &dyn FunctionContext<'b>,
1088 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1089 let mut nums = collect_numeric_stats(args)?;
1090 if nums.is_empty() {
1091 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1092 ExcelError::new_na(),
1093 )));
1094 }
1095 nums.sort_by(|a, b| a.partial_cmp(b).unwrap());
1096 let mut best_val = nums[0];
1097 let mut best_cnt = 1usize;
1098 let mut cur_val = nums[0];
1099 let mut cur_cnt = 1usize;
1100 for &v in &nums[1..] {
1101 if (v - cur_val).abs() < 1e-12 {
1102 cur_cnt += 1;
1103 } else {
1104 if cur_cnt > best_cnt {
1105 best_cnt = cur_cnt;
1106 best_val = cur_val;
1107 }
1108 cur_val = v;
1109 cur_cnt = 1;
1110 }
1111 }
1112 if cur_cnt > best_cnt {
1113 best_cnt = cur_cnt;
1114 best_val = cur_val;
1115 }
1116 if best_cnt <= 1 {
1117 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1118 ExcelError::new_na(),
1119 )))
1120 } else {
1121 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
1122 best_val,
1123 )))
1124 }
1125 }
1126}
1127
1128/// Returns all modal values as a vertical array.
1129///
1130/// Use `MODE.MULT` when a data set can have multiple values with the same highest frequency.
1131///
1132/// # Remarks
1133/// - Returns each tied mode as a separate row in the result array.
1134/// - Returns `#N/A` when every numeric value appears only once.
1135/// - Non-numeric values in referenced ranges are ignored.
1136///
1137/// # Examples
1138///
1139/// ```yaml,sandbox
1140/// title: "Multiple modes from direct values"
1141/// formula: "=MODE.MULT({1,2,2,3,3,4})"
1142/// expected:
1143/// - [2]
1144/// - [3]
1145/// ```
1146///
1147/// ```yaml,sandbox
1148/// title: "Single repeated mode still returns an array"
1149/// grid:
1150/// A1: 5
1151/// A2: 5
1152/// A3: 2
1153/// A4: 1
1154/// formula: "=MODE.MULT(A1:A4)"
1155/// expected:
1156/// - [5]
1157/// ```
1158///
1159/// ```yaml,docs
1160/// related:
1161/// - MODE.SNGL
1162/// - FREQUENCY
1163/// - MEDIAN
1164/// faq:
1165/// - q: "Why can MODE.MULT return an array result?"
1166/// a: "It emits every value tied for highest frequency as separate rows."
1167/// ```
1168#[derive(Debug)]
1169pub struct ModeMultiFn;
1170/// [formualizer-docgen:schema:start]
1171/// Name: MODE.MULT
1172/// Type: ModeMultiFn
1173/// Min args: 1
1174/// Max args: variadic
1175/// Variadic: true
1176/// Signature: MODE.MULT(arg1...: number@range)
1177/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1178/// Caps: PURE, REDUCTION, NUMERIC_ONLY
1179/// [formualizer-docgen:schema:end]
1180impl Function for ModeMultiFn {
1181 func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
1182 fn name(&self) -> &'static str {
1183 "MODE.MULT"
1184 }
1185 fn min_args(&self) -> usize {
1186 1
1187 }
1188 fn variadic(&self) -> bool {
1189 true
1190 }
1191 fn arg_schema(&self) -> &'static [ArgSchema] {
1192 &ARG_RANGE_NUM_LENIENT_ONE[..]
1193 }
1194 fn eval<'a, 'b, 'c>(
1195 &self,
1196 args: &'c [ArgumentHandle<'a, 'b>],
1197 _ctx: &dyn FunctionContext<'b>,
1198 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1199 let mut nums = collect_numeric_stats(args)?;
1200 if nums.is_empty() {
1201 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1202 ExcelError::new_na(),
1203 )));
1204 }
1205 nums.sort_by(|a, b| a.partial_cmp(b).unwrap());
1206 let mut runs: Vec<(f64, usize)> = Vec::new();
1207 let mut cur_val = nums[0];
1208 let mut cur_cnt = 1usize;
1209 for &v in &nums[1..] {
1210 if (v - cur_val).abs() < 1e-12 {
1211 cur_cnt += 1;
1212 } else {
1213 runs.push((cur_val, cur_cnt));
1214 cur_val = v;
1215 cur_cnt = 1;
1216 }
1217 }
1218 runs.push((cur_val, cur_cnt));
1219 let max_freq = runs.iter().map(|r| r.1).max().unwrap_or(0);
1220 if max_freq <= 1 {
1221 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1222 ExcelError::new_na(),
1223 )));
1224 }
1225 let rows: Vec<Vec<LiteralValue>> = runs
1226 .into_iter()
1227 .filter(|&(_, c)| c == max_freq)
1228 .map(|(v, _)| vec![LiteralValue::Number(v)])
1229 .collect();
1230 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Array(rows)))
1231 }
1232}
1233
1234/// Returns the k-th percentile of a data set using inclusive interpolation.
1235///
1236/// `PERCENTILE.INC` accepts percentile values from `0` through `1` and interpolates between
1237/// sorted values as needed.
1238///
1239/// # Remarks
1240/// - `k` must be in the inclusive range `[0, 1]`.
1241/// - Returns `#NUM!` for empty numeric input or invalid percentile arguments.
1242/// - Alias `PERCENTILE` is supported.
1243///
1244/// # Examples
1245///
1246/// ```yaml,sandbox
1247/// title: "Inclusive 25th percentile from direct values"
1248/// formula: "=PERCENTILE.INC({1,2,3,4,5},0.25)"
1249/// expected: 2
1250/// ```
1251///
1252/// ```yaml,sandbox
1253/// title: "Inclusive median-style interpolation from a range"
1254/// grid:
1255/// A1: 10
1256/// A2: 20
1257/// A3: 30
1258/// A4: 40
1259/// formula: "=PERCENTILE.INC(A1:A4,0.5)"
1260/// expected: 25
1261/// ```
1262///
1263/// ```yaml,docs
1264/// related:
1265/// - PERCENTILE.EXC
1266/// - QUARTILE.INC
1267/// - PERCENTRANK.INC
1268/// faq:
1269/// - q: "What k range is valid for PERCENTILE.INC?"
1270/// a: "k must be between 0 and 1 inclusive; outside that range returns #NUM!."
1271/// ```
1272#[derive(Debug)]
1273pub struct PercentileInc; // inclusive
1274/// [formualizer-docgen:schema:start]
1275/// Name: PERCENTILE.INC
1276/// Type: PercentileInc
1277/// Min args: 2
1278/// Max args: variadic
1279/// Variadic: true
1280/// Signature: PERCENTILE.INC(arg1...: number@range)
1281/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1282/// Caps: PURE, NUMERIC_ONLY
1283/// [formualizer-docgen:schema:end]
1284impl Function for PercentileInc {
1285 func_caps!(PURE, NUMERIC_ONLY);
1286 fn name(&self) -> &'static str {
1287 "PERCENTILE.INC"
1288 }
1289 fn aliases(&self) -> &'static [&'static str] {
1290 &["PERCENTILE"]
1291 }
1292 fn min_args(&self) -> usize {
1293 2
1294 }
1295 fn variadic(&self) -> bool {
1296 true
1297 }
1298 fn arg_schema(&self) -> &'static [ArgSchema] {
1299 &ARG_RANGE_NUM_LENIENT_ONE[..]
1300 }
1301 fn eval<'a, 'b, 'c>(
1302 &self,
1303 args: &'c [ArgumentHandle<'a, 'b>],
1304 _ctx: &dyn FunctionContext<'b>,
1305 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1306 if args.len() < 2 {
1307 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1308 ExcelError::new_num(),
1309 )));
1310 }
1311 let pv = scalar_like_value(args.last().unwrap())?;
1312 let p = match coerce_num(&pv) {
1313 Ok(n) => n,
1314 Err(_) => {
1315 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1316 ExcelError::new_num(),
1317 )));
1318 }
1319 };
1320 let mut nums = collect_numeric_stats(&args[..args.len() - 1])?;
1321 if nums.is_empty() {
1322 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1323 ExcelError::new_num(),
1324 )));
1325 }
1326 nums.sort_by(|a, b| a.partial_cmp(b).unwrap());
1327 match percentile_inc(&nums, p) {
1328 Ok(v) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(v))),
1329 Err(e) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
1330 }
1331 }
1332}
1333
1334/// Returns the k-th percentile of a data set using exclusive interpolation.
1335///
1336/// `PERCENTILE.EXC` uses the `n+1` rank basis and excludes the exact endpoints `0` and `1`.
1337///
1338/// # Remarks
1339/// - `k` must satisfy `0 < k < 1`.
1340/// - Returns `#NUM!` when the percentile falls outside the valid rank range for the data size.
1341/// - Returns `#NUM!` for empty numeric input.
1342///
1343/// # Examples
1344///
1345/// ```yaml,sandbox
1346/// title: "Exclusive 25th percentile from direct values"
1347/// formula: "=PERCENTILE.EXC({1,2,3,4,5},0.25)"
1348/// expected: 1.5
1349/// ```
1350///
1351/// ```yaml,sandbox
1352/// title: "Exclusive percentile from a range"
1353/// grid:
1354/// A1: 10
1355/// A2: 20
1356/// A3: 30
1357/// A4: 40
1358/// A5: 50
1359/// formula: "=PERCENTILE.EXC(A1:A5,0.6)"
1360/// expected: 36
1361/// ```
1362///
1363/// ```yaml,docs
1364/// related:
1365/// - PERCENTILE.INC
1366/// - QUARTILE.EXC
1367/// - PERCENTRANK.EXC
1368/// faq:
1369/// - q: "Why does PERCENTILE.EXC reject k=0 or k=1?"
1370/// a: "Exclusive percentile uses the n+1 basis and requires strictly 0 < k < 1."
1371/// ```
1372#[derive(Debug)]
1373pub struct PercentileExc; // exclusive
1374/// [formualizer-docgen:schema:start]
1375/// Name: PERCENTILE.EXC
1376/// Type: PercentileExc
1377/// Min args: 2
1378/// Max args: variadic
1379/// Variadic: true
1380/// Signature: PERCENTILE.EXC(arg1...: number@range)
1381/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1382/// Caps: PURE, NUMERIC_ONLY
1383/// [formualizer-docgen:schema:end]
1384impl Function for PercentileExc {
1385 func_caps!(PURE, NUMERIC_ONLY);
1386 fn name(&self) -> &'static str {
1387 "PERCENTILE.EXC"
1388 }
1389 fn min_args(&self) -> usize {
1390 2
1391 }
1392 fn variadic(&self) -> bool {
1393 true
1394 }
1395 fn arg_schema(&self) -> &'static [ArgSchema] {
1396 &ARG_RANGE_NUM_LENIENT_ONE[..]
1397 }
1398 fn eval<'a, 'b, 'c>(
1399 &self,
1400 args: &'c [ArgumentHandle<'a, 'b>],
1401 _ctx: &dyn FunctionContext<'b>,
1402 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1403 if args.len() < 2 {
1404 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1405 ExcelError::new_num(),
1406 )));
1407 }
1408 let pv = scalar_like_value(args.last().unwrap())?;
1409 let p = match coerce_num(&pv) {
1410 Ok(n) => n,
1411 Err(_) => {
1412 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1413 ExcelError::new_num(),
1414 )));
1415 }
1416 };
1417 let mut nums = collect_numeric_stats(&args[..args.len() - 1])?;
1418 if nums.is_empty() {
1419 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1420 ExcelError::new_num(),
1421 )));
1422 }
1423 nums.sort_by(|a, b| a.partial_cmp(b).unwrap());
1424 match percentile_exc(&nums, p) {
1425 Ok(v) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(v))),
1426 Err(e) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
1427 }
1428 }
1429}
1430
1431/// Returns an inclusive quartile value for a data set.
1432///
1433/// `QUARTILE.INC` maps quartile index `0..4` onto minimum, quartiles, median, and maximum.
1434///
1435/// # Remarks
1436/// - Valid quartile index values are `0`, `1`, `2`, `3`, and `4`.
1437/// - Uses inclusive percentile logic for quartiles `1` through `3`.
1438/// - Returns `#NUM!` for invalid quartile index values or empty numeric input.
1439/// - Alias `QUARTILE` is supported.
1440///
1441/// # Examples
1442///
1443/// ```yaml,sandbox
1444/// title: "First quartile from direct values"
1445/// formula: "=QUARTILE.INC({1,2,3,4,5},1)"
1446/// expected: 2
1447/// ```
1448///
1449/// ```yaml,sandbox
1450/// title: "Third quartile from a range"
1451/// grid:
1452/// A1: 10
1453/// A2: 20
1454/// A3: 30
1455/// A4: 40
1456/// formula: "=QUARTILE.INC(A1:A4,3)"
1457/// expected: 32.5
1458/// ```
1459///
1460/// ```yaml,docs
1461/// related:
1462/// - QUARTILE.EXC
1463/// - PERCENTILE.INC
1464/// - MEDIAN
1465/// faq:
1466/// - q: "Which quartile numbers are valid for QUARTILE.INC?"
1467/// a: "Only 0 through 4 are valid; other quartile indices return #NUM!."
1468/// ```
1469#[derive(Debug)]
1470pub struct QuartileInc; // quartile inclusive
1471/// [formualizer-docgen:schema:start]
1472/// Name: QUARTILE.INC
1473/// Type: QuartileInc
1474/// Min args: 2
1475/// Max args: variadic
1476/// Variadic: true
1477/// Signature: QUARTILE.INC(arg1...: number@range)
1478/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1479/// Caps: PURE, NUMERIC_ONLY
1480/// [formualizer-docgen:schema:end]
1481impl Function for QuartileInc {
1482 func_caps!(PURE, NUMERIC_ONLY);
1483 fn name(&self) -> &'static str {
1484 "QUARTILE.INC"
1485 }
1486 fn aliases(&self) -> &'static [&'static str] {
1487 &["QUARTILE"]
1488 }
1489 fn min_args(&self) -> usize {
1490 2
1491 }
1492 fn variadic(&self) -> bool {
1493 true
1494 }
1495 fn arg_schema(&self) -> &'static [ArgSchema] {
1496 &ARG_RANGE_NUM_LENIENT_ONE[..]
1497 }
1498 fn eval<'a, 'b, 'c>(
1499 &self,
1500 args: &'c [ArgumentHandle<'a, 'b>],
1501 _ctx: &dyn FunctionContext<'b>,
1502 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1503 if args.len() < 2 {
1504 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1505 ExcelError::new_num(),
1506 )));
1507 }
1508 let qv = scalar_like_value(args.last().unwrap())?;
1509 let q = match coerce_num(&qv) {
1510 Ok(n) => n,
1511 Err(_) => {
1512 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1513 ExcelError::new_num(),
1514 )));
1515 }
1516 };
1517 let q_i = q as i64;
1518 if !(0..=4).contains(&q_i) {
1519 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1520 ExcelError::new_num(),
1521 )));
1522 }
1523 let mut nums = collect_numeric_stats(&args[..args.len() - 1])?;
1524 if nums.is_empty() {
1525 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1526 ExcelError::new_num(),
1527 )));
1528 }
1529 nums.sort_by(|a, b| a.partial_cmp(b).unwrap());
1530 let p = match q_i {
1531 0 => {
1532 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
1533 nums[0],
1534 )));
1535 }
1536 4 => {
1537 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
1538 nums[nums.len() - 1],
1539 )));
1540 }
1541 1 => 0.25,
1542 2 => 0.5,
1543 3 => 0.75,
1544 _ => {
1545 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1546 ExcelError::new_num(),
1547 )));
1548 }
1549 };
1550 match percentile_inc(&nums, p) {
1551 Ok(v) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(v))),
1552 Err(e) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
1553 }
1554 }
1555}
1556
1557/// Returns an exclusive quartile value for a data set.
1558///
1559/// `QUARTILE.EXC` applies exclusive percentile interpolation and supports quartiles `1` through
1560/// `3`.
1561///
1562/// # Remarks
1563/// - Valid quartile index values are `1`, `2`, and `3`.
1564/// - Returns `#NUM!` for invalid quartile index values.
1565/// - Returns `#NUM!` when the input is too small for exclusive quartile interpolation.
1566///
1567/// # Examples
1568///
1569/// ```yaml,sandbox
1570/// title: "First exclusive quartile from direct values"
1571/// formula: "=QUARTILE.EXC({1,2,3,4,5,6,7,8},1)"
1572/// expected: 2.25
1573/// ```
1574///
1575/// ```yaml,sandbox
1576/// title: "Third exclusive quartile from a range"
1577/// grid:
1578/// A1: 10
1579/// A2: 20
1580/// A3: 30
1581/// A4: 40
1582/// A5: 50
1583/// A6: 60
1584/// A7: 70
1585/// A8: 80
1586/// formula: "=QUARTILE.EXC(A1:A8,3)"
1587/// expected: 67.5
1588/// ```
1589///
1590/// ```yaml,docs
1591/// related:
1592/// - QUARTILE.INC
1593/// - PERCENTILE.EXC
1594/// - MEDIAN
1595/// faq:
1596/// - q: "Why can QUARTILE.EXC return #NUM! on small datasets?"
1597/// a: "Exclusive quartiles need enough data for valid interior rank interpolation."
1598/// ```
1599#[derive(Debug)]
1600pub struct QuartileExc; // quartile exclusive
1601/// [formualizer-docgen:schema:start]
1602/// Name: QUARTILE.EXC
1603/// Type: QuartileExc
1604/// Min args: 2
1605/// Max args: variadic
1606/// Variadic: true
1607/// Signature: QUARTILE.EXC(arg1...: number@range)
1608/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1609/// Caps: PURE, NUMERIC_ONLY
1610/// [formualizer-docgen:schema:end]
1611impl Function for QuartileExc {
1612 func_caps!(PURE, NUMERIC_ONLY);
1613 fn name(&self) -> &'static str {
1614 "QUARTILE.EXC"
1615 }
1616 fn min_args(&self) -> usize {
1617 2
1618 }
1619 fn variadic(&self) -> bool {
1620 true
1621 }
1622 fn arg_schema(&self) -> &'static [ArgSchema] {
1623 &ARG_RANGE_NUM_LENIENT_ONE[..]
1624 }
1625 fn eval<'a, 'b, 'c>(
1626 &self,
1627 args: &'c [ArgumentHandle<'a, 'b>],
1628 _ctx: &dyn FunctionContext<'b>,
1629 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1630 if args.len() < 2 {
1631 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1632 ExcelError::new_num(),
1633 )));
1634 }
1635 let qv = scalar_like_value(args.last().unwrap())?;
1636 let q = match coerce_num(&qv) {
1637 Ok(n) => n,
1638 Err(_) => {
1639 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1640 ExcelError::new_num(),
1641 )));
1642 }
1643 };
1644 let q_i = q as i64;
1645 if !(1..=3).contains(&q_i) {
1646 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1647 ExcelError::new_num(),
1648 )));
1649 }
1650 let mut nums = collect_numeric_stats(&args[..args.len() - 1])?;
1651 if nums.len() < 2 {
1652 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1653 ExcelError::new_num(),
1654 )));
1655 }
1656 nums.sort_by(|a, b| a.partial_cmp(b).unwrap());
1657 let p = match q_i {
1658 1 => 0.25,
1659 2 => 0.5,
1660 3 => 0.75,
1661 _ => {
1662 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1663 ExcelError::new_num(),
1664 )));
1665 }
1666 };
1667 match percentile_exc(&nums, p) {
1668 Ok(v) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(v))),
1669 Err(e) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
1670 }
1671 }
1672}
1673
1674/// Multiplies all numeric arguments and returns their product.
1675///
1676/// `PRODUCT` is useful for chained growth factors, scaling ratios, and compound multipliers.
1677///
1678/// # Remarks
1679/// - Non-numeric values in referenced ranges are ignored.
1680/// - Returns `0` when no numeric values are found.
1681/// - Direct scalar arguments still attempt numeric coercion.
1682///
1683/// # Examples
1684///
1685/// ```yaml,sandbox
1686/// title: "Product of scalar values"
1687/// formula: "=PRODUCT(2,3,4)"
1688/// expected: 24
1689/// ```
1690///
1691/// ```yaml,sandbox
1692/// title: "Product from a range"
1693/// grid:
1694/// A1: 1
1695/// A2: 5
1696/// A3: 10
1697/// formula: "=PRODUCT(A1:A3)"
1698/// expected: 50
1699/// ```
1700///
1701/// ```yaml,docs
1702/// related:
1703/// - SUM
1704/// - GEOMEAN
1705/// - SUMPRODUCT
1706/// faq:
1707/// - q: "Why does PRODUCT return 0 when no numeric inputs are found?"
1708/// a: "This implementation returns 0 for an empty numeric set after filtering."
1709/// ```
1710#[derive(Debug)]
1711pub struct ProductFn;
1712/// [formualizer-docgen:schema:start]
1713/// Name: PRODUCT
1714/// Type: ProductFn
1715/// Min args: 1
1716/// Max args: variadic
1717/// Variadic: true
1718/// Signature: PRODUCT(arg1...: number@range)
1719/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1720/// Caps: PURE, REDUCTION, NUMERIC_ONLY
1721/// [formualizer-docgen:schema:end]
1722impl Function for ProductFn {
1723 func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
1724 fn name(&self) -> &'static str {
1725 "PRODUCT"
1726 }
1727 fn min_args(&self) -> usize {
1728 1
1729 }
1730 fn variadic(&self) -> bool {
1731 true
1732 }
1733 fn dependency_contract(&self, arity: usize) -> Option<FunctionDependencyContract> {
1734 FunctionDependencyContract::static_reduction(arity, self.min_args())
1735 }
1736 fn arg_schema(&self) -> &'static [ArgSchema] {
1737 &ARG_RANGE_NUM_LENIENT_ONE[..]
1738 }
1739 fn eval<'a, 'b, 'c>(
1740 &self,
1741 args: &'c [ArgumentHandle<'a, 'b>],
1742 _ctx: &dyn FunctionContext<'b>,
1743 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1744 let nums = collect_numeric_stats(args)?;
1745 if nums.is_empty() {
1746 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0)));
1747 }
1748 let result = nums.iter().product::<f64>();
1749 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
1750 result,
1751 )))
1752 }
1753}
1754
1755/// Returns the geometric mean of positive numeric values.
1756///
1757/// `GEOMEAN` is commonly used for rates of change and multiplicative growth comparisons.
1758///
1759/// # Remarks
1760/// - All numeric inputs must be strictly greater than `0`.
1761/// - Returns `#NUM!` if any value is `<= 0`, or if no numeric values are provided.
1762/// - Non-numeric values in referenced ranges are ignored.
1763///
1764/// # Examples
1765///
1766/// ```yaml,sandbox
1767/// title: "Geometric mean from scalar values"
1768/// formula: "=GEOMEAN(4,16)"
1769/// expected: 8
1770/// ```
1771///
1772/// ```yaml,sandbox
1773/// title: "Geometric mean from a range"
1774/// grid:
1775/// A1: 1
1776/// A2: 3
1777/// A3: 9
1778/// formula: "=GEOMEAN(A1:A3)"
1779/// expected: 3
1780/// ```
1781///
1782/// ```yaml,docs
1783/// related:
1784/// - HARMEAN
1785/// - PRODUCT
1786/// - AVERAGE
1787/// faq:
1788/// - q: "When does GEOMEAN return #NUM!?"
1789/// a: "GEOMEAN returns #NUM! if any numeric value is <= 0 or if no numeric values exist."
1790/// ```
1791#[derive(Debug)]
1792pub struct GeomeanFn;
1793/// [formualizer-docgen:schema:start]
1794/// Name: GEOMEAN
1795/// Type: GeomeanFn
1796/// Min args: 1
1797/// Max args: variadic
1798/// Variadic: true
1799/// Signature: GEOMEAN(arg1...: number@range)
1800/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1801/// Caps: PURE, REDUCTION, NUMERIC_ONLY
1802/// [formualizer-docgen:schema:end]
1803impl Function for GeomeanFn {
1804 func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
1805 fn name(&self) -> &'static str {
1806 "GEOMEAN"
1807 }
1808 fn min_args(&self) -> usize {
1809 1
1810 }
1811 fn variadic(&self) -> bool {
1812 true
1813 }
1814 fn arg_schema(&self) -> &'static [ArgSchema] {
1815 &ARG_RANGE_NUM_LENIENT_ONE[..]
1816 }
1817 fn eval<'a, 'b, 'c>(
1818 &self,
1819 args: &'c [ArgumentHandle<'a, 'b>],
1820 _ctx: &dyn FunctionContext<'b>,
1821 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1822 let nums = collect_numeric_stats(args)?;
1823 if nums.is_empty() {
1824 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1825 ExcelError::new_num(),
1826 )));
1827 }
1828 // All values must be positive
1829 if nums.iter().any(|&n| n <= 0.0) {
1830 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1831 ExcelError::new_num(),
1832 )));
1833 }
1834 // Geometric mean = (x1 * x2 * ... * xn)^(1/n)
1835 // Use log to avoid overflow: exp(mean(ln(x)))
1836 let log_sum: f64 = nums.iter().map(|x| x.ln()).sum();
1837 let result = (log_sum / nums.len() as f64).exp();
1838 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
1839 result,
1840 )))
1841 }
1842}
1843
1844/// Returns the harmonic mean of positive numeric values.
1845///
1846/// `HARMEAN` emphasizes smaller values and is useful for averaging rates and ratios.
1847///
1848/// # Remarks
1849/// - All numeric inputs must be strictly greater than `0`.
1850/// - Returns `#NUM!` if any value is `<= 0`, or if no numeric values are provided.
1851/// - Non-numeric values in referenced ranges are ignored.
1852///
1853/// # Examples
1854///
1855/// ```yaml,sandbox
1856/// title: "Harmonic mean from scalar values"
1857/// formula: "=HARMEAN(1,2,4)"
1858/// expected: 1.7142857142857142
1859/// ```
1860///
1861/// ```yaml,sandbox
1862/// title: "Harmonic mean from a range"
1863/// grid:
1864/// A1: 2
1865/// A2: 3
1866/// A3: 6
1867/// formula: "=HARMEAN(A1:A3)"
1868/// expected: 3
1869/// ```
1870///
1871/// ```yaml,docs
1872/// related:
1873/// - GEOMEAN
1874/// - AVERAGE
1875/// - PRODUCT
1876/// faq:
1877/// - q: "Why does HARMEAN fail on zeros?"
1878/// a: "Harmonic mean uses reciprocals, so inputs must be strictly positive."
1879/// ```
1880#[derive(Debug)]
1881pub struct HarmeanFn;
1882/// [formualizer-docgen:schema:start]
1883/// Name: HARMEAN
1884/// Type: HarmeanFn
1885/// Min args: 1
1886/// Max args: variadic
1887/// Variadic: true
1888/// Signature: HARMEAN(arg1...: number@range)
1889/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1890/// Caps: PURE, REDUCTION, NUMERIC_ONLY
1891/// [formualizer-docgen:schema:end]
1892impl Function for HarmeanFn {
1893 func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
1894 fn name(&self) -> &'static str {
1895 "HARMEAN"
1896 }
1897 fn min_args(&self) -> usize {
1898 1
1899 }
1900 fn variadic(&self) -> bool {
1901 true
1902 }
1903 fn arg_schema(&self) -> &'static [ArgSchema] {
1904 &ARG_RANGE_NUM_LENIENT_ONE[..]
1905 }
1906 fn eval<'a, 'b, 'c>(
1907 &self,
1908 args: &'c [ArgumentHandle<'a, 'b>],
1909 _ctx: &dyn FunctionContext<'b>,
1910 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1911 let nums = collect_numeric_stats(args)?;
1912 if nums.is_empty() {
1913 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1914 ExcelError::new_num(),
1915 )));
1916 }
1917 // All values must be positive
1918 if nums.iter().any(|&n| n <= 0.0) {
1919 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1920 ExcelError::new_num(),
1921 )));
1922 }
1923 // Harmonic mean = n / sum(1/x)
1924 let sum_reciprocals: f64 = nums.iter().map(|x| 1.0 / x).sum();
1925 let result = nums.len() as f64 / sum_reciprocals;
1926 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
1927 result,
1928 )))
1929 }
1930}
1931
1932/// Returns the average of absolute deviations from the mean.
1933///
1934/// `AVEDEV` provides a robust spread measure that is less sensitive to outliers than squared-error
1935/// metrics.
1936///
1937/// # Remarks
1938/// - Returns `#NUM!` when no numeric values are available.
1939/// - Non-numeric values in referenced ranges are ignored.
1940/// - Uses the arithmetic mean as the center point.
1941///
1942/// # Examples
1943///
1944/// ```yaml,sandbox
1945/// title: "Average absolute deviation from scalar values"
1946/// formula: "=AVEDEV(2,4,6)"
1947/// expected: 1.3333333333333333
1948/// ```
1949///
1950/// ```yaml,sandbox
1951/// title: "Average absolute deviation from a range"
1952/// grid:
1953/// A1: 1
1954/// A2: 1
1955/// A3: 3
1956/// A4: 5
1957/// formula: "=AVEDEV(A1:A4)"
1958/// expected: 1.5
1959/// ```
1960///
1961/// ```yaml,docs
1962/// related:
1963/// - DEVSQ
1964/// - STDEV.S
1965/// - VAR.S
1966/// faq:
1967/// - q: "What center does AVEDEV use for deviations?"
1968/// a: "It computes absolute deviations around the arithmetic mean of included values."
1969/// ```
1970#[derive(Debug)]
1971pub struct AvedevFn;
1972/// [formualizer-docgen:schema:start]
1973/// Name: AVEDEV
1974/// Type: AvedevFn
1975/// Min args: 1
1976/// Max args: variadic
1977/// Variadic: true
1978/// Signature: AVEDEV(arg1...: number@range)
1979/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1980/// Caps: PURE, REDUCTION, NUMERIC_ONLY
1981/// [formualizer-docgen:schema:end]
1982impl Function for AvedevFn {
1983 func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
1984 fn name(&self) -> &'static str {
1985 "AVEDEV"
1986 }
1987 fn min_args(&self) -> usize {
1988 1
1989 }
1990 fn variadic(&self) -> bool {
1991 true
1992 }
1993 fn arg_schema(&self) -> &'static [ArgSchema] {
1994 &ARG_RANGE_NUM_LENIENT_ONE[..]
1995 }
1996 fn eval<'a, 'b, 'c>(
1997 &self,
1998 args: &'c [ArgumentHandle<'a, 'b>],
1999 _ctx: &dyn FunctionContext<'b>,
2000 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2001 let nums = collect_numeric_stats(args)?;
2002 if nums.is_empty() {
2003 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2004 ExcelError::new_num(),
2005 )));
2006 }
2007 let mean = nums.iter().sum::<f64>() / nums.len() as f64;
2008 let avedev = nums.iter().map(|x| (x - mean).abs()).sum::<f64>() / nums.len() as f64;
2009 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
2010 avedev,
2011 )))
2012 }
2013}
2014
2015/// Returns the sum of squared deviations from the mean.
2016///
2017/// `DEVSQ` is useful for variance-related calculations and diagnostics of spread.
2018///
2019/// # Remarks
2020/// - Returns `#NUM!` when no numeric values are available.
2021/// - Non-numeric values in referenced ranges are ignored.
2022/// - Uses the arithmetic mean of included values.
2023///
2024/// # Examples
2025///
2026/// ```yaml,sandbox
2027/// title: "Sum of squared deviations from scalar values"
2028/// formula: "=DEVSQ(2,4,6)"
2029/// expected: 8
2030/// ```
2031///
2032/// ```yaml,sandbox
2033/// title: "Sum of squared deviations from a range"
2034/// grid:
2035/// A1: 1
2036/// A2: 2
2037/// A3: 3
2038/// A4: 4
2039/// formula: "=DEVSQ(A1:A4)"
2040/// expected: 5
2041/// ```
2042#[derive(Debug)]
2043pub struct DevsqFn;
2044
2045/* ─────────────────────────── MAXIFS / MINIFS ──────────────────────────── */
2046
2047use super::utils::{ARG_ANY_ONE, criteria_match};
2048
2049/// Returns the maximum numeric value in a range that meets all criteria.
2050///
2051/// `MAXIFS` applies one or more `(criteria_range, criteria)` pairs and returns the largest
2052/// matching numeric value.
2053///
2054/// # Remarks
2055/// - Arguments must be `target_range` plus one or more criteria pairs.
2056/// - Criteria are combined with logical AND.
2057/// - Returns `0` when no cells satisfy all criteria.
2058/// - Non-numeric cells in `target_range` are ignored.
2059///
2060/// # Examples
2061///
2062/// ```yaml,sandbox
2063/// title: "Maximum value for one condition"
2064/// grid:
2065/// A1: 10
2066/// A2: 20
2067/// A3: 15
2068/// B1: "East"
2069/// B2: "West"
2070/// B3: "East"
2071/// formula: "=MAXIFS(A1:A3,B1:B3,\"East\")"
2072/// expected: 15
2073/// ```
2074///
2075/// ```yaml,sandbox
2076/// title: "Maximum value with two criteria"
2077/// grid:
2078/// A1: 100
2079/// A2: 80
2080/// A3: 90
2081/// A4: 70
2082/// B1: "A"
2083/// B2: "A"
2084/// B3: "B"
2085/// B4: "B"
2086/// C1: "Q1"
2087/// C2: "Q2"
2088/// C3: "Q1"
2089/// C4: "Q1"
2090/// formula: "=MAXIFS(A1:A4,B1:B4,\"B\",C1:C4,\"Q1\")"
2091/// expected: 90
2092/// ```
2093///
2094/// ```yaml,docs
2095/// related:
2096/// - MINIFS
2097/// - MAX
2098/// - SUMIFS
2099/// faq:
2100/// - q: "What does MAXIFS return when no rows match all criteria?"
2101/// a: "It returns 0 when no numeric target cells satisfy every criterion."
2102/// ```
2103#[derive(Debug)]
2104pub struct MaxIfsFn;
2105/// [formualizer-docgen:schema:start]
2106/// Name: MAXIFS
2107/// Type: MaxIfsFn
2108/// Min args: 3
2109/// Max args: variadic
2110/// Variadic: true
2111/// Signature: MAXIFS(arg1...: any@scalar)
2112/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
2113/// Caps: PURE, REDUCTION
2114/// [formualizer-docgen:schema:end]
2115impl Function for MaxIfsFn {
2116 func_caps!(PURE, REDUCTION);
2117 fn name(&self) -> &'static str {
2118 "MAXIFS"
2119 }
2120 fn min_args(&self) -> usize {
2121 3
2122 }
2123 fn variadic(&self) -> bool {
2124 true
2125 }
2126 fn arg_schema(&self) -> &'static [ArgSchema] {
2127 &ARG_ANY_ONE[..]
2128 }
2129 fn eval<'a, 'b, 'c>(
2130 &self,
2131 args: &'c [ArgumentHandle<'a, 'b>],
2132 _ctx: &dyn FunctionContext<'b>,
2133 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2134 eval_maxminifs(args, true)
2135 }
2136}
2137
2138/// Returns the minimum numeric value in a range that meets all criteria.
2139///
2140/// `MINIFS` evaluates one or more `(criteria_range, criteria)` pairs and returns the smallest
2141/// matching numeric value.
2142///
2143/// # Remarks
2144/// - Arguments must be `target_range` plus one or more criteria pairs.
2145/// - Criteria are combined with logical AND.
2146/// - Returns `0` when no cells satisfy all criteria.
2147/// - Non-numeric cells in `target_range` are ignored.
2148///
2149/// # Examples
2150///
2151/// ```yaml,sandbox
2152/// title: "Minimum value for one condition"
2153/// grid:
2154/// A1: 10
2155/// A2: 20
2156/// A3: 15
2157/// B1: "East"
2158/// B2: "West"
2159/// B3: "East"
2160/// formula: "=MINIFS(A1:A3,B1:B3,\"East\")"
2161/// expected: 10
2162/// ```
2163///
2164/// ```yaml,sandbox
2165/// title: "Minimum value with two criteria"
2166/// grid:
2167/// A1: 100
2168/// A2: 80
2169/// A3: 90
2170/// A4: 70
2171/// B1: "A"
2172/// B2: "A"
2173/// B3: "B"
2174/// B4: "B"
2175/// C1: "Q1"
2176/// C2: "Q2"
2177/// C3: "Q1"
2178/// C4: "Q1"
2179/// formula: "=MINIFS(A1:A4,B1:B4,\"B\",C1:C4,\"Q1\")"
2180/// expected: 70
2181/// ```
2182///
2183/// ```yaml,docs
2184/// related:
2185/// - MAXIFS
2186/// - MIN
2187/// - SUMIFS
2188/// faq:
2189/// - q: "How does MINIFS treat non-numeric target cells?"
2190/// a: "Non-numeric target cells are ignored; only numeric matches are eligible."
2191/// ```
2192#[derive(Debug)]
2193pub struct MinIfsFn;
2194/// [formualizer-docgen:schema:start]
2195/// Name: MINIFS
2196/// Type: MinIfsFn
2197/// Min args: 3
2198/// Max args: variadic
2199/// Variadic: true
2200/// Signature: MINIFS(arg1...: any@scalar)
2201/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
2202/// Caps: PURE, REDUCTION
2203/// [formualizer-docgen:schema:end]
2204impl Function for MinIfsFn {
2205 func_caps!(PURE, REDUCTION);
2206 fn name(&self) -> &'static str {
2207 "MINIFS"
2208 }
2209 fn min_args(&self) -> usize {
2210 3
2211 }
2212 fn variadic(&self) -> bool {
2213 true
2214 }
2215 fn arg_schema(&self) -> &'static [ArgSchema] {
2216 &ARG_ANY_ONE[..]
2217 }
2218 fn eval<'a, 'b, 'c>(
2219 &self,
2220 args: &'c [ArgumentHandle<'a, 'b>],
2221 _ctx: &dyn FunctionContext<'b>,
2222 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2223 eval_maxminifs(args, false)
2224 }
2225}
2226
2227/// Shared implementation for MAXIFS and MINIFS
2228fn eval_maxminifs<'a, 'b>(
2229 args: &[ArgumentHandle<'a, 'b>],
2230 is_max: bool,
2231) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2232 // Validate argument count: must be target_range + N pairs
2233 if args.len() < 3 || !(args.len() - 1).is_multiple_of(2) {
2234 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2235 ExcelError::new_value().with_message(format!(
2236 "Function expects 1 target_range followed by N pairs (criteria_range, criteria); got {} args",
2237 args.len()
2238 )),
2239 )));
2240 }
2241
2242 // Get target range
2243 let target_view = match args[0].range_view() {
2244 Ok(v) => v,
2245 Err(_) => {
2246 // Single value case - if criteria match, return that value
2247 let target_val = args[0].value()?.into_literal();
2248 if let LiteralValue::Error(e) = target_val {
2249 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
2250 }
2251 // Check all criteria against empty/scalar
2252 let mut all_match = true;
2253 for i in (1..args.len()).step_by(2) {
2254 let crit_val = args[i].value()?.into_literal();
2255 let pred = crate::args::parse_criteria(&args[i + 1].value()?.into_literal())?;
2256 if !criteria_match(&pred, &crit_val) {
2257 all_match = false;
2258 break;
2259 }
2260 }
2261 if all_match {
2262 return match coerce_num(&target_val) {
2263 Ok(n) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(n))),
2264 Err(_) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0))),
2265 };
2266 }
2267 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0)));
2268 }
2269 };
2270
2271 let (rows, cols) = target_view.dims();
2272
2273 // Parse all criteria
2274 let mut criteria_ranges = Vec::new();
2275 let mut predicates = Vec::new();
2276 for i in (1..args.len()).step_by(2) {
2277 let crit_view = args[i].range_view().ok();
2278 let pred = crate::args::parse_criteria(&args[i + 1].value()?.into_literal())?;
2279 criteria_ranges.push(crit_view);
2280 predicates.push(pred);
2281 }
2282
2283 // Iterate through all cells and find max/min where all criteria match
2284 let mut result: Option<f64> = None;
2285
2286 for r in 0..rows {
2287 for c in 0..cols {
2288 // Check all criteria
2289 let mut all_match = true;
2290 for (crit_idx, pred) in predicates.iter().enumerate() {
2291 let crit_val = match &criteria_ranges[crit_idx] {
2292 Some(view) => {
2293 let (cr, cc) = view.dims();
2294 if r < cr && c < cc {
2295 view.get_cell(r, c)
2296 } else {
2297 LiteralValue::Empty
2298 }
2299 }
2300 None => LiteralValue::Empty,
2301 };
2302 if !criteria_match(pred, &crit_val) {
2303 all_match = false;
2304 break;
2305 }
2306 }
2307
2308 if all_match {
2309 let target_val = target_view.get_cell(r, c);
2310 match target_val {
2311 LiteralValue::Error(e) => {
2312 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
2313 }
2314 LiteralValue::Number(n) => {
2315 result = Some(match result {
2316 None => n,
2317 Some(curr) => {
2318 if is_max {
2319 curr.max(n)
2320 } else {
2321 curr.min(n)
2322 }
2323 }
2324 });
2325 }
2326 LiteralValue::Int(i) => {
2327 let n = i as f64;
2328 result = Some(match result {
2329 None => n,
2330 Some(curr) => {
2331 if is_max {
2332 curr.max(n)
2333 } else {
2334 curr.min(n)
2335 }
2336 }
2337 });
2338 }
2339 _ => {} // Skip non-numeric
2340 }
2341 }
2342 }
2343 }
2344
2345 // Excel returns 0 if no matches found
2346 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
2347 result.unwrap_or(0.0),
2348 )))
2349}
2350
2351/* ─────────────────────────── TRIMMEAN ──────────────────────────── */
2352
2353/// Returns the mean after trimming a percentage of values from both tails.
2354///
2355/// `TRIMMEAN` sorts numeric data, removes an equal count from low and high ends, then averages the
2356/// remaining interior values.
2357///
2358/// # Remarks
2359/// - `percent` must satisfy `0 <= percent < 1`.
2360/// - The trimmed count per side is `floor(n * percent / 2)`.
2361/// - Returns `#NUM!` for invalid percent values or when no numeric values are available.
2362///
2363/// # Examples
2364///
2365/// ```yaml,sandbox
2366/// title: "Trimmed mean from direct values"
2367/// formula: "=TRIMMEAN({1,2,3,4,5,6},0.3333333333333333)"
2368/// expected: 3.5
2369/// ```
2370///
2371/// ```yaml,sandbox
2372/// title: "Trimmed mean from a range"
2373/// grid:
2374/// A1: 10
2375/// A2: 12
2376/// A3: 13
2377/// A4: 20
2378/// A5: 21
2379/// A6: 30
2380/// formula: "=TRIMMEAN(A1:A6,0.4)"
2381/// expected: 16.5
2382/// ```
2383#[derive(Debug)]
2384pub struct TrimmeanFn;
2385/// [formualizer-docgen:schema:start]
2386/// Name: TRIMMEAN
2387/// Type: TrimmeanFn
2388/// Min args: 2
2389/// Max args: 1
2390/// Variadic: false
2391/// Signature: TRIMMEAN(arg1: number@range)
2392/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
2393/// Caps: PURE, REDUCTION, NUMERIC_ONLY
2394/// [formualizer-docgen:schema:end]
2395impl Function for TrimmeanFn {
2396 func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
2397 fn name(&self) -> &'static str {
2398 "TRIMMEAN"
2399 }
2400 fn min_args(&self) -> usize {
2401 2
2402 }
2403 fn arg_schema(&self) -> &'static [ArgSchema] {
2404 &ARG_RANGE_NUM_LENIENT_ONE[..]
2405 }
2406 fn eval<'a, 'b, 'c>(
2407 &self,
2408 args: &'c [ArgumentHandle<'a, 'b>],
2409 _ctx: &dyn FunctionContext<'b>,
2410 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2411 let mut nums = collect_numeric_stats(&args[0..1])?;
2412 if nums.is_empty() {
2413 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2414 ExcelError::new_num(),
2415 )));
2416 }
2417
2418 let percent = match args[1].value()?.into_literal() {
2419 LiteralValue::Error(e) => {
2420 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
2421 }
2422 other => coerce_num(&other)?,
2423 };
2424
2425 // Percent must be between 0 and 1 (exclusive of 1)
2426 if !(0.0..1.0).contains(&percent) {
2427 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2428 ExcelError::new_num(),
2429 )));
2430 }
2431
2432 nums.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
2433
2434 let n = nums.len();
2435 // Number of values to exclude from each end
2436 let exclude = ((n as f64 * percent) / 2.0).floor() as usize;
2437
2438 if 2 * exclude >= n {
2439 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2440 ExcelError::new_num(),
2441 )));
2442 }
2443
2444 let trimmed = &nums[exclude..n - exclude];
2445 let sum: f64 = trimmed.iter().sum();
2446 let mean = sum / trimmed.len() as f64;
2447
2448 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(mean)))
2449 }
2450}
2451
2452/* ─────────────────────────── CORREL ──────────────────────────── */
2453
2454/// Helper to collect two paired arrays for regression/correlation functions
2455fn collect_paired_arrays(args: &[ArgumentHandle]) -> Result<(Vec<f64>, Vec<f64>), ExcelError> {
2456 let y_nums = collect_numeric_stats(&args[0..1])?;
2457 let x_nums = collect_numeric_stats(&args[1..2])?;
2458
2459 // Arrays must have same length
2460 if y_nums.len() != x_nums.len() {
2461 return Err(ExcelError::new_na());
2462 }
2463
2464 if y_nums.is_empty() {
2465 return Err(ExcelError::new_div());
2466 }
2467
2468 Ok((y_nums, x_nums))
2469}
2470
2471/// Returns the Pearson correlation coefficient between two numeric arrays.
2472///
2473/// `CORREL` measures linear relationship strength from `-1` (perfect inverse) to `1` (perfect
2474/// direct).
2475///
2476/// # Remarks
2477/// - Both arrays must produce the same number of numeric values.
2478/// - Returns `#N/A` when array lengths differ.
2479/// - Returns `#DIV/0!` when either series has zero variance.
2480///
2481/// # Examples
2482///
2483/// ```yaml,sandbox
2484/// title: "Perfect positive linear correlation"
2485/// formula: "=CORREL({2,4,6},{1,2,3})"
2486/// expected: 1
2487/// ```
2488///
2489/// ```yaml,sandbox
2490/// title: "Perfect negative linear correlation"
2491/// grid:
2492/// A1: 10
2493/// A2: 8
2494/// A3: 6
2495/// B1: 1
2496/// B2: 2
2497/// B3: 3
2498/// formula: "=CORREL(A1:A3,B1:B3)"
2499/// expected: -1
2500/// ```
2501#[derive(Debug)]
2502pub struct CorrelFn;
2503/// [formualizer-docgen:schema:start]
2504/// Name: CORREL
2505/// Type: CorrelFn
2506/// Min args: 2
2507/// Max args: 1
2508/// Variadic: false
2509/// Signature: CORREL(arg1: number@range)
2510/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
2511/// Caps: PURE, REDUCTION, NUMERIC_ONLY
2512/// [formualizer-docgen:schema:end]
2513impl Function for CorrelFn {
2514 func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
2515 fn name(&self) -> &'static str {
2516 "CORREL"
2517 }
2518 fn min_args(&self) -> usize {
2519 2
2520 }
2521 fn arg_schema(&self) -> &'static [ArgSchema] {
2522 &ARG_RANGE_NUM_LENIENT_ONE[..]
2523 }
2524 fn eval<'a, 'b, 'c>(
2525 &self,
2526 args: &'c [ArgumentHandle<'a, 'b>],
2527 _ctx: &dyn FunctionContext<'b>,
2528 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2529 let (y, x) = match collect_paired_arrays(args) {
2530 Ok(v) => v,
2531 Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
2532 };
2533
2534 let n = x.len() as f64;
2535 let mean_x = x.iter().sum::<f64>() / n;
2536 let mean_y = y.iter().sum::<f64>() / n;
2537
2538 let mut sum_xy = 0.0;
2539 let mut sum_x2 = 0.0;
2540 let mut sum_y2 = 0.0;
2541
2542 for i in 0..x.len() {
2543 let dx = x[i] - mean_x;
2544 let dy = y[i] - mean_y;
2545 sum_xy += dx * dy;
2546 sum_x2 += dx * dx;
2547 sum_y2 += dy * dy;
2548 }
2549
2550 let denom = (sum_x2 * sum_y2).sqrt();
2551 if denom == 0.0 {
2552 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2553 ExcelError::new_div(),
2554 )));
2555 }
2556
2557 let correl = sum_xy / denom;
2558 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
2559 correl,
2560 )))
2561 }
2562}
2563
2564/* ─────────────────────────── SLOPE ──────────────────────────── */
2565
2566/// Returns the slope of the linear regression line for paired data.
2567///
2568/// `SLOPE` fits `y = m*x + b` and returns `m`, the rate of change in `y` per unit of `x`.
2569///
2570/// # Remarks
2571/// - `known_y` and `known_x` must have the same numeric length.
2572/// - Returns `#N/A` for mismatched lengths.
2573/// - Returns `#DIV/0!` if all `x` values are identical.
2574///
2575/// # Examples
2576///
2577/// ```yaml,sandbox
2578/// title: "Positive slope from direct arrays"
2579/// formula: "=SLOPE({2,4,6},{1,2,3})"
2580/// expected: 2
2581/// ```
2582///
2583/// ```yaml,sandbox
2584/// title: "Negative slope from ranges"
2585/// grid:
2586/// A1: 10
2587/// A2: 8
2588/// A3: 6
2589/// B1: 1
2590/// B2: 2
2591/// B3: 3
2592/// formula: "=SLOPE(A1:A3,B1:B3)"
2593/// expected: -2
2594/// ```
2595#[derive(Debug)]
2596pub struct SlopeFn;
2597/// [formualizer-docgen:schema:start]
2598/// Name: SLOPE
2599/// Type: SlopeFn
2600/// Min args: 2
2601/// Max args: 1
2602/// Variadic: false
2603/// Signature: SLOPE(arg1: number@range)
2604/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
2605/// Caps: PURE, REDUCTION, NUMERIC_ONLY
2606/// [formualizer-docgen:schema:end]
2607impl Function for SlopeFn {
2608 func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
2609 fn name(&self) -> &'static str {
2610 "SLOPE"
2611 }
2612 fn min_args(&self) -> usize {
2613 2
2614 }
2615 fn arg_schema(&self) -> &'static [ArgSchema] {
2616 &ARG_RANGE_NUM_LENIENT_ONE[..]
2617 }
2618 fn eval<'a, 'b, 'c>(
2619 &self,
2620 args: &'c [ArgumentHandle<'a, 'b>],
2621 _ctx: &dyn FunctionContext<'b>,
2622 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2623 let (y, x) = match collect_paired_arrays(args) {
2624 Ok(v) => v,
2625 Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
2626 };
2627
2628 let n = x.len() as f64;
2629 let mean_x = x.iter().sum::<f64>() / n;
2630 let mean_y = y.iter().sum::<f64>() / n;
2631
2632 let mut sum_xy = 0.0;
2633 let mut sum_x2 = 0.0;
2634
2635 for i in 0..x.len() {
2636 let dx = x[i] - mean_x;
2637 let dy = y[i] - mean_y;
2638 sum_xy += dx * dy;
2639 sum_x2 += dx * dx;
2640 }
2641
2642 if sum_x2 == 0.0 {
2643 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2644 ExcelError::new_div(),
2645 )));
2646 }
2647
2648 let slope = sum_xy / sum_x2;
2649 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
2650 slope,
2651 )))
2652 }
2653}
2654
2655/* ─────────────────────────── INTERCEPT ──────────────────────────── */
2656
2657/// Returns the y-intercept of the linear regression line for paired data.
2658///
2659/// `INTERCEPT` fits `y = m*x + b` and returns `b`, the predicted `y` when `x = 0`.
2660///
2661/// # Remarks
2662/// - `known_y` and `known_x` must have the same numeric length.
2663/// - Returns `#N/A` for mismatched lengths.
2664/// - Returns `#DIV/0!` if all `x` values are identical.
2665///
2666/// # Examples
2667///
2668/// ```yaml,sandbox
2669/// title: "Positive intercept from direct arrays"
2670/// formula: "=INTERCEPT({3,5,7},{1,2,3})"
2671/// expected: 1
2672/// ```
2673///
2674/// ```yaml,sandbox
2675/// title: "Intercept from range-based linear trend"
2676/// grid:
2677/// A1: 10
2678/// A2: 8
2679/// A3: 6
2680/// B1: 1
2681/// B2: 2
2682/// B3: 3
2683/// formula: "=INTERCEPT(A1:A3,B1:B3)"
2684/// expected: 12
2685/// ```
2686#[derive(Debug)]
2687pub struct InterceptFn;
2688/// [formualizer-docgen:schema:start]
2689/// Name: INTERCEPT
2690/// Type: InterceptFn
2691/// Min args: 2
2692/// Max args: 1
2693/// Variadic: false
2694/// Signature: INTERCEPT(arg1: number@range)
2695/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
2696/// Caps: PURE, REDUCTION, NUMERIC_ONLY
2697/// [formualizer-docgen:schema:end]
2698impl Function for InterceptFn {
2699 func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
2700 fn name(&self) -> &'static str {
2701 "INTERCEPT"
2702 }
2703 fn min_args(&self) -> usize {
2704 2
2705 }
2706 fn arg_schema(&self) -> &'static [ArgSchema] {
2707 &ARG_RANGE_NUM_LENIENT_ONE[..]
2708 }
2709 fn eval<'a, 'b, 'c>(
2710 &self,
2711 args: &'c [ArgumentHandle<'a, 'b>],
2712 _ctx: &dyn FunctionContext<'b>,
2713 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2714 let (y, x) = match collect_paired_arrays(args) {
2715 Ok(v) => v,
2716 Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
2717 };
2718
2719 let n = x.len() as f64;
2720 let mean_x = x.iter().sum::<f64>() / n;
2721 let mean_y = y.iter().sum::<f64>() / n;
2722
2723 let mut sum_xy = 0.0;
2724 let mut sum_x2 = 0.0;
2725
2726 for i in 0..x.len() {
2727 let dx = x[i] - mean_x;
2728 let dy = y[i] - mean_y;
2729 sum_xy += dx * dy;
2730 sum_x2 += dx * dx;
2731 }
2732
2733 if sum_x2 == 0.0 {
2734 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2735 ExcelError::new_div(),
2736 )));
2737 }
2738
2739 let slope = sum_xy / sum_x2;
2740 let intercept = mean_y - slope * mean_x;
2741 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
2742 intercept,
2743 )))
2744 }
2745}
2746
2747/// [formualizer-docgen:schema:start]
2748/// Name: DEVSQ
2749/// Type: DevsqFn
2750/// Min args: 1
2751/// Max args: variadic
2752/// Variadic: true
2753/// Signature: DEVSQ(arg1...: number@range)
2754/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
2755/// Caps: PURE, REDUCTION, NUMERIC_ONLY
2756/// [formualizer-docgen:schema:end]
2757impl Function for DevsqFn {
2758 func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
2759 fn name(&self) -> &'static str {
2760 "DEVSQ"
2761 }
2762 fn min_args(&self) -> usize {
2763 1
2764 }
2765 fn variadic(&self) -> bool {
2766 true
2767 }
2768 fn arg_schema(&self) -> &'static [ArgSchema] {
2769 &ARG_RANGE_NUM_LENIENT_ONE[..]
2770 }
2771 fn eval<'a, 'b, 'c>(
2772 &self,
2773 args: &'c [ArgumentHandle<'a, 'b>],
2774 _ctx: &dyn FunctionContext<'b>,
2775 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2776 let nums = collect_numeric_stats(args)?;
2777 if nums.is_empty() {
2778 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2779 ExcelError::new_num(),
2780 )));
2781 }
2782 let mean = nums.iter().sum::<f64>() / nums.len() as f64;
2783 let devsq = nums.iter().map(|x| (x - mean).powi(2)).sum::<f64>();
2784 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
2785 devsq,
2786 )))
2787 }
2788}
2789
2790/* ═══════════════════════════════════════════════════════════════════════════
2791STATISTICAL DISTRIBUTION FUNCTIONS
2792═══════════════════════════════════════════════════════════════════════════ */
2793
2794/// Helper: Standard normal CDF using error function approximation
2795fn std_norm_cdf(z: f64) -> f64 {
2796 // Use the complementary error function: Φ(z) = 0.5 * erfc(-z / sqrt(2))
2797 // Approximation using Abramowitz and Stegun formula 7.1.26
2798 let a1 = 0.254829592;
2799 let a2 = -0.284496736;
2800 let a3 = 1.421413741;
2801 let a4 = -1.453152027;
2802 let a5 = 1.061405429;
2803 let p = 0.3275911;
2804
2805 let sign = if z < 0.0 { -1.0 } else { 1.0 };
2806 let z_abs = z.abs() / std::f64::consts::SQRT_2;
2807
2808 let t = 1.0 / (1.0 + p * z_abs);
2809 let y = 1.0 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * (-z_abs * z_abs).exp();
2810
2811 0.5 * (1.0 + sign * y)
2812}
2813
2814/// Helper: Standard normal PDF
2815fn std_norm_pdf(z: f64) -> f64 {
2816 let inv_sqrt_2pi = 1.0 / (2.0 * std::f64::consts::PI).sqrt();
2817 inv_sqrt_2pi * (-0.5 * z * z).exp()
2818}
2819
2820/// Helper: Inverse standard normal CDF (probit function)
2821/// Uses Rational approximation from Abramowitz and Stegun
2822#[allow(clippy::excessive_precision)]
2823fn std_norm_inv(p: f64) -> Option<f64> {
2824 if p <= 0.0 || p >= 1.0 {
2825 return None;
2826 }
2827
2828 // Coefficients for rational approximation
2829 const A: [f64; 6] = [
2830 -3.969683028665376e+01,
2831 2.209460984245205e+02,
2832 -2.759285104469687e+02,
2833 1.383577518672690e+02,
2834 -3.066479806614716e+01,
2835 2.506628277459239e+00,
2836 ];
2837 const B: [f64; 5] = [
2838 -5.447609879822406e+01,
2839 1.615858368580409e+02,
2840 -1.556989798598866e+02,
2841 6.680131188771972e+01,
2842 -1.328068155288572e+01,
2843 ];
2844 const C: [f64; 6] = [
2845 -7.784894002430293e-03,
2846 -3.223964580411365e-01,
2847 -2.400758277161838e+00,
2848 -2.549732539343734e+00,
2849 4.374664141464968e+00,
2850 2.938163982698783e+00,
2851 ];
2852 const D: [f64; 4] = [
2853 7.784695709041462e-03,
2854 3.224671290700398e-01,
2855 2.445134137142996e+00,
2856 3.754408661907416e+00,
2857 ];
2858
2859 const P_LOW: f64 = 0.02425;
2860 const P_HIGH: f64 = 1.0 - P_LOW;
2861
2862 let q = p - 0.5;
2863
2864 if p < P_LOW {
2865 // Lower tail
2866 let r = (-2.0 * p.ln()).sqrt();
2867 let num = ((((C[0] * r + C[1]) * r + C[2]) * r + C[3]) * r + C[4]) * r + C[5];
2868 let den = (((D[0] * r + D[1]) * r + D[2]) * r + D[3]) * r + 1.0;
2869 Some(num / den)
2870 } else if p <= P_HIGH {
2871 // Central region
2872 let r = q * q;
2873 let num = ((((A[0] * r + A[1]) * r + A[2]) * r + A[3]) * r + A[4]) * r + A[5];
2874 let den = ((((B[0] * r + B[1]) * r + B[2]) * r + B[3]) * r + B[4]) * r + 1.0;
2875 Some(q * num / den)
2876 } else {
2877 // Upper tail
2878 let r = (-2.0 * (1.0 - p).ln()).sqrt();
2879 let num = ((((C[0] * r + C[1]) * r + C[2]) * r + C[3]) * r + C[4]) * r + C[5];
2880 let den = (((D[0] * r + D[1]) * r + D[2]) * r + D[3]) * r + 1.0;
2881 Some(-num / den)
2882 }
2883}
2884
2885/// Returns the standard normal probability for a z-score as either a CDF or PDF value.
2886///
2887/// Use `NORM.S.DIST` for z-based probability lookups when the distribution has mean `0` and
2888/// standard deviation `1`.
2889///
2890/// # Remarks
2891/// - Set `cumulative` to a non-zero value for the cumulative distribution `P(Z <= z)`.
2892/// - Set `cumulative` to `0` for the probability density at exactly `z`.
2893/// - Accepts any real-valued `z`; no domain clipping is applied.
2894/// - Invalid numeric coercions propagate as spreadsheet errors.
2895///
2896/// # Examples
2897///
2898/// ```yaml,sandbox
2899/// title: "Standard normal CDF at zero"
2900/// formula: "=NORM.S.DIST(0,TRUE)"
2901/// expected: 0.5
2902/// ```
2903///
2904/// ```yaml,sandbox
2905/// title: "Standard normal PDF at zero"
2906/// formula: "=NORM.S.DIST(0,FALSE)"
2907/// expected: 0.3989422804014327
2908/// ```
2909#[derive(Debug)]
2910pub struct NormSDistFn;
2911/// [formualizer-docgen:schema:start]
2912/// Name: NORM.S.DIST
2913/// Type: NormSDistFn
2914/// Min args: 2
2915/// Max args: 2
2916/// Variadic: false
2917/// Signature: NORM.S.DIST(arg1: number@scalar, arg2: number@scalar)
2918/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
2919/// Caps: PURE
2920/// [formualizer-docgen:schema:end]
2921impl Function for NormSDistFn {
2922 func_caps!(PURE);
2923 fn name(&self) -> &'static str {
2924 "NORM.S.DIST"
2925 }
2926 fn min_args(&self) -> usize {
2927 2
2928 }
2929 fn arg_schema(&self) -> &'static [ArgSchema] {
2930 use std::sync::LazyLock;
2931 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
2932 vec![
2933 ArgSchema::number_lenient_scalar(),
2934 ArgSchema::number_lenient_scalar(),
2935 ]
2936 });
2937 &SCHEMA[..]
2938 }
2939 fn eval<'a, 'b, 'c>(
2940 &self,
2941 args: &'c [ArgumentHandle<'a, 'b>],
2942 _ctx: &dyn FunctionContext<'b>,
2943 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2944 let z = coerce_num(&scalar_like_value(&args[0])?)?;
2945 let cumulative = coerce_num(&scalar_like_value(&args[1])?)? != 0.0;
2946
2947 let result = if cumulative {
2948 std_norm_cdf(z)
2949 } else {
2950 std_norm_pdf(z)
2951 };
2952 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
2953 result,
2954 )))
2955 }
2956}
2957
2958/// Returns the z-score whose standard normal cumulative probability matches `probability`.
2959///
2960/// This is the inverse of `NORM.S.DIST(z, TRUE)` and is commonly used for critical-value
2961/// thresholds.
2962///
2963/// # Remarks
2964/// - `probability` must be strictly between `0` and `1`.
2965/// - Returns `#NUM!` when `probability <= 0` or `probability >= 1`.
2966/// - Output can be negative, zero, or positive depending on which side of `0.5` you query.
2967/// - Invalid numeric coercions propagate as spreadsheet errors.
2968///
2969/// # Examples
2970///
2971/// ```yaml,sandbox
2972/// title: "Median probability maps to zero"
2973/// formula: "=NORM.S.INV(0.5)"
2974/// expected: 0
2975/// ```
2976///
2977/// ```yaml,sandbox
2978/// title: "Upper-tail critical z-score"
2979/// formula: "=NORM.S.INV(0.975)"
2980/// expected: 1.959963986120195
2981/// ```
2982#[derive(Debug)]
2983pub struct NormSInvFn;
2984/// [formualizer-docgen:schema:start]
2985/// Name: NORM.S.INV
2986/// Type: NormSInvFn
2987/// Min args: 1
2988/// Max args: 1
2989/// Variadic: false
2990/// Signature: NORM.S.INV(arg1: number@scalar)
2991/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
2992/// Caps: PURE
2993/// [formualizer-docgen:schema:end]
2994impl Function for NormSInvFn {
2995 func_caps!(PURE);
2996 fn name(&self) -> &'static str {
2997 "NORM.S.INV"
2998 }
2999 fn min_args(&self) -> usize {
3000 1
3001 }
3002 fn arg_schema(&self) -> &'static [ArgSchema] {
3003 use std::sync::LazyLock;
3004 static SCHEMA: LazyLock<Vec<ArgSchema>> =
3005 LazyLock::new(|| vec![ArgSchema::number_lenient_scalar()]);
3006 &SCHEMA[..]
3007 }
3008 fn eval<'a, 'b, 'c>(
3009 &self,
3010 args: &'c [ArgumentHandle<'a, 'b>],
3011 _ctx: &dyn FunctionContext<'b>,
3012 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3013 let p = coerce_num(&scalar_like_value(&args[0])?)?;
3014
3015 match std_norm_inv(p) {
3016 Some(z) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(z))),
3017 None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3018 ExcelError::new_num(),
3019 ))),
3020 }
3021 }
3022}
3023
3024/// Returns the normal-distribution probability at `x` for a given mean and standard deviation.
3025///
3026/// Use `NORM.DIST` for either cumulative probabilities or point density under a non-standard
3027/// normal model.
3028///
3029/// # Remarks
3030/// - Set `cumulative` to non-zero for `P(X <= x)`; set it to `0` for density mode.
3031/// - `standard_dev` must be strictly greater than `0`.
3032/// - Returns `#NUM!` when `standard_dev <= 0`.
3033/// - Invalid numeric coercions propagate as spreadsheet errors.
3034///
3035/// # Examples
3036///
3037/// ```yaml,sandbox
3038/// title: "Normal CDF at the mean"
3039/// formula: "=NORM.DIST(50,50,10,TRUE)"
3040/// expected: 0.5
3041/// ```
3042///
3043/// ```yaml,sandbox
3044/// title: "Normal PDF at the mean"
3045/// formula: "=NORM.DIST(50,50,10,FALSE)"
3046/// expected: 0.03989422804014327
3047/// ```
3048#[derive(Debug)]
3049pub struct NormDistFn;
3050/// [formualizer-docgen:schema:start]
3051/// Name: NORM.DIST
3052/// Type: NormDistFn
3053/// Min args: 4
3054/// Max args: 4
3055/// Variadic: false
3056/// Signature: NORM.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar)
3057/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg4{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
3058/// Caps: PURE
3059/// [formualizer-docgen:schema:end]
3060impl Function for NormDistFn {
3061 func_caps!(PURE);
3062 fn name(&self) -> &'static str {
3063 "NORM.DIST"
3064 }
3065 fn min_args(&self) -> usize {
3066 4
3067 }
3068 fn arg_schema(&self) -> &'static [ArgSchema] {
3069 use std::sync::LazyLock;
3070 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
3071 vec![
3072 ArgSchema::number_lenient_scalar(),
3073 ArgSchema::number_lenient_scalar(),
3074 ArgSchema::number_lenient_scalar(),
3075 ArgSchema::number_lenient_scalar(),
3076 ]
3077 });
3078 &SCHEMA[..]
3079 }
3080 fn eval<'a, 'b, 'c>(
3081 &self,
3082 args: &'c [ArgumentHandle<'a, 'b>],
3083 _ctx: &dyn FunctionContext<'b>,
3084 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3085 let x = coerce_num(&scalar_like_value(&args[0])?)?;
3086 let mean = coerce_num(&scalar_like_value(&args[1])?)?;
3087 let std_dev = coerce_num(&scalar_like_value(&args[2])?)?;
3088 let cumulative = coerce_num(&scalar_like_value(&args[3])?)? != 0.0;
3089
3090 if std_dev <= 0.0 {
3091 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3092 ExcelError::new_num(),
3093 )));
3094 }
3095
3096 let z = (x - mean) / std_dev;
3097
3098 let result = if cumulative {
3099 std_norm_cdf(z)
3100 } else {
3101 std_norm_pdf(z) / std_dev
3102 };
3103 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
3104 result,
3105 )))
3106 }
3107}
3108
3109/// Returns the value `x` whose normal cumulative probability equals `probability`.
3110///
3111/// This function is the inverse of `NORM.DIST(x, mean, standard_dev, TRUE)`.
3112///
3113/// # Remarks
3114/// - `probability` must be strictly between `0` and `1`.
3115/// - `standard_dev` must be strictly greater than `0`.
3116/// - Returns `#NUM!` for invalid probability bounds or non-positive standard deviation.
3117/// - Invalid numeric coercions propagate as spreadsheet errors.
3118///
3119/// # Examples
3120///
3121/// ```yaml,sandbox
3122/// title: "Median probability returns the mean"
3123/// formula: "=NORM.INV(0.5,10,2)"
3124/// expected: 10
3125/// ```
3126///
3127/// ```yaml,sandbox
3128/// title: "One-standard-deviation quantile"
3129/// formula: "=NORM.INV(0.841344746068543,0,1)"
3130/// expected: 1
3131/// ```
3132#[derive(Debug)]
3133pub struct NormInvFn;
3134/// [formualizer-docgen:schema:start]
3135/// Name: NORM.INV
3136/// Type: NormInvFn
3137/// Min args: 3
3138/// Max args: 3
3139/// Variadic: false
3140/// Signature: NORM.INV(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
3141/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
3142/// Caps: PURE
3143/// [formualizer-docgen:schema:end]
3144impl Function for NormInvFn {
3145 func_caps!(PURE);
3146 fn name(&self) -> &'static str {
3147 "NORM.INV"
3148 }
3149 fn min_args(&self) -> usize {
3150 3
3151 }
3152 fn arg_schema(&self) -> &'static [ArgSchema] {
3153 use std::sync::LazyLock;
3154 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
3155 vec![
3156 ArgSchema::number_lenient_scalar(),
3157 ArgSchema::number_lenient_scalar(),
3158 ArgSchema::number_lenient_scalar(),
3159 ]
3160 });
3161 &SCHEMA[..]
3162 }
3163 fn eval<'a, 'b, 'c>(
3164 &self,
3165 args: &'c [ArgumentHandle<'a, 'b>],
3166 _ctx: &dyn FunctionContext<'b>,
3167 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3168 let p = coerce_num(&scalar_like_value(&args[0])?)?;
3169 let mean = coerce_num(&scalar_like_value(&args[1])?)?;
3170 let std_dev = coerce_num(&scalar_like_value(&args[2])?)?;
3171
3172 if std_dev <= 0.0 {
3173 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3174 ExcelError::new_num(),
3175 )));
3176 }
3177
3178 match std_norm_inv(p) {
3179 Some(z) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
3180 mean + z * std_dev,
3181 ))),
3182 None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3183 ExcelError::new_num(),
3184 ))),
3185 }
3186 }
3187}
3188
3189/// Returns the log-normal probability at `x` as either a cumulative value or density.
3190///
3191/// `LOGNORM.DIST` models positive-valued variables where `ln(X)` follows a normal distribution.
3192///
3193/// # Remarks
3194/// - Set `cumulative` to non-zero for CDF mode; set it to `0` for PDF mode.
3195/// - Requires `x > 0` and `standard_dev > 0`.
3196/// - Returns `#NUM!` when `x <= 0` or `standard_dev <= 0`.
3197/// - Invalid numeric coercions propagate as spreadsheet errors.
3198///
3199/// # Examples
3200///
3201/// ```yaml,sandbox
3202/// title: "Log-normal CDF at x = 1"
3203/// formula: "=LOGNORM.DIST(1,0,1,TRUE)"
3204/// expected: 0.5
3205/// ```
3206///
3207/// ```yaml,sandbox
3208/// title: "Log-normal PDF at x = 1"
3209/// formula: "=LOGNORM.DIST(1,0,1,FALSE)"
3210/// expected: 0.3989422804014327
3211/// ```
3212#[derive(Debug)]
3213pub struct LognormDistFn;
3214/// [formualizer-docgen:schema:start]
3215/// Name: LOGNORM.DIST
3216/// Type: LognormDistFn
3217/// Min args: 4
3218/// Max args: 4
3219/// Variadic: false
3220/// Signature: LOGNORM.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar)
3221/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg4{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
3222/// Caps: PURE
3223/// [formualizer-docgen:schema:end]
3224impl Function for LognormDistFn {
3225 func_caps!(PURE);
3226 fn name(&self) -> &'static str {
3227 "LOGNORM.DIST"
3228 }
3229 fn min_args(&self) -> usize {
3230 4
3231 }
3232 fn arg_schema(&self) -> &'static [ArgSchema] {
3233 use std::sync::LazyLock;
3234 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
3235 vec![
3236 ArgSchema::number_lenient_scalar(),
3237 ArgSchema::number_lenient_scalar(),
3238 ArgSchema::number_lenient_scalar(),
3239 ArgSchema::number_lenient_scalar(),
3240 ]
3241 });
3242 &SCHEMA[..]
3243 }
3244 fn eval<'a, 'b, 'c>(
3245 &self,
3246 args: &'c [ArgumentHandle<'a, 'b>],
3247 _ctx: &dyn FunctionContext<'b>,
3248 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3249 let x = coerce_num(&scalar_like_value(&args[0])?)?;
3250 let mean = coerce_num(&scalar_like_value(&args[1])?)?;
3251 let std_dev = coerce_num(&scalar_like_value(&args[2])?)?;
3252 let cumulative = coerce_num(&scalar_like_value(&args[3])?)? != 0.0;
3253
3254 if x <= 0.0 || std_dev <= 0.0 {
3255 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3256 ExcelError::new_num(),
3257 )));
3258 }
3259
3260 let z = (x.ln() - mean) / std_dev;
3261
3262 let result = if cumulative {
3263 std_norm_cdf(z)
3264 } else {
3265 std_norm_pdf(z) / (x * std_dev)
3266 };
3267 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
3268 result,
3269 )))
3270 }
3271}
3272
3273/// Returns the positive value `x` whose log-normal cumulative probability is `probability`.
3274///
3275/// This function inverts `LOGNORM.DIST(x, mean, standard_dev, TRUE)`.
3276///
3277/// # Remarks
3278/// - `probability` must be strictly between `0` and `1`.
3279/// - `standard_dev` must be strictly greater than `0`.
3280/// - Returns `#NUM!` when inputs violate probability or scale constraints.
3281/// - Invalid numeric coercions propagate as spreadsheet errors.
3282///
3283/// # Examples
3284///
3285/// ```yaml,sandbox
3286/// title: "Median log-normal quantile"
3287/// formula: "=LOGNORM.INV(0.5,0,1)"
3288/// expected: 1
3289/// ```
3290///
3291/// ```yaml,sandbox
3292/// title: "Upper quantile for mean 0 and stdev 1"
3293/// formula: "=LOGNORM.INV(0.841344746068543,0,1)"
3294/// expected: 2.718281828459045
3295/// ```
3296#[derive(Debug)]
3297pub struct LognormInvFn;
3298/// [formualizer-docgen:schema:start]
3299/// Name: LOGNORM.INV
3300/// Type: LognormInvFn
3301/// Min args: 3
3302/// Max args: 3
3303/// Variadic: false
3304/// Signature: LOGNORM.INV(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
3305/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
3306/// Caps: PURE
3307/// [formualizer-docgen:schema:end]
3308impl Function for LognormInvFn {
3309 func_caps!(PURE);
3310 fn name(&self) -> &'static str {
3311 "LOGNORM.INV"
3312 }
3313 fn min_args(&self) -> usize {
3314 3
3315 }
3316 fn arg_schema(&self) -> &'static [ArgSchema] {
3317 use std::sync::LazyLock;
3318 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
3319 vec![
3320 ArgSchema::number_lenient_scalar(),
3321 ArgSchema::number_lenient_scalar(),
3322 ArgSchema::number_lenient_scalar(),
3323 ]
3324 });
3325 &SCHEMA[..]
3326 }
3327 fn eval<'a, 'b, 'c>(
3328 &self,
3329 args: &'c [ArgumentHandle<'a, 'b>],
3330 _ctx: &dyn FunctionContext<'b>,
3331 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3332 let p = coerce_num(&scalar_like_value(&args[0])?)?;
3333 let mean = coerce_num(&scalar_like_value(&args[1])?)?;
3334 let std_dev = coerce_num(&scalar_like_value(&args[2])?)?;
3335
3336 if std_dev <= 0.0 {
3337 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3338 ExcelError::new_num(),
3339 )));
3340 }
3341
3342 match std_norm_inv(p) {
3343 Some(z) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
3344 (mean + z * std_dev).exp(),
3345 ))),
3346 None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3347 ExcelError::new_num(),
3348 ))),
3349 }
3350 }
3351}
3352
3353/// Returns the standard normal probability density at `x`.
3354///
3355/// `PHI` is equivalent to `NORM.S.DIST(x, FALSE)` and is useful in continuous-probability
3356/// calculations.
3357///
3358/// # Remarks
3359/// - Evaluates the density of a standard normal variable centered at `0`.
3360/// - The result is always non-negative and symmetric around `x = 0`.
3361/// - Works for any real input value.
3362/// - Invalid numeric coercions propagate as spreadsheet errors.
3363///
3364/// # Examples
3365///
3366/// ```yaml,sandbox
3367/// title: "Standard normal density at zero"
3368/// formula: "=PHI(0)"
3369/// expected: 0.3989422804014327
3370/// ```
3371///
3372/// ```yaml,sandbox
3373/// title: "Standard normal density at one"
3374/// formula: "=PHI(1)"
3375/// expected: 0.24197072451914337
3376/// ```
3377#[derive(Debug)]
3378pub struct PhiFn;
3379/// [formualizer-docgen:schema:start]
3380/// Name: PHI
3381/// Type: PhiFn
3382/// Min args: 1
3383/// Max args: 1
3384/// Variadic: false
3385/// Signature: PHI(arg1: number@scalar)
3386/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
3387/// Caps: PURE
3388/// [formualizer-docgen:schema:end]
3389impl Function for PhiFn {
3390 func_caps!(PURE);
3391 fn name(&self) -> &'static str {
3392 "PHI"
3393 }
3394 fn min_args(&self) -> usize {
3395 1
3396 }
3397 fn arg_schema(&self) -> &'static [ArgSchema] {
3398 use std::sync::LazyLock;
3399 static SCHEMA: LazyLock<Vec<ArgSchema>> =
3400 LazyLock::new(|| vec![ArgSchema::number_lenient_scalar()]);
3401 &SCHEMA[..]
3402 }
3403 fn eval<'a, 'b, 'c>(
3404 &self,
3405 args: &'c [ArgumentHandle<'a, 'b>],
3406 _ctx: &dyn FunctionContext<'b>,
3407 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3408 let z = coerce_num(&scalar_like_value(&args[0])?)?;
3409 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
3410 std_norm_pdf(z),
3411 )))
3412 }
3413}
3414
3415/// Returns the standard normal area between `0` and `z`.
3416///
3417/// `GAUSS` computes `NORM.S.DIST(z, TRUE) - 0.5`, preserving the sign of `z`.
3418///
3419/// # Remarks
3420/// - Positive `z` returns a positive area; negative `z` returns a negative area.
3421/// - `GAUSS(0)` returns `0`.
3422/// - Output magnitude is always less than `0.5`.
3423/// - Invalid numeric coercions propagate as spreadsheet errors.
3424///
3425/// # Examples
3426///
3427/// ```yaml,sandbox
3428/// title: "Area from mean to z = 1"
3429/// formula: "=GAUSS(1)"
3430/// expected: 0.3413447460685429
3431/// ```
3432///
3433/// ```yaml,sandbox
3434/// title: "Symmetric negative z-value"
3435/// formula: "=GAUSS(-1)"
3436/// expected: -0.3413447460685429
3437/// ```
3438#[derive(Debug)]
3439pub struct GaussFn;
3440/// [formualizer-docgen:schema:start]
3441/// Name: GAUSS
3442/// Type: GaussFn
3443/// Min args: 1
3444/// Max args: 1
3445/// Variadic: false
3446/// Signature: GAUSS(arg1: number@scalar)
3447/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
3448/// Caps: PURE
3449/// [formualizer-docgen:schema:end]
3450impl Function for GaussFn {
3451 func_caps!(PURE);
3452 fn name(&self) -> &'static str {
3453 "GAUSS"
3454 }
3455 fn min_args(&self) -> usize {
3456 1
3457 }
3458 fn arg_schema(&self) -> &'static [ArgSchema] {
3459 use std::sync::LazyLock;
3460 static SCHEMA: LazyLock<Vec<ArgSchema>> =
3461 LazyLock::new(|| vec![ArgSchema::number_lenient_scalar()]);
3462 &SCHEMA[..]
3463 }
3464 fn eval<'a, 'b, 'c>(
3465 &self,
3466 args: &'c [ArgumentHandle<'a, 'b>],
3467 _ctx: &dyn FunctionContext<'b>,
3468 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3469 let z = coerce_num(&scalar_like_value(&args[0])?)?;
3470 // GAUSS(z) = Φ(z) - 0.5
3471 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
3472 std_norm_cdf(z) - 0.5,
3473 )))
3474 }
3475}
3476
3477/// Helper: Log-gamma function
3478#[allow(clippy::excessive_precision)]
3479fn ln_gamma(x: f64) -> f64 {
3480 // Lanczos approximation
3481 const G: f64 = 7.0;
3482 const C: [f64; 9] = [
3483 0.99999999999980993,
3484 676.5203681218851,
3485 -1259.1392167224028,
3486 771.32342877765313,
3487 -176.61502916214059,
3488 12.507343278686905,
3489 -0.13857109526572012,
3490 9.9843695780195716e-6,
3491 1.5056327351493116e-7,
3492 ];
3493
3494 if x < 0.5 {
3495 // Reflection formula
3496 let pi = std::f64::consts::PI;
3497 pi.ln() - (pi * x).sin().ln() - ln_gamma(1.0 - x)
3498 } else {
3499 let x = x - 1.0;
3500 let mut ag = C[0];
3501 for (i, c) in C.iter().enumerate().skip(1) {
3502 ag += c / (x + i as f64);
3503 }
3504 let tmp = x + G + 0.5;
3505 0.5 * (2.0 * std::f64::consts::PI).ln() + (tmp).ln() * (x + 0.5) - tmp + ag.ln()
3506 }
3507}
3508
3509/// Helper: Regularized lower incomplete gamma function P(a, x)
3510fn gamma_p(a: f64, x: f64) -> f64 {
3511 if x < 0.0 || a <= 0.0 {
3512 return 0.0;
3513 }
3514 if x == 0.0 {
3515 return 0.0;
3516 }
3517
3518 // Use series expansion for x < a+1
3519 if x < a + 1.0 {
3520 gamma_series(a, x)
3521 } else {
3522 // Use continued fraction for x >= a+1
3523 1.0 - gamma_cf(a, x)
3524 }
3525}
3526
3527/// Helper: Series expansion for incomplete gamma
3528fn gamma_series(a: f64, x: f64) -> f64 {
3529 let ln_ga = ln_gamma(a);
3530 let mut sum = 1.0 / a;
3531 let mut term = sum;
3532 for n in 1..200 {
3533 term *= x / (a + n as f64);
3534 sum += term;
3535 if term.abs() < sum.abs() * 1e-15 {
3536 break;
3537 }
3538 }
3539 sum * (-x + a * x.ln() - ln_ga).exp()
3540}
3541
3542/// Helper: Continued fraction for upper incomplete gamma Q(a,x)
3543/// Using modified Lentz's algorithm (Numerical Recipes formulation)
3544fn gamma_cf(a: f64, x: f64) -> f64 {
3545 let ln_ga = ln_gamma(a);
3546 const TINY: f64 = 1e-30;
3547 const EPS: f64 = 1e-14;
3548
3549 // Set up for evaluating continued fraction by modified Lentz's method
3550 let mut b = x + 1.0 - a;
3551 let mut c = 1.0 / TINY;
3552 let mut d = 1.0 / b;
3553 let mut h = d;
3554
3555 for i in 1..=200 {
3556 let an = -(i as f64) * (i as f64 - a);
3557 b += 2.0;
3558 d = an * d + b;
3559 if d.abs() < TINY {
3560 d = TINY;
3561 }
3562 c = b + an / c;
3563 if c.abs() < TINY {
3564 c = TINY;
3565 }
3566 d = 1.0 / d;
3567 let delta = d * c;
3568 h *= delta;
3569 if (delta - 1.0).abs() <= EPS {
3570 break;
3571 }
3572 }
3573
3574 h * (-x + a * x.ln() - ln_ga).exp()
3575}
3576
3577/// Helper: Regularized incomplete beta function I_x(a,b)
3578/// Uses the continued fraction representation (NIST DLMF 8.17.22)
3579fn beta_i(x: f64, a: f64, b: f64) -> f64 {
3580 if x <= 0.0 {
3581 return 0.0;
3582 }
3583 if x >= 1.0 {
3584 return 1.0;
3585 }
3586 if a <= 0.0 || b <= 0.0 {
3587 return f64::NAN;
3588 }
3589
3590 // Use symmetry for better convergence: I_x(a,b) = 1 - I_{1-x}(b,a)
3591 if x > (a + 1.0) / (a + b + 2.0) {
3592 return 1.0 - beta_i(1.0 - x, b, a);
3593 }
3594
3595 // Compute the prefactor: x^a * (1-x)^b / (a * B(a,b))
3596 let ln_beta = ln_gamma(a) + ln_gamma(b) - ln_gamma(a + b);
3597 let ln_prefactor = a * x.ln() + b * (1.0 - x).ln() - ln_beta - a.ln();
3598 let prefactor = ln_prefactor.exp();
3599
3600 // Evaluate the continued fraction using modified Lentz algorithm
3601 // The CF is: 1 / (1 + d1/(1 + d2/(1 + ...)))
3602 // where d_{2m+1} = -(a+m)(a+b+m)x / ((a+2m)(a+2m+1))
3603 // d_{2m} = m(b-m)x / ((a+2m-1)(a+2m))
3604 const EPS: f64 = 1e-14;
3605 const TINY: f64 = 1e-30;
3606
3607 let qab = a + b;
3608 let qap = a + 1.0;
3609 let qam = a - 1.0;
3610 let mut c = 1.0;
3611 let mut d = 1.0 - qab * x / qap;
3612 if d.abs() < TINY {
3613 d = TINY;
3614 }
3615 d = 1.0 / d;
3616 let mut h = d;
3617
3618 for m in 1..=200 {
3619 let m_f64 = m as f64;
3620 let m2 = 2.0 * m_f64;
3621
3622 // Even step: d_{2m} = m(b-m)x / ((a+2m-1)(a+2m))
3623 let aa = m_f64 * (b - m_f64) * x / ((qam + m2) * (a + m2));
3624 d = 1.0 + aa * d;
3625 if d.abs() < TINY {
3626 d = TINY;
3627 }
3628 c = 1.0 + aa / c;
3629 if c.abs() < TINY {
3630 c = TINY;
3631 }
3632 d = 1.0 / d;
3633 h *= d * c;
3634
3635 // Odd step: d_{2m+1} = -(a+m)(a+b+m)x / ((a+2m)(a+2m+1))
3636 let aa = -((a + m_f64) * (qab + m_f64) * x) / ((a + m2) * (qap + m2));
3637 d = 1.0 + aa * d;
3638 if d.abs() < TINY {
3639 d = TINY;
3640 }
3641 c = 1.0 + aa / c;
3642 if c.abs() < TINY {
3643 c = TINY;
3644 }
3645 d = 1.0 / d;
3646 let delta = d * c;
3647 h *= delta;
3648
3649 if (delta - 1.0).abs() <= EPS {
3650 break;
3651 }
3652 }
3653
3654 prefactor * h
3655}
3656
3657/// Helper: T distribution CDF
3658fn t_cdf(t: f64, df: f64) -> f64 {
3659 let x = df / (df + t * t);
3660 0.5 * (1.0 + t.signum() * (1.0 - beta_i(x, df / 2.0, 0.5)))
3661}
3662
3663/// Helper: T distribution inverse CDF using Newton-Raphson
3664fn t_inv(p: f64, df: f64) -> Option<f64> {
3665 if p <= 0.0 || p >= 1.0 {
3666 return None;
3667 }
3668
3669 // Initial guess using normal approximation
3670 let mut t = std_norm_inv(p)?;
3671
3672 // Newton-Raphson iteration
3673 for _ in 0..50 {
3674 let cdf = t_cdf(t, df);
3675 let pdf = t_pdf(t, df);
3676 if pdf.abs() < 1e-30 {
3677 break;
3678 }
3679 let delta = (cdf - p) / pdf;
3680 t -= delta;
3681 if delta.abs() < 1e-12 {
3682 break;
3683 }
3684 }
3685
3686 Some(t)
3687}
3688
3689/// Helper: T distribution PDF
3690fn t_pdf(t: f64, df: f64) -> f64 {
3691 let coef =
3692 (ln_gamma((df + 1.0) / 2.0) - ln_gamma(df / 2.0) - 0.5 * (df * std::f64::consts::PI).ln())
3693 .exp();
3694 coef * (1.0 + t * t / df).powf(-(df + 1.0) / 2.0)
3695}
3696
3697/// Helper: Chi-square CDF
3698fn chisq_cdf(x: f64, df: f64) -> f64 {
3699 if x <= 0.0 {
3700 return 0.0;
3701 }
3702 gamma_p(df / 2.0, x / 2.0)
3703}
3704
3705/// Helper: Chi-square inverse CDF using Newton-Raphson
3706fn chisq_inv(p: f64, df: f64) -> Option<f64> {
3707 if p <= 0.0 || p >= 1.0 {
3708 return None;
3709 }
3710
3711 // Initial guess
3712 let mut x = df.max(1.0);
3713 if p < 0.5 {
3714 x = x.min(1.0);
3715 }
3716
3717 // Newton-Raphson iteration
3718 for _ in 0..100 {
3719 let cdf = chisq_cdf(x, df);
3720 let pdf = chisq_pdf(x, df);
3721 if pdf.abs() < 1e-30 {
3722 break;
3723 }
3724 let delta = (cdf - p) / pdf;
3725 let new_x = (x - delta).max(1e-15);
3726 if (new_x - x).abs() < 1e-12 * x {
3727 x = new_x;
3728 break;
3729 }
3730 x = new_x;
3731 }
3732
3733 Some(x)
3734}
3735
3736/// Helper: Chi-square PDF
3737fn chisq_pdf(x: f64, df: f64) -> f64 {
3738 if x <= 0.0 {
3739 return 0.0;
3740 }
3741 let k = df / 2.0;
3742 ((k - 1.0) * x.ln() - x / 2.0 - k * 2.0_f64.ln() - ln_gamma(k)).exp()
3743}
3744
3745/// Helper: F distribution CDF
3746fn f_cdf(f: f64, d1: f64, d2: f64) -> f64 {
3747 if f <= 0.0 {
3748 return 0.0;
3749 }
3750 let x = d1 * f / (d1 * f + d2);
3751 beta_i(x, d1 / 2.0, d2 / 2.0)
3752}
3753
3754/// Helper: F distribution inverse CDF using Newton-Raphson
3755fn f_inv(p: f64, d1: f64, d2: f64) -> Option<f64> {
3756 if p <= 0.0 || p >= 1.0 {
3757 return None;
3758 }
3759
3760 // Initial guess
3761 let mut f = 1.0;
3762
3763 // Newton-Raphson iteration
3764 for _ in 0..100 {
3765 let cdf = f_cdf(f, d1, d2);
3766 let pdf = f_pdf(f, d1, d2);
3767 if pdf.abs() < 1e-30 {
3768 break;
3769 }
3770 let delta = (cdf - p) / pdf;
3771 let new_f = (f - delta).max(1e-15);
3772 if (new_f - f).abs() < 1e-12 * f {
3773 f = new_f;
3774 break;
3775 }
3776 f = new_f;
3777 }
3778
3779 Some(f)
3780}
3781
3782/// Helper: F distribution PDF
3783fn f_pdf(f: f64, d1: f64, d2: f64) -> f64 {
3784 if f <= 0.0 {
3785 return 0.0;
3786 }
3787 let ln_beta = ln_gamma(d1 / 2.0) + ln_gamma(d2 / 2.0) - ln_gamma((d1 + d2) / 2.0);
3788 let coef = (d1 / 2.0) * (d1 / d2).ln() + (d1 / 2.0 - 1.0) * f.ln()
3789 - ((d1 + d2) / 2.0) * (1.0 + d1 * f / d2).ln()
3790 - ln_beta;
3791 coef.exp()
3792}
3793
3794/// Returns the Student's t probability for `x` and a given degrees-of-freedom value.
3795///
3796/// Use `T.DIST` in either cumulative mode (left-tail probability) or density mode.
3797///
3798/// # Remarks
3799/// - Set `cumulative` to non-zero for CDF mode, or `0` for PDF mode.
3800/// - `deg_freedom` must be at least `1`.
3801/// - Returns `#NUM!` when `deg_freedom < 1`.
3802/// - Invalid numeric coercions propagate as spreadsheet errors.
3803///
3804/// # Examples
3805///
3806/// ```yaml,sandbox
3807/// title: "t CDF at zero"
3808/// formula: "=T.DIST(0,10,TRUE)"
3809/// expected: 0.5
3810/// ```
3811///
3812/// ```yaml,sandbox
3813/// title: "t PDF at zero"
3814/// formula: "=T.DIST(0,10,FALSE)"
3815/// expected: 0.389108383966031
3816/// ```
3817#[derive(Debug)]
3818pub struct TDistFn;
3819/// [formualizer-docgen:schema:start]
3820/// Name: T.DIST
3821/// Type: TDistFn
3822/// Min args: 3
3823/// Max args: 3
3824/// Variadic: false
3825/// Signature: T.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
3826/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
3827/// Caps: PURE
3828/// [formualizer-docgen:schema:end]
3829impl Function for TDistFn {
3830 func_caps!(PURE);
3831 fn name(&self) -> &'static str {
3832 "T.DIST"
3833 }
3834 fn min_args(&self) -> usize {
3835 3
3836 }
3837 fn arg_schema(&self) -> &'static [ArgSchema] {
3838 use std::sync::LazyLock;
3839 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
3840 vec![
3841 ArgSchema::number_lenient_scalar(),
3842 ArgSchema::number_lenient_scalar(),
3843 ArgSchema::number_lenient_scalar(),
3844 ]
3845 });
3846 &SCHEMA[..]
3847 }
3848 fn eval<'a, 'b, 'c>(
3849 &self,
3850 args: &'c [ArgumentHandle<'a, 'b>],
3851 _ctx: &dyn FunctionContext<'b>,
3852 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3853 let x = coerce_num(&scalar_like_value(&args[0])?)?;
3854 let df = coerce_num(&scalar_like_value(&args[1])?)?;
3855 let cumulative = coerce_num(&scalar_like_value(&args[2])?)? != 0.0;
3856
3857 if df < 1.0 {
3858 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3859 ExcelError::new_num(),
3860 )));
3861 }
3862
3863 let result = if cumulative {
3864 t_cdf(x, df)
3865 } else {
3866 t_pdf(x, df)
3867 };
3868 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
3869 result,
3870 )))
3871 }
3872}
3873
3874/// Returns the t-value whose left-tail probability equals `probability`.
3875///
3876/// `T.INV` is the inverse of `T.DIST(x, deg_freedom, TRUE)`.
3877///
3878/// # Remarks
3879/// - `probability` must be strictly between `0` and `1`.
3880/// - `deg_freedom` must be at least `1`.
3881/// - Returns `#NUM!` for out-of-range probability or invalid degrees of freedom.
3882/// - Invalid numeric coercions propagate as spreadsheet errors.
3883///
3884/// # Examples
3885///
3886/// ```yaml,sandbox
3887/// title: "Median t quantile"
3888/// formula: "=T.INV(0.5,10)"
3889/// expected: 0
3890/// ```
3891///
3892/// ```yaml,sandbox
3893/// title: "Upper-tail critical value"
3894/// formula: "=T.INV(0.975,10)"
3895/// expected: 2.228138851986273
3896/// ```
3897#[derive(Debug)]
3898pub struct TInvFn;
3899/// [formualizer-docgen:schema:start]
3900/// Name: T.INV
3901/// Type: TInvFn
3902/// Min args: 2
3903/// Max args: 2
3904/// Variadic: false
3905/// Signature: T.INV(arg1: number@scalar, arg2: number@scalar)
3906/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
3907/// Caps: PURE
3908/// [formualizer-docgen:schema:end]
3909impl Function for TInvFn {
3910 func_caps!(PURE);
3911 fn name(&self) -> &'static str {
3912 "T.INV"
3913 }
3914 fn min_args(&self) -> usize {
3915 2
3916 }
3917 fn arg_schema(&self) -> &'static [ArgSchema] {
3918 use std::sync::LazyLock;
3919 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
3920 vec![
3921 ArgSchema::number_lenient_scalar(),
3922 ArgSchema::number_lenient_scalar(),
3923 ]
3924 });
3925 &SCHEMA[..]
3926 }
3927 fn eval<'a, 'b, 'c>(
3928 &self,
3929 args: &'c [ArgumentHandle<'a, 'b>],
3930 _ctx: &dyn FunctionContext<'b>,
3931 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3932 let p = coerce_num(&scalar_like_value(&args[0])?)?;
3933 let df = coerce_num(&scalar_like_value(&args[1])?)?;
3934
3935 if df < 1.0 {
3936 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3937 ExcelError::new_num(),
3938 )));
3939 }
3940
3941 match t_inv(p, df) {
3942 Some(result) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
3943 result,
3944 ))),
3945 None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3946 ExcelError::new_num(),
3947 ))),
3948 }
3949 }
3950}
3951
3952/// Returns the chi-square probability for `x` with the specified degrees of freedom.
3953///
3954/// Use `CHISQ.DIST` in cumulative mode for left-tail probability or density mode for the PDF.
3955///
3956/// # Remarks
3957/// - Set `cumulative` to non-zero for CDF mode, or `0` for PDF mode.
3958/// - Requires `x >= 0` and `deg_freedom >= 1`.
3959/// - Returns `#NUM!` for negative `x` or invalid degrees of freedom.
3960/// - Invalid numeric coercions propagate as spreadsheet errors.
3961///
3962/// # Examples
3963///
3964/// ```yaml,sandbox
3965/// title: "Chi-square CDF at zero"
3966/// formula: "=CHISQ.DIST(0,4,TRUE)"
3967/// expected: 0
3968/// ```
3969///
3970/// ```yaml,sandbox
3971/// title: "Chi-square PDF example"
3972/// formula: "=CHISQ.DIST(2,2,FALSE)"
3973/// expected: 0.18393972058572117
3974/// ```
3975#[derive(Debug)]
3976pub struct ChisqDistFn;
3977/// [formualizer-docgen:schema:start]
3978/// Name: CHISQ.DIST
3979/// Type: ChisqDistFn
3980/// Min args: 3
3981/// Max args: 3
3982/// Variadic: false
3983/// Signature: CHISQ.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
3984/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
3985/// Caps: PURE
3986/// [formualizer-docgen:schema:end]
3987impl Function for ChisqDistFn {
3988 func_caps!(PURE);
3989 fn name(&self) -> &'static str {
3990 "CHISQ.DIST"
3991 }
3992 fn min_args(&self) -> usize {
3993 3
3994 }
3995 fn arg_schema(&self) -> &'static [ArgSchema] {
3996 use std::sync::LazyLock;
3997 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
3998 vec![
3999 ArgSchema::number_lenient_scalar(),
4000 ArgSchema::number_lenient_scalar(),
4001 ArgSchema::number_lenient_scalar(),
4002 ]
4003 });
4004 &SCHEMA[..]
4005 }
4006 fn eval<'a, 'b, 'c>(
4007 &self,
4008 args: &'c [ArgumentHandle<'a, 'b>],
4009 _ctx: &dyn FunctionContext<'b>,
4010 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4011 let x = coerce_num(&scalar_like_value(&args[0])?)?;
4012 let df = coerce_num(&scalar_like_value(&args[1])?)?;
4013 let cumulative = coerce_num(&scalar_like_value(&args[2])?)? != 0.0;
4014
4015 if df < 1.0 || x < 0.0 {
4016 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4017 ExcelError::new_num(),
4018 )));
4019 }
4020
4021 let result = if cumulative {
4022 chisq_cdf(x, df)
4023 } else {
4024 chisq_pdf(x, df)
4025 };
4026 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
4027 result,
4028 )))
4029 }
4030}
4031
4032/// Returns the chi-square value whose left-tail probability is `probability`.
4033///
4034/// `CHISQ.INV` inverts `CHISQ.DIST(x, deg_freedom, TRUE)`.
4035///
4036/// # Remarks
4037/// - `probability` must be strictly between `0` and `1`.
4038/// - `deg_freedom` must be at least `1`.
4039/// - Returns `#NUM!` when arguments are outside valid ranges.
4040/// - Invalid numeric coercions propagate as spreadsheet errors.
4041///
4042/// # Examples
4043///
4044/// ```yaml,sandbox
4045/// title: "Median chi-square quantile for df=2"
4046/// formula: "=CHISQ.INV(0.5,2)"
4047/// expected: 1.3862943611198906
4048/// ```
4049///
4050/// ```yaml,sandbox
4051/// title: "Upper quantile for df=10"
4052/// formula: "=CHISQ.INV(0.95,10)"
4053/// expected: 18.307038053275146
4054/// ```
4055#[derive(Debug)]
4056pub struct ChisqInvFn;
4057/// [formualizer-docgen:schema:start]
4058/// Name: CHISQ.INV
4059/// Type: ChisqInvFn
4060/// Min args: 2
4061/// Max args: 2
4062/// Variadic: false
4063/// Signature: CHISQ.INV(arg1: number@scalar, arg2: number@scalar)
4064/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
4065/// Caps: PURE
4066/// [formualizer-docgen:schema:end]
4067impl Function for ChisqInvFn {
4068 func_caps!(PURE);
4069 fn name(&self) -> &'static str {
4070 "CHISQ.INV"
4071 }
4072 fn min_args(&self) -> usize {
4073 2
4074 }
4075 fn arg_schema(&self) -> &'static [ArgSchema] {
4076 use std::sync::LazyLock;
4077 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
4078 vec![
4079 ArgSchema::number_lenient_scalar(),
4080 ArgSchema::number_lenient_scalar(),
4081 ]
4082 });
4083 &SCHEMA[..]
4084 }
4085 fn eval<'a, 'b, 'c>(
4086 &self,
4087 args: &'c [ArgumentHandle<'a, 'b>],
4088 _ctx: &dyn FunctionContext<'b>,
4089 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4090 let p = coerce_num(&scalar_like_value(&args[0])?)?;
4091 let df = coerce_num(&scalar_like_value(&args[1])?)?;
4092
4093 if df < 1.0 {
4094 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4095 ExcelError::new_num(),
4096 )));
4097 }
4098
4099 match chisq_inv(p, df) {
4100 Some(result) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
4101 result,
4102 ))),
4103 None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4104 ExcelError::new_num(),
4105 ))),
4106 }
4107 }
4108}
4109
4110/// Returns the F-distribution probability for `x` with numerator and denominator degrees of freedom.
4111///
4112/// Use `F.DIST` for left-tail cumulative probabilities or density values in variance-ratio tests.
4113///
4114/// # Remarks
4115/// - Set `cumulative` to non-zero for CDF mode, or `0` for PDF mode.
4116/// - Requires `x >= 0`, `deg_freedom1 >= 1`, and `deg_freedom2 >= 1`.
4117/// - Returns `#NUM!` when any domain constraint is violated.
4118/// - Invalid numeric coercions propagate as spreadsheet errors.
4119///
4120/// # Examples
4121///
4122/// ```yaml,sandbox
4123/// title: "F CDF with symmetric 2 and 2 degrees of freedom"
4124/// formula: "=F.DIST(1,2,2,TRUE)"
4125/// expected: 0.5
4126/// ```
4127///
4128/// ```yaml,sandbox
4129/// title: "F PDF with symmetric 2 and 2 degrees of freedom"
4130/// formula: "=F.DIST(1,2,2,FALSE)"
4131/// expected: 0.25
4132/// ```
4133#[derive(Debug)]
4134pub struct FDistFn;
4135/// [formualizer-docgen:schema:start]
4136/// Name: F.DIST
4137/// Type: FDistFn
4138/// Min args: 4
4139/// Max args: 4
4140/// Variadic: false
4141/// Signature: F.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar)
4142/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg4{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
4143/// Caps: PURE
4144/// [formualizer-docgen:schema:end]
4145impl Function for FDistFn {
4146 func_caps!(PURE);
4147 fn name(&self) -> &'static str {
4148 "F.DIST"
4149 }
4150 fn min_args(&self) -> usize {
4151 4
4152 }
4153 fn arg_schema(&self) -> &'static [ArgSchema] {
4154 use std::sync::LazyLock;
4155 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
4156 vec![
4157 ArgSchema::number_lenient_scalar(),
4158 ArgSchema::number_lenient_scalar(),
4159 ArgSchema::number_lenient_scalar(),
4160 ArgSchema::number_lenient_scalar(),
4161 ]
4162 });
4163 &SCHEMA[..]
4164 }
4165 fn eval<'a, 'b, 'c>(
4166 &self,
4167 args: &'c [ArgumentHandle<'a, 'b>],
4168 _ctx: &dyn FunctionContext<'b>,
4169 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4170 let x = coerce_num(&scalar_like_value(&args[0])?)?;
4171 let d1 = coerce_num(&scalar_like_value(&args[1])?)?;
4172 let d2 = coerce_num(&scalar_like_value(&args[2])?)?;
4173 let cumulative = coerce_num(&scalar_like_value(&args[3])?)? != 0.0;
4174
4175 if d1 < 1.0 || d2 < 1.0 || x < 0.0 {
4176 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4177 ExcelError::new_num(),
4178 )));
4179 }
4180
4181 let result = if cumulative {
4182 f_cdf(x, d1, d2)
4183 } else {
4184 f_pdf(x, d1, d2)
4185 };
4186 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
4187 result,
4188 )))
4189 }
4190}
4191
4192/// Returns the F value whose left-tail probability equals `probability`.
4193///
4194/// `F.INV` inverts `F.DIST(x, deg_freedom1, deg_freedom2, TRUE)`.
4195///
4196/// # Remarks
4197/// - `probability` must be strictly between `0` and `1`.
4198/// - `deg_freedom1` and `deg_freedom2` must each be at least `1`.
4199/// - Returns `#NUM!` for invalid probability or degree-of-freedom arguments.
4200/// - Invalid numeric coercions propagate as spreadsheet errors.
4201///
4202/// # Examples
4203///
4204/// ```yaml,sandbox
4205/// title: "Median F quantile with symmetric 2 and 2 degrees of freedom"
4206/// formula: "=F.INV(0.5,2,2)"
4207/// expected: 1
4208/// ```
4209///
4210/// ```yaml,sandbox
4211/// title: "Upper-tail F critical value"
4212/// formula: "=F.INV(0.95,5,10)"
4213/// expected: 3.3258345304130112
4214/// ```
4215#[derive(Debug)]
4216pub struct FInvFn;
4217/// [formualizer-docgen:schema:start]
4218/// Name: F.INV
4219/// Type: FInvFn
4220/// Min args: 3
4221/// Max args: 3
4222/// Variadic: false
4223/// Signature: F.INV(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
4224/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
4225/// Caps: PURE
4226/// [formualizer-docgen:schema:end]
4227impl Function for FInvFn {
4228 func_caps!(PURE);
4229 fn name(&self) -> &'static str {
4230 "F.INV"
4231 }
4232 fn min_args(&self) -> usize {
4233 3
4234 }
4235 fn arg_schema(&self) -> &'static [ArgSchema] {
4236 use std::sync::LazyLock;
4237 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
4238 vec![
4239 ArgSchema::number_lenient_scalar(),
4240 ArgSchema::number_lenient_scalar(),
4241 ArgSchema::number_lenient_scalar(),
4242 ]
4243 });
4244 &SCHEMA[..]
4245 }
4246 fn eval<'a, 'b, 'c>(
4247 &self,
4248 args: &'c [ArgumentHandle<'a, 'b>],
4249 _ctx: &dyn FunctionContext<'b>,
4250 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4251 let p = coerce_num(&scalar_like_value(&args[0])?)?;
4252 let d1 = coerce_num(&scalar_like_value(&args[1])?)?;
4253 let d2 = coerce_num(&scalar_like_value(&args[2])?)?;
4254
4255 if d1 < 1.0 || d2 < 1.0 {
4256 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4257 ExcelError::new_num(),
4258 )));
4259 }
4260
4261 match f_inv(p, d1, d2) {
4262 Some(result) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
4263 result,
4264 ))),
4265 None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4266 ExcelError::new_num(),
4267 ))),
4268 }
4269 }
4270}
4271
4272/// Returns the z-score of `x` relative to a mean and standard deviation.
4273///
4274/// `STANDARDIZE` computes `(x - mean) / standard_dev`.
4275///
4276/// # Remarks
4277/// - `standard_dev` must be strictly greater than `0`.
4278/// - Returns `#NUM!` when `standard_dev <= 0`.
4279/// - Positive output means `x` is above the mean; negative output means below.
4280/// - Invalid numeric coercions propagate as spreadsheet errors.
4281///
4282/// # Examples
4283///
4284/// ```yaml,sandbox
4285/// title: "One standard deviation above the mean"
4286/// formula: "=STANDARDIZE(42,40,2)"
4287/// expected: 1
4288/// ```
4289///
4290/// ```yaml,sandbox
4291/// title: "Exactly at the mean"
4292/// formula: "=STANDARDIZE(100,100,10)"
4293/// expected: 0
4294/// ```
4295#[derive(Debug)]
4296pub struct StandardizeFn;
4297/// [formualizer-docgen:schema:start]
4298/// Name: STANDARDIZE
4299/// Type: StandardizeFn
4300/// Min args: 3
4301/// Max args: 3
4302/// Variadic: false
4303/// Signature: STANDARDIZE(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
4304/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
4305/// Caps: PURE
4306/// [formualizer-docgen:schema:end]
4307impl Function for StandardizeFn {
4308 func_caps!(PURE);
4309 fn name(&self) -> &'static str {
4310 "STANDARDIZE"
4311 }
4312 fn min_args(&self) -> usize {
4313 3
4314 }
4315 fn arg_schema(&self) -> &'static [ArgSchema] {
4316 use std::sync::LazyLock;
4317 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
4318 vec![
4319 ArgSchema::number_lenient_scalar(),
4320 ArgSchema::number_lenient_scalar(),
4321 ArgSchema::number_lenient_scalar(),
4322 ]
4323 });
4324 &SCHEMA[..]
4325 }
4326 fn eval<'a, 'b, 'c>(
4327 &self,
4328 args: &'c [ArgumentHandle<'a, 'b>],
4329 _ctx: &dyn FunctionContext<'b>,
4330 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4331 let x = coerce_num(&scalar_like_value(&args[0])?)?;
4332 let mean = coerce_num(&scalar_like_value(&args[1])?)?;
4333 let std_dev = coerce_num(&scalar_like_value(&args[2])?)?;
4334
4335 if std_dev <= 0.0 {
4336 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4337 ExcelError::new_num(),
4338 )));
4339 }
4340
4341 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
4342 (x - mean) / std_dev,
4343 )))
4344 }
4345}
4346
4347/// Helper: Factorial function
4348fn factorial(n: i64) -> f64 {
4349 if n < 0 {
4350 return f64::NAN;
4351 }
4352 if n <= 1 {
4353 return 1.0;
4354 }
4355 // For large n, use gamma function: n! = Gamma(n+1)
4356 if n > 20 {
4357 return ln_gamma((n + 1) as f64).exp();
4358 }
4359 let mut result = 1.0;
4360 for i in 2..=n {
4361 result *= i as f64;
4362 }
4363 result
4364}
4365
4366/// Helper: Log of binomial coefficient (n choose k)
4367fn ln_binom(n: i64, k: i64) -> f64 {
4368 if k < 0 || k > n {
4369 return f64::NEG_INFINITY;
4370 }
4371 if k == 0 || k == n {
4372 return 0.0;
4373 }
4374 ln_gamma((n + 1) as f64) - ln_gamma((k + 1) as f64) - ln_gamma((n - k + 1) as f64)
4375}
4376
4377/// Returns the binomial probability for a count of successes across independent trials.
4378///
4379/// Use `BINOM.DIST` to evaluate either exact-success probability (PMF) or cumulative probability
4380/// up to a success count (CDF).
4381///
4382/// # Remarks
4383/// - `number_s` and `trials` are truncated to integers.
4384/// - Requires `0 <= number_s <= trials`, `trials >= 0`, and `0 <= probability_s <= 1`.
4385/// - Set `cumulative` to non-zero for CDF mode, or `0` for PMF mode.
4386/// - Returns `#NUM!` for invalid count or probability ranges.
4387///
4388/// # Examples
4389///
4390/// ```yaml,sandbox
4391/// title: "Binomial PMF for exactly 3 successes"
4392/// formula: "=BINOM.DIST(3,10,0.5,FALSE)"
4393/// expected: 0.1171875
4394/// ```
4395///
4396/// ```yaml,sandbox
4397/// title: "Binomial CDF for at most 3 successes"
4398/// formula: "=BINOM.DIST(3,10,0.5,TRUE)"
4399/// expected: 0.171875
4400/// ```
4401#[derive(Debug)]
4402pub struct BinomDistFn;
4403/// [formualizer-docgen:schema:start]
4404/// Name: BINOM.DIST
4405/// Type: BinomDistFn
4406/// Min args: 4
4407/// Max args: 4
4408/// Variadic: false
4409/// Signature: BINOM.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar)
4410/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg4{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
4411/// Caps: PURE
4412/// [formualizer-docgen:schema:end]
4413impl Function for BinomDistFn {
4414 func_caps!(PURE);
4415 fn name(&self) -> &'static str {
4416 "BINOM.DIST"
4417 }
4418 fn min_args(&self) -> usize {
4419 4
4420 }
4421 fn arg_schema(&self) -> &'static [ArgSchema] {
4422 use std::sync::LazyLock;
4423 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
4424 vec![
4425 ArgSchema::number_lenient_scalar(),
4426 ArgSchema::number_lenient_scalar(),
4427 ArgSchema::number_lenient_scalar(),
4428 ArgSchema::number_lenient_scalar(),
4429 ]
4430 });
4431 &SCHEMA[..]
4432 }
4433 fn eval<'a, 'b, 'c>(
4434 &self,
4435 args: &'c [ArgumentHandle<'a, 'b>],
4436 _ctx: &dyn FunctionContext<'b>,
4437 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4438 let k = coerce_num(&scalar_like_value(&args[0])?)?.trunc() as i64;
4439 let n = coerce_num(&scalar_like_value(&args[1])?)?.trunc() as i64;
4440 let p = coerce_num(&scalar_like_value(&args[2])?)?;
4441 let cumulative = coerce_num(&scalar_like_value(&args[3])?)? != 0.0;
4442
4443 if n < 0 || k < 0 || k > n || !(0.0..=1.0).contains(&p) {
4444 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4445 ExcelError::new_num(),
4446 )));
4447 }
4448
4449 let result = if cumulative {
4450 // CDF: sum from i=0 to k of P(X=i)
4451 let mut sum = 0.0;
4452 for i in 0..=k {
4453 let ln_prob =
4454 ln_binom(n, i) + (i as f64) * p.ln() + ((n - i) as f64) * (1.0 - p).ln();
4455 sum += ln_prob.exp();
4456 }
4457 sum
4458 } else {
4459 // PMF: P(X=k)
4460 let ln_prob = ln_binom(n, k) + (k as f64) * p.ln() + ((n - k) as f64) * (1.0 - p).ln();
4461 ln_prob.exp()
4462 };
4463
4464 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
4465 result,
4466 )))
4467 }
4468}
4469
4470/// Returns the Poisson probability for event count `x` at average rate `mean`.
4471///
4472/// `POISSON.DIST` supports exact-count mode (PMF) and cumulative mode (CDF).
4473///
4474/// # Remarks
4475/// - `x` is truncated to an integer and must be at least `0`.
4476/// - `mean` must be non-negative.
4477/// - Set `cumulative` to non-zero for CDF mode, or `0` for PMF mode.
4478/// - Returns `#NUM!` for negative counts or negative mean values.
4479///
4480/// # Examples
4481///
4482/// ```yaml,sandbox
4483/// title: "Poisson PMF for zero events"
4484/// formula: "=POISSON.DIST(0,2,FALSE)"
4485/// expected: 0.1353352832366127
4486/// ```
4487///
4488/// ```yaml,sandbox
4489/// title: "Poisson CDF up to two events"
4490/// formula: "=POISSON.DIST(2,2,TRUE)"
4491/// expected: 0.6766764161830634
4492/// ```
4493#[derive(Debug)]
4494pub struct PoissonDistFn;
4495/// [formualizer-docgen:schema:start]
4496/// Name: POISSON.DIST
4497/// Type: PoissonDistFn
4498/// Min args: 3
4499/// Max args: 3
4500/// Variadic: false
4501/// Signature: POISSON.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
4502/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
4503/// Caps: PURE
4504/// [formualizer-docgen:schema:end]
4505impl Function for PoissonDistFn {
4506 func_caps!(PURE);
4507 fn name(&self) -> &'static str {
4508 "POISSON.DIST"
4509 }
4510 fn min_args(&self) -> usize {
4511 3
4512 }
4513 fn arg_schema(&self) -> &'static [ArgSchema] {
4514 use std::sync::LazyLock;
4515 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
4516 vec![
4517 ArgSchema::number_lenient_scalar(),
4518 ArgSchema::number_lenient_scalar(),
4519 ArgSchema::number_lenient_scalar(),
4520 ]
4521 });
4522 &SCHEMA[..]
4523 }
4524 fn eval<'a, 'b, 'c>(
4525 &self,
4526 args: &'c [ArgumentHandle<'a, 'b>],
4527 _ctx: &dyn FunctionContext<'b>,
4528 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4529 let k = coerce_num(&scalar_like_value(&args[0])?)?.trunc() as i64;
4530 let lambda = coerce_num(&scalar_like_value(&args[1])?)?;
4531 let cumulative = coerce_num(&scalar_like_value(&args[2])?)? != 0.0;
4532
4533 if k < 0 || lambda < 0.0 {
4534 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4535 ExcelError::new_num(),
4536 )));
4537 }
4538
4539 let result = if cumulative {
4540 // CDF: sum from i=0 to k of P(X=i) = 1 - Q(k+1, lambda)
4541 // Using the regularized incomplete gamma function
4542 1.0 - gamma_p((k + 1) as f64, lambda)
4543 } else {
4544 // PMF: P(X=k) = lambda^k * e^(-lambda) / k!
4545 // Use log to avoid overflow
4546 let ln_prob = (k as f64) * lambda.ln() - lambda - ln_gamma((k + 1) as f64);
4547 ln_prob.exp()
4548 };
4549
4550 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
4551 result,
4552 )))
4553 }
4554}
4555
4556/// Returns the exponential-distribution probability at `x` for rate `lambda`.
4557///
4558/// Use `EXPON.DIST` for waiting-time models where events occur with a constant hazard rate.
4559///
4560/// # Remarks
4561/// - Requires `x >= 0` and `lambda > 0`.
4562/// - Set `cumulative` to non-zero for CDF mode, or `0` for PDF mode.
4563/// - Returns `#NUM!` when inputs violate domain requirements.
4564/// - Invalid numeric coercions propagate as spreadsheet errors.
4565///
4566/// # Examples
4567///
4568/// ```yaml,sandbox
4569/// title: "Exponential CDF"
4570/// formula: "=EXPON.DIST(1,1,TRUE)"
4571/// expected: 0.6321205588285577
4572/// ```
4573///
4574/// ```yaml,sandbox
4575/// title: "Exponential PDF"
4576/// formula: "=EXPON.DIST(1,1,FALSE)"
4577/// expected: 0.36787944117144233
4578/// ```
4579#[derive(Debug)]
4580pub struct ExponDistFn;
4581/// [formualizer-docgen:schema:start]
4582/// Name: EXPON.DIST
4583/// Type: ExponDistFn
4584/// Min args: 3
4585/// Max args: 3
4586/// Variadic: false
4587/// Signature: EXPON.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
4588/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
4589/// Caps: PURE
4590/// [formualizer-docgen:schema:end]
4591impl Function for ExponDistFn {
4592 func_caps!(PURE);
4593 fn name(&self) -> &'static str {
4594 "EXPON.DIST"
4595 }
4596 fn min_args(&self) -> usize {
4597 3
4598 }
4599 fn arg_schema(&self) -> &'static [ArgSchema] {
4600 use std::sync::LazyLock;
4601 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
4602 vec![
4603 ArgSchema::number_lenient_scalar(),
4604 ArgSchema::number_lenient_scalar(),
4605 ArgSchema::number_lenient_scalar(),
4606 ]
4607 });
4608 &SCHEMA[..]
4609 }
4610 fn eval<'a, 'b, 'c>(
4611 &self,
4612 args: &'c [ArgumentHandle<'a, 'b>],
4613 _ctx: &dyn FunctionContext<'b>,
4614 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4615 let x = coerce_num(&scalar_like_value(&args[0])?)?;
4616 let lambda = coerce_num(&scalar_like_value(&args[1])?)?;
4617 let cumulative = coerce_num(&scalar_like_value(&args[2])?)? != 0.0;
4618
4619 if x < 0.0 || lambda <= 0.0 {
4620 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4621 ExcelError::new_num(),
4622 )));
4623 }
4624
4625 let result = if cumulative {
4626 // CDF: 1 - e^(-lambda*x)
4627 1.0 - (-lambda * x).exp()
4628 } else {
4629 // PDF: lambda * e^(-lambda*x)
4630 lambda * (-lambda * x).exp()
4631 };
4632
4633 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
4634 result,
4635 )))
4636 }
4637}
4638
4639/// Returns the gamma-distribution probability at `x` for shape `alpha` and scale `beta`.
4640///
4641/// `GAMMA.DIST` supports cumulative and density modes for right-skewed waiting-time models.
4642///
4643/// # Remarks
4644/// - Requires `x >= 0`, `alpha > 0`, and `beta > 0`.
4645/// - Set `cumulative` to non-zero for CDF mode, or `0` for PDF mode.
4646/// - Returns `#NUM!` when any parameter is outside its valid range.
4647/// - Invalid numeric coercions propagate as spreadsheet errors.
4648///
4649/// # Examples
4650///
4651/// ```yaml,sandbox
4652/// title: "Gamma CDF with alpha=1 and beta=2"
4653/// formula: "=GAMMA.DIST(2,1,2,TRUE)"
4654/// expected: 0.6321205588285577
4655/// ```
4656///
4657/// ```yaml,sandbox
4658/// title: "Gamma PDF with alpha=1 and beta=2"
4659/// formula: "=GAMMA.DIST(2,1,2,FALSE)"
4660/// expected: 0.18393972058572117
4661/// ```
4662#[derive(Debug)]
4663pub struct GammaDistFn;
4664/// [formualizer-docgen:schema:start]
4665/// Name: GAMMA.DIST
4666/// Type: GammaDistFn
4667/// Min args: 4
4668/// Max args: 4
4669/// Variadic: false
4670/// Signature: GAMMA.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar)
4671/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg4{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
4672/// Caps: PURE
4673/// [formualizer-docgen:schema:end]
4674impl Function for GammaDistFn {
4675 func_caps!(PURE);
4676 fn name(&self) -> &'static str {
4677 "GAMMA.DIST"
4678 }
4679 fn min_args(&self) -> usize {
4680 4
4681 }
4682 fn arg_schema(&self) -> &'static [ArgSchema] {
4683 use std::sync::LazyLock;
4684 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
4685 vec![
4686 ArgSchema::number_lenient_scalar(),
4687 ArgSchema::number_lenient_scalar(),
4688 ArgSchema::number_lenient_scalar(),
4689 ArgSchema::number_lenient_scalar(),
4690 ]
4691 });
4692 &SCHEMA[..]
4693 }
4694 fn eval<'a, 'b, 'c>(
4695 &self,
4696 args: &'c [ArgumentHandle<'a, 'b>],
4697 _ctx: &dyn FunctionContext<'b>,
4698 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4699 let x = coerce_num(&scalar_like_value(&args[0])?)?;
4700 let alpha = coerce_num(&scalar_like_value(&args[1])?)?; // shape
4701 let beta = coerce_num(&scalar_like_value(&args[2])?)?; // scale
4702 let cumulative = coerce_num(&scalar_like_value(&args[3])?)? != 0.0;
4703
4704 if x < 0.0 || alpha <= 0.0 || beta <= 0.0 {
4705 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4706 ExcelError::new_num(),
4707 )));
4708 }
4709
4710 let result = if cumulative {
4711 // CDF: P(alpha, x/beta) where P is the regularized lower incomplete gamma
4712 gamma_p(alpha, x / beta)
4713 } else {
4714 // PDF: x^(alpha-1) * e^(-x/beta) / (beta^alpha * Gamma(alpha))
4715 let ln_pdf = (alpha - 1.0) * x.ln() - x / beta - alpha * beta.ln() - ln_gamma(alpha);
4716 ln_pdf.exp()
4717 };
4718
4719 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
4720 result,
4721 )))
4722 }
4723}
4724
4725/// Returns the Weibull-distribution probability at `x` for shape `alpha` and scale `beta`.
4726///
4727/// `WEIBULL.DIST` is commonly used for reliability and time-to-failure analysis.
4728///
4729/// # Remarks
4730/// - Requires `x >= 0`, `alpha > 0`, and `beta > 0`.
4731/// - Set `cumulative` to non-zero for CDF mode, or `0` for PDF mode.
4732/// - Returns `#NUM!` when parameters fall outside valid ranges.
4733/// - In PDF mode at `x = 0`, behavior follows the Weibull shape-specific limit.
4734///
4735/// # Examples
4736///
4737/// ```yaml,sandbox
4738/// title: "Weibull CDF with alpha=1 and beta=2"
4739/// formula: "=WEIBULL.DIST(2,1,2,TRUE)"
4740/// expected: 0.6321205588285577
4741/// ```
4742///
4743/// ```yaml,sandbox
4744/// title: "Weibull PDF with alpha=1 and beta=2"
4745/// formula: "=WEIBULL.DIST(2,1,2,FALSE)"
4746/// expected: 0.18393972058572117
4747/// ```
4748#[derive(Debug)]
4749pub struct WeibullDistFn;
4750/// [formualizer-docgen:schema:start]
4751/// Name: WEIBULL.DIST
4752/// Type: WeibullDistFn
4753/// Min args: 4
4754/// Max args: 4
4755/// Variadic: false
4756/// Signature: WEIBULL.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar)
4757/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg4{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
4758/// Caps: PURE
4759/// [formualizer-docgen:schema:end]
4760impl Function for WeibullDistFn {
4761 func_caps!(PURE);
4762 fn name(&self) -> &'static str {
4763 "WEIBULL.DIST"
4764 }
4765 fn min_args(&self) -> usize {
4766 4
4767 }
4768 fn arg_schema(&self) -> &'static [ArgSchema] {
4769 use std::sync::LazyLock;
4770 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
4771 vec![
4772 ArgSchema::number_lenient_scalar(),
4773 ArgSchema::number_lenient_scalar(),
4774 ArgSchema::number_lenient_scalar(),
4775 ArgSchema::number_lenient_scalar(),
4776 ]
4777 });
4778 &SCHEMA[..]
4779 }
4780 fn eval<'a, 'b, 'c>(
4781 &self,
4782 args: &'c [ArgumentHandle<'a, 'b>],
4783 _ctx: &dyn FunctionContext<'b>,
4784 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4785 let x = coerce_num(&scalar_like_value(&args[0])?)?;
4786 let alpha = coerce_num(&scalar_like_value(&args[1])?)?; // shape
4787 let beta = coerce_num(&scalar_like_value(&args[2])?)?; // scale
4788 let cumulative = coerce_num(&scalar_like_value(&args[3])?)? != 0.0;
4789
4790 if x < 0.0 || alpha <= 0.0 || beta <= 0.0 {
4791 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4792 ExcelError::new_num(),
4793 )));
4794 }
4795
4796 let result = if cumulative {
4797 // CDF: 1 - e^(-(x/beta)^alpha)
4798 1.0 - (-(x / beta).powf(alpha)).exp()
4799 } else {
4800 // PDF: (alpha/beta) * (x/beta)^(alpha-1) * e^(-(x/beta)^alpha)
4801 if x == 0.0 {
4802 if alpha < 1.0 {
4803 f64::INFINITY
4804 } else if alpha == 1.0 {
4805 alpha / beta
4806 } else {
4807 0.0
4808 }
4809 } else {
4810 (alpha / beta) * (x / beta).powf(alpha - 1.0) * (-(x / beta).powf(alpha)).exp()
4811 }
4812 };
4813
4814 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
4815 result,
4816 )))
4817 }
4818}
4819
4820/// Returns the beta-distribution probability for `x`, with optional lower/upper bounds.
4821///
4822/// `BETA.DIST` can evaluate either the cumulative probability or density on `[A, B]` (default
4823/// `[0, 1]`).
4824///
4825/// # Remarks
4826/// - Requires `alpha > 0`, `beta > 0`, and `A < B`.
4827/// - `x` must lie within the inclusive interval `[A, B]`.
4828/// - Set `cumulative` to non-zero for CDF mode, or `0` for PDF mode.
4829/// - Returns `#NUM!` for invalid bounds, parameters, or out-of-range `x`.
4830///
4831/// # Examples
4832///
4833/// ```yaml,sandbox
4834/// title: "Uniform beta CDF on [0,1]"
4835/// formula: "=BETA.DIST(0.3,1,1,TRUE)"
4836/// expected: 0.3
4837/// ```
4838///
4839/// ```yaml,sandbox
4840/// title: "Uniform beta PDF on [0,1]"
4841/// formula: "=BETA.DIST(0.3,1,1,FALSE)"
4842/// expected: 1
4843/// ```
4844#[derive(Debug)]
4845pub struct BetaDistFn;
4846/// [formualizer-docgen:schema:start]
4847/// Name: BETA.DIST
4848/// Type: BetaDistFn
4849/// Min args: 4
4850/// Max args: variadic
4851/// Variadic: true
4852/// Signature: BETA.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar, arg5: number@scalar, arg6...: number@scalar)
4853/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg4{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg5{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg6{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
4854/// Caps: PURE
4855/// [formualizer-docgen:schema:end]
4856impl Function for BetaDistFn {
4857 func_caps!(PURE);
4858 fn name(&self) -> &'static str {
4859 "BETA.DIST"
4860 }
4861 fn min_args(&self) -> usize {
4862 4
4863 }
4864 fn variadic(&self) -> bool {
4865 true
4866 }
4867 fn arg_schema(&self) -> &'static [ArgSchema] {
4868 use std::sync::LazyLock;
4869 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
4870 vec![
4871 ArgSchema::number_lenient_scalar(),
4872 ArgSchema::number_lenient_scalar(),
4873 ArgSchema::number_lenient_scalar(),
4874 ArgSchema::number_lenient_scalar(),
4875 ArgSchema::number_lenient_scalar(),
4876 ArgSchema::number_lenient_scalar(),
4877 ]
4878 });
4879 &SCHEMA[..]
4880 }
4881 fn eval<'a, 'b, 'c>(
4882 &self,
4883 args: &'c [ArgumentHandle<'a, 'b>],
4884 _ctx: &dyn FunctionContext<'b>,
4885 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4886 let x = coerce_num(&scalar_like_value(&args[0])?)?;
4887 let alpha = coerce_num(&scalar_like_value(&args[1])?)?;
4888 let beta_param = coerce_num(&scalar_like_value(&args[2])?)?;
4889 let cumulative = coerce_num(&scalar_like_value(&args[3])?)? != 0.0;
4890
4891 // Optional bounds A and B (default 0 and 1)
4892 let a = if args.len() > 4 {
4893 coerce_num(&scalar_like_value(&args[4])?)?
4894 } else {
4895 0.0
4896 };
4897 let b = if args.len() > 5 {
4898 coerce_num(&scalar_like_value(&args[5])?)?
4899 } else {
4900 1.0
4901 };
4902
4903 if alpha <= 0.0 || beta_param <= 0.0 || a >= b {
4904 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4905 ExcelError::new_num(),
4906 )));
4907 }
4908
4909 // x must be in [a, b]
4910 if x < a || x > b {
4911 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4912 ExcelError::new_num(),
4913 )));
4914 }
4915
4916 // Transform x to standard [0,1] interval
4917 let x_std = (x - a) / (b - a);
4918
4919 let result = if cumulative {
4920 // CDF: I_x(alpha, beta) - regularized incomplete beta function
4921 beta_i(x_std, alpha, beta_param)
4922 } else {
4923 // PDF: (x-A)^(alpha-1) * (B-x)^(beta-1) / ((B-A)^(alpha+beta-1) * B(alpha, beta))
4924 let ln_beta = ln_gamma(alpha) + ln_gamma(beta_param) - ln_gamma(alpha + beta_param);
4925 let scale = b - a;
4926 if (x_std == 0.0 && alpha < 1.0) || (x_std == 1.0 && beta_param < 1.0) {
4927 f64::INFINITY
4928 } else if x_std == 0.0 {
4929 if alpha == 1.0 {
4930 (1.0 - x_std).powf(beta_param - 1.0) / (scale * ln_beta.exp())
4931 } else {
4932 0.0
4933 }
4934 } else if x_std == 1.0 {
4935 if beta_param == 1.0 {
4936 x_std.powf(alpha - 1.0) / (scale * ln_beta.exp())
4937 } else {
4938 0.0
4939 }
4940 } else {
4941 let ln_pdf =
4942 (alpha - 1.0) * x_std.ln() + (beta_param - 1.0) * (1.0 - x_std).ln() - ln_beta;
4943 ln_pdf.exp() / scale
4944 }
4945 };
4946
4947 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
4948 result,
4949 )))
4950 }
4951}
4952
4953/// Returns negative-binomial probabilities for failures observed before a target success count.
4954///
4955/// `NEGBINOM.DIST` supports exact-failure mode (PMF) and cumulative mode (CDF).
4956///
4957/// # Remarks
4958/// - `number_f` is truncated and must be `>= 0`.
4959/// - `number_s` is truncated and must be `>= 1`.
4960/// - `probability_s` must satisfy `0 < p < 1`.
4961/// - Returns `#NUM!` when counts or probability are outside valid ranges.
4962///
4963/// # Examples
4964///
4965/// ```yaml,sandbox
4966/// title: "Negative binomial PMF"
4967/// formula: "=NEGBINOM.DIST(2,1,0.5,FALSE)"
4968/// expected: 0.125
4969/// ```
4970///
4971/// ```yaml,sandbox
4972/// title: "Negative binomial CDF"
4973/// formula: "=NEGBINOM.DIST(2,1,0.5,TRUE)"
4974/// expected: 0.875
4975/// ```
4976#[derive(Debug)]
4977pub struct NegbinomDistFn;
4978/// [formualizer-docgen:schema:start]
4979/// Name: NEGBINOM.DIST
4980/// Type: NegbinomDistFn
4981/// Min args: 4
4982/// Max args: 4
4983/// Variadic: false
4984/// Signature: NEGBINOM.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar)
4985/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg4{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
4986/// Caps: PURE
4987/// [formualizer-docgen:schema:end]
4988impl Function for NegbinomDistFn {
4989 func_caps!(PURE);
4990 fn name(&self) -> &'static str {
4991 "NEGBINOM.DIST"
4992 }
4993 fn min_args(&self) -> usize {
4994 4
4995 }
4996 fn arg_schema(&self) -> &'static [ArgSchema] {
4997 use std::sync::LazyLock;
4998 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
4999 vec![
5000 ArgSchema::number_lenient_scalar(),
5001 ArgSchema::number_lenient_scalar(),
5002 ArgSchema::number_lenient_scalar(),
5003 ArgSchema::number_lenient_scalar(),
5004 ]
5005 });
5006 &SCHEMA[..]
5007 }
5008 fn eval<'a, 'b, 'c>(
5009 &self,
5010 args: &'c [ArgumentHandle<'a, 'b>],
5011 _ctx: &dyn FunctionContext<'b>,
5012 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5013 let number_f = coerce_num(&scalar_like_value(&args[0])?)?.trunc() as i64; // number of failures
5014 let number_s = coerce_num(&scalar_like_value(&args[1])?)?.trunc() as i64; // number of successes
5015 let prob_s = coerce_num(&scalar_like_value(&args[2])?)?; // probability of success
5016 let cumulative = coerce_num(&scalar_like_value(&args[3])?)? != 0.0;
5017
5018 if number_f < 0 || number_s < 1 || prob_s <= 0.0 || prob_s >= 1.0 {
5019 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5020 ExcelError::new_num(),
5021 )));
5022 }
5023
5024 let result = if cumulative {
5025 // CDF: sum from i=0 to number_f of P(X=i)
5026 // This is equivalent to I_{prob_s}(number_s, number_f + 1) using regularized beta
5027 beta_i(prob_s, number_s as f64, (number_f + 1) as f64)
5028 } else {
5029 // PMF: C(number_f + number_s - 1, number_s - 1) * prob_s^number_s * (1-prob_s)^number_f
5030 // = C(k + r - 1, r - 1) * p^r * (1-p)^k where k = number_f, r = number_s
5031 let ln_prob = ln_binom(number_f + number_s - 1, number_s - 1)
5032 + (number_s as f64) * prob_s.ln()
5033 + (number_f as f64) * (1.0 - prob_s).ln();
5034 ln_prob.exp()
5035 };
5036
5037 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
5038 result,
5039 )))
5040 }
5041}
5042
5043/// Returns hypergeometric probabilities for successes drawn without replacement.
5044///
5045/// Use `HYPGEOM.DIST` for finite-population sampling where each draw changes remaining odds.
5046///
5047/// # Remarks
5048/// - Count inputs are truncated to integers.
5049/// - Requires valid population/sample bounds and feasible success counts.
5050/// - Set `cumulative` to non-zero for CDF mode, or `0` for PMF mode.
5051/// - Returns `#NUM!` for invalid population setup; out-of-support PMF values return `0`.
5052///
5053/// # Examples
5054///
5055/// ```yaml,sandbox
5056/// title: "Hypergeometric PMF"
5057/// formula: "=HYPGEOM.DIST(1,3,4,10,FALSE)"
5058/// expected: 0.5
5059/// ```
5060///
5061/// ```yaml,sandbox
5062/// title: "Hypergeometric CDF"
5063/// formula: "=HYPGEOM.DIST(1,3,4,10,TRUE)"
5064/// expected: 0.6666666666666666
5065/// ```
5066#[derive(Debug)]
5067pub struct HypgeomDistFn;
5068/// [formualizer-docgen:schema:start]
5069/// Name: HYPGEOM.DIST
5070/// Type: HypgeomDistFn
5071/// Min args: 5
5072/// Max args: 5
5073/// Variadic: false
5074/// Signature: HYPGEOM.DIST(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar, arg5: number@scalar)
5075/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg4{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg5{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
5076/// Caps: PURE
5077/// [formualizer-docgen:schema:end]
5078impl Function for HypgeomDistFn {
5079 func_caps!(PURE);
5080 fn name(&self) -> &'static str {
5081 "HYPGEOM.DIST"
5082 }
5083 fn min_args(&self) -> usize {
5084 5
5085 }
5086 fn arg_schema(&self) -> &'static [ArgSchema] {
5087 use std::sync::LazyLock;
5088 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
5089 vec![
5090 ArgSchema::number_lenient_scalar(),
5091 ArgSchema::number_lenient_scalar(),
5092 ArgSchema::number_lenient_scalar(),
5093 ArgSchema::number_lenient_scalar(),
5094 ArgSchema::number_lenient_scalar(),
5095 ]
5096 });
5097 &SCHEMA[..]
5098 }
5099 fn eval<'a, 'b, 'c>(
5100 &self,
5101 args: &'c [ArgumentHandle<'a, 'b>],
5102 _ctx: &dyn FunctionContext<'b>,
5103 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5104 let sample_s = coerce_num(&scalar_like_value(&args[0])?)?.trunc() as i64; // successes in sample
5105 let number_sample = coerce_num(&scalar_like_value(&args[1])?)?.trunc() as i64; // sample size
5106 let population_s = coerce_num(&scalar_like_value(&args[2])?)?.trunc() as i64; // successes in population
5107 let number_pop = coerce_num(&scalar_like_value(&args[3])?)?.trunc() as i64; // population size
5108 let cumulative = coerce_num(&scalar_like_value(&args[4])?)? != 0.0;
5109
5110 // Validation
5111 if number_pop <= 0
5112 || population_s < 0
5113 || population_s > number_pop
5114 || number_sample < 0
5115 || number_sample > number_pop
5116 || sample_s < 0
5117 {
5118 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5119 ExcelError::new_num(),
5120 )));
5121 }
5122
5123 // sample_s must be at least max(0, number_sample - (number_pop - population_s))
5124 // and at most min(number_sample, population_s)
5125 let min_successes = 0.max(number_sample - (number_pop - population_s));
5126 let max_successes = number_sample.min(population_s);
5127
5128 if sample_s < min_successes || sample_s > max_successes {
5129 // Return 0 for PMF, or appropriate CDF value
5130 if cumulative {
5131 if sample_s < min_successes {
5132 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0)));
5133 } else {
5134 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(1.0)));
5135 }
5136 } else {
5137 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0)));
5138 }
5139 }
5140
5141 let result = if cumulative {
5142 // CDF: sum from i=min_successes to sample_s of P(X=i)
5143 let mut sum = 0.0;
5144 for i in min_successes..=sample_s {
5145 sum += hypgeom_pmf(i, number_sample, population_s, number_pop);
5146 }
5147 sum
5148 } else {
5149 // PMF: C(population_s, sample_s) * C(number_pop - population_s, number_sample - sample_s) / C(number_pop, number_sample)
5150 hypgeom_pmf(sample_s, number_sample, population_s, number_pop)
5151 };
5152
5153 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
5154 result,
5155 )))
5156 }
5157}
5158
5159/// Helper: Hypergeometric PMF
5160fn hypgeom_pmf(k: i64, n: i64, k_pop: i64, n_pop: i64) -> f64 {
5161 // P(X=k) = C(K, k) * C(N-K, n-k) / C(N, n)
5162 // Using logs to avoid overflow
5163 let ln_prob = ln_binom(k_pop, k) + ln_binom(n_pop - k_pop, n - k) - ln_binom(n_pop, n);
5164 ln_prob.exp()
5165}
5166
5167/* ═══════════════════════════════════════════════════════════════════════════
5168COVARIANCE AND CORRELATION FUNCTIONS
5169═══════════════════════════════════════════════════════════════════════════ */
5170
5171/// Returns population covariance for two paired numeric data sets.
5172///
5173/// `COVARIANCE.P` measures joint variability using `n` in the denominator.
5174///
5175/// # Remarks
5176/// - Arrays must resolve to the same number of numeric points.
5177/// - Uses population scaling (`/ n`) rather than sample scaling.
5178/// - Positive output indicates same-direction movement; negative output indicates opposite movement.
5179/// - Pairing and shape mismatches return spreadsheet errors from paired-array validation.
5180///
5181/// # Examples
5182///
5183/// ```yaml,sandbox
5184/// title: "Positive population covariance"
5185/// formula: "=COVARIANCE.P({1,3,5},{2,4,6})"
5186/// expected: 2.6666666666666665
5187/// ```
5188///
5189/// ```yaml,sandbox
5190/// title: "Negative population covariance"
5191/// formula: "=COVARIANCE.P({1,2,3},{3,2,1})"
5192/// expected: -0.6666666666666666
5193/// ```
5194#[derive(Debug)]
5195pub struct CovariancePFn;
5196/// [formualizer-docgen:schema:start]
5197/// Name: COVARIANCE.P
5198/// Type: CovariancePFn
5199/// Min args: 2
5200/// Max args: 1
5201/// Variadic: false
5202/// Signature: COVARIANCE.P(arg1: number@range)
5203/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
5204/// Caps: PURE, REDUCTION, NUMERIC_ONLY
5205/// [formualizer-docgen:schema:end]
5206impl Function for CovariancePFn {
5207 func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
5208 fn name(&self) -> &'static str {
5209 "COVARIANCE.P"
5210 }
5211 fn aliases(&self) -> &'static [&'static str] {
5212 &["COVAR"]
5213 }
5214 fn min_args(&self) -> usize {
5215 2
5216 }
5217 fn arg_schema(&self) -> &'static [ArgSchema] {
5218 &ARG_RANGE_NUM_LENIENT_ONE[..]
5219 }
5220 fn eval<'a, 'b, 'c>(
5221 &self,
5222 args: &'c [ArgumentHandle<'a, 'b>],
5223 _ctx: &dyn FunctionContext<'b>,
5224 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5225 let (y, x) = match collect_paired_arrays(args) {
5226 Ok(v) => v,
5227 Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
5228 };
5229
5230 let n = x.len() as f64;
5231 let mean_x = x.iter().sum::<f64>() / n;
5232 let mean_y = y.iter().sum::<f64>() / n;
5233
5234 let mut sum_xy = 0.0;
5235 for i in 0..x.len() {
5236 let dx = x[i] - mean_x;
5237 let dy = y[i] - mean_y;
5238 sum_xy += dx * dy;
5239 }
5240
5241 // Population covariance divides by n
5242 let covar = sum_xy / n;
5243 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
5244 covar,
5245 )))
5246 }
5247}
5248
5249/// Returns sample covariance for two paired numeric data sets.
5250///
5251/// `COVARIANCE.S` measures joint variability using `n - 1` in the denominator.
5252///
5253/// # Remarks
5254/// - Arrays must contain paired numeric values with matching lengths.
5255/// - Requires at least two paired points.
5256/// - Returns `#DIV/0!` when fewer than two numeric pairs are available.
5257/// - Pairing and shape mismatches return spreadsheet errors from paired-array validation.
5258///
5259/// # Examples
5260///
5261/// ```yaml,sandbox
5262/// title: "Positive sample covariance"
5263/// formula: "=COVARIANCE.S({1,3,5},{2,4,6})"
5264/// expected: 4
5265/// ```
5266///
5267/// ```yaml,sandbox
5268/// title: "Negative sample covariance"
5269/// formula: "=COVARIANCE.S({1,2,3},{3,2,1})"
5270/// expected: -1
5271/// ```
5272#[derive(Debug)]
5273pub struct CovarianceSFn;
5274/// [formualizer-docgen:schema:start]
5275/// Name: COVARIANCE.S
5276/// Type: CovarianceSFn
5277/// Min args: 2
5278/// Max args: 1
5279/// Variadic: false
5280/// Signature: COVARIANCE.S(arg1: number@range)
5281/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
5282/// Caps: PURE, REDUCTION, NUMERIC_ONLY
5283/// [formualizer-docgen:schema:end]
5284impl Function for CovarianceSFn {
5285 func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
5286 fn name(&self) -> &'static str {
5287 "COVARIANCE.S"
5288 }
5289 fn min_args(&self) -> usize {
5290 2
5291 }
5292 fn arg_schema(&self) -> &'static [ArgSchema] {
5293 &ARG_RANGE_NUM_LENIENT_ONE[..]
5294 }
5295 fn eval<'a, 'b, 'c>(
5296 &self,
5297 args: &'c [ArgumentHandle<'a, 'b>],
5298 _ctx: &dyn FunctionContext<'b>,
5299 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5300 let (y, x) = match collect_paired_arrays(args) {
5301 Ok(v) => v,
5302 Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
5303 };
5304
5305 let n = x.len();
5306 if n < 2 {
5307 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5308 ExcelError::new_div(),
5309 )));
5310 }
5311
5312 let mean_x = x.iter().sum::<f64>() / n as f64;
5313 let mean_y = y.iter().sum::<f64>() / n as f64;
5314
5315 let mut sum_xy = 0.0;
5316 for i in 0..n {
5317 let dx = x[i] - mean_x;
5318 let dy = y[i] - mean_y;
5319 sum_xy += dx * dy;
5320 }
5321
5322 // Sample covariance divides by (n - 1)
5323 let covar = sum_xy / (n - 1) as f64;
5324 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
5325 covar,
5326 )))
5327 }
5328}
5329
5330/// Returns the Pearson correlation coefficient between two paired numeric arrays.
5331///
5332/// `PEARSON` reports linear association on a normalized scale from `-1` to `1`.
5333///
5334/// # Remarks
5335/// - Arrays must contain the same number of numeric observations.
5336/// - Returns `#DIV/0!` when either array has zero variance.
5337/// - Positive values indicate positive linear association; negative values indicate inverse association.
5338/// - Pairing and shape mismatches return spreadsheet errors from paired-array validation.
5339///
5340/// # Examples
5341///
5342/// ```yaml,sandbox
5343/// title: "Perfect positive linear correlation"
5344/// formula: "=PEARSON({1,2,3},{2,4,6})"
5345/// expected: 1
5346/// ```
5347///
5348/// ```yaml,sandbox
5349/// title: "Perfect negative linear correlation"
5350/// formula: "=PEARSON({1,2,3},{3,2,1})"
5351/// expected: -1
5352/// ```
5353#[derive(Debug)]
5354pub struct PearsonFn;
5355/// [formualizer-docgen:schema:start]
5356/// Name: PEARSON
5357/// Type: PearsonFn
5358/// Min args: 2
5359/// Max args: 1
5360/// Variadic: false
5361/// Signature: PEARSON(arg1: number@range)
5362/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
5363/// Caps: PURE, REDUCTION, NUMERIC_ONLY
5364/// [formualizer-docgen:schema:end]
5365impl Function for PearsonFn {
5366 func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
5367 fn name(&self) -> &'static str {
5368 "PEARSON"
5369 }
5370 fn min_args(&self) -> usize {
5371 2
5372 }
5373 fn arg_schema(&self) -> &'static [ArgSchema] {
5374 &ARG_RANGE_NUM_LENIENT_ONE[..]
5375 }
5376 fn eval<'a, 'b, 'c>(
5377 &self,
5378 args: &'c [ArgumentHandle<'a, 'b>],
5379 _ctx: &dyn FunctionContext<'b>,
5380 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5381 let (y, x) = match collect_paired_arrays(args) {
5382 Ok(v) => v,
5383 Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
5384 };
5385
5386 let n = x.len() as f64;
5387 let mean_x = x.iter().sum::<f64>() / n;
5388 let mean_y = y.iter().sum::<f64>() / n;
5389
5390 let mut sum_xy = 0.0;
5391 let mut sum_x2 = 0.0;
5392 let mut sum_y2 = 0.0;
5393
5394 for i in 0..x.len() {
5395 let dx = x[i] - mean_x;
5396 let dy = y[i] - mean_y;
5397 sum_xy += dx * dy;
5398 sum_x2 += dx * dx;
5399 sum_y2 += dy * dy;
5400 }
5401
5402 let denom = (sum_x2 * sum_y2).sqrt();
5403 if denom == 0.0 {
5404 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5405 ExcelError::new_div(),
5406 )));
5407 }
5408
5409 let correl = sum_xy / denom;
5410 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
5411 correl,
5412 )))
5413 }
5414}
5415
5416/// Returns the coefficient of determination (`R^2`) for paired x/y data.
5417///
5418/// `RSQ` is the square of Pearson correlation and indicates explained linear variance.
5419///
5420/// # Remarks
5421/// - Arrays must contain the same number of numeric observations.
5422/// - Result is in `[0, 1]` for valid numeric inputs.
5423/// - Returns `#DIV/0!` when either input array has zero variance.
5424/// - Pairing and shape mismatches return spreadsheet errors from paired-array validation.
5425///
5426/// # Examples
5427///
5428/// ```yaml,sandbox
5429/// title: "Perfect linear fit"
5430/// formula: "=RSQ({1,2,3},{2,4,6})"
5431/// expected: 1
5432/// ```
5433///
5434/// ```yaml,sandbox
5435/// title: "Strong but imperfect linear relationship"
5436/// formula: "=RSQ({1,2,3},{1,2,4})"
5437/// expected: 0.9642857142857143
5438/// ```
5439#[derive(Debug)]
5440pub struct RsqFn;
5441/// [formualizer-docgen:schema:start]
5442/// Name: RSQ
5443/// Type: RsqFn
5444/// Min args: 2
5445/// Max args: 1
5446/// Variadic: false
5447/// Signature: RSQ(arg1: number@range)
5448/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
5449/// Caps: PURE, REDUCTION, NUMERIC_ONLY
5450/// [formualizer-docgen:schema:end]
5451impl Function for RsqFn {
5452 func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
5453 fn name(&self) -> &'static str {
5454 "RSQ"
5455 }
5456 fn min_args(&self) -> usize {
5457 2
5458 }
5459 fn arg_schema(&self) -> &'static [ArgSchema] {
5460 &ARG_RANGE_NUM_LENIENT_ONE[..]
5461 }
5462 fn eval<'a, 'b, 'c>(
5463 &self,
5464 args: &'c [ArgumentHandle<'a, 'b>],
5465 _ctx: &dyn FunctionContext<'b>,
5466 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5467 let (y, x) = match collect_paired_arrays(args) {
5468 Ok(v) => v,
5469 Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
5470 };
5471
5472 let n = x.len() as f64;
5473 let mean_x = x.iter().sum::<f64>() / n;
5474 let mean_y = y.iter().sum::<f64>() / n;
5475
5476 let mut sum_xy = 0.0;
5477 let mut sum_x2 = 0.0;
5478 let mut sum_y2 = 0.0;
5479
5480 for i in 0..x.len() {
5481 let dx = x[i] - mean_x;
5482 let dy = y[i] - mean_y;
5483 sum_xy += dx * dy;
5484 sum_x2 += dx * dx;
5485 sum_y2 += dy * dy;
5486 }
5487
5488 let denom = sum_x2 * sum_y2;
5489 if denom == 0.0 {
5490 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5491 ExcelError::new_div(),
5492 )));
5493 }
5494
5495 // R-squared = r^2 = (sum_xy)^2 / (sum_x2 * sum_y2)
5496 let rsq = (sum_xy * sum_xy) / denom;
5497 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(rsq)))
5498 }
5499}
5500
5501/// Returns the standard error of y-estimates from a simple linear regression.
5502///
5503/// `STEYX` measures the typical residual size around the fitted regression line.
5504///
5505/// # Remarks
5506/// - Requires paired x/y inputs with matching numeric lengths.
5507/// - Requires at least three paired points.
5508/// - Returns `#DIV/0!` when `n < 3` or x-values have zero variance.
5509/// - Pairing and shape mismatches return spreadsheet errors from paired-array validation.
5510///
5511/// # Examples
5512///
5513/// ```yaml,sandbox
5514/// title: "Perfect linear fit has zero standard error"
5515/// formula: "=STEYX({2,4,6},{1,2,3})"
5516/// expected: 0
5517/// ```
5518///
5519/// ```yaml,sandbox
5520/// title: "Non-zero regression standard error"
5521/// formula: "=STEYX({2,5,7},{1,2,3})"
5522/// expected: 0.408248290463863
5523/// ```
5524#[derive(Debug)]
5525pub struct SteyxFn;
5526/// [formualizer-docgen:schema:start]
5527/// Name: STEYX
5528/// Type: SteyxFn
5529/// Min args: 2
5530/// Max args: 1
5531/// Variadic: false
5532/// Signature: STEYX(arg1: number@range)
5533/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
5534/// Caps: PURE, REDUCTION, NUMERIC_ONLY
5535/// [formualizer-docgen:schema:end]
5536impl Function for SteyxFn {
5537 func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
5538 fn name(&self) -> &'static str {
5539 "STEYX"
5540 }
5541 fn min_args(&self) -> usize {
5542 2
5543 }
5544 fn arg_schema(&self) -> &'static [ArgSchema] {
5545 &ARG_RANGE_NUM_LENIENT_ONE[..]
5546 }
5547 fn eval<'a, 'b, 'c>(
5548 &self,
5549 args: &'c [ArgumentHandle<'a, 'b>],
5550 _ctx: &dyn FunctionContext<'b>,
5551 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5552 let (y, x) = match collect_paired_arrays(args) {
5553 Ok(v) => v,
5554 Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
5555 };
5556
5557 let n = x.len();
5558 if n < 3 {
5559 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5560 ExcelError::new_div(),
5561 )));
5562 }
5563
5564 let n_f = n as f64;
5565 let mean_x = x.iter().sum::<f64>() / n_f;
5566 let mean_y = y.iter().sum::<f64>() / n_f;
5567
5568 let mut sum_xy = 0.0;
5569 let mut sum_x2 = 0.0;
5570 let mut sum_y2 = 0.0;
5571
5572 for i in 0..n {
5573 let dx = x[i] - mean_x;
5574 let dy = y[i] - mean_y;
5575 sum_xy += dx * dy;
5576 sum_x2 += dx * dx;
5577 sum_y2 += dy * dy;
5578 }
5579
5580 if sum_x2 == 0.0 {
5581 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5582 ExcelError::new_div(),
5583 )));
5584 }
5585
5586 // STEYX = sqrt((sum_y2 - (sum_xy)^2 / sum_x2) / (n - 2))
5587 let sse = sum_y2 - (sum_xy * sum_xy) / sum_x2;
5588 if sse < 0.0 {
5589 // This can happen due to floating point errors; return 0 in such case
5590 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0)));
5591 }
5592 let steyx = (sse / (n_f - 2.0)).sqrt();
5593 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
5594 steyx,
5595 )))
5596 }
5597}
5598
5599/* ─────────────────────────── SKEW ──────────────────────────── */
5600
5601/// Returns the sample skewness of a numeric distribution.
5602///
5603/// `SKEW` quantifies asymmetry: positive values indicate a longer right tail, negative values a
5604/// longer left tail.
5605///
5606/// # Remarks
5607/// - Requires at least three numeric values.
5608/// - Returns `#DIV/0!` when there are fewer than three numbers or zero sample standard deviation.
5609/// - Non-numeric values in ranges are ignored by statistical-collection rules.
5610/// - Uses the Excel-style sample skewness correction factor.
5611///
5612/// # Examples
5613///
5614/// ```yaml,sandbox
5615/// title: "Symmetric sample"
5616/// formula: "=SKEW({1,2,3})"
5617/// expected: 0
5618/// ```
5619///
5620/// ```yaml,sandbox
5621/// title: "Right-skewed sample"
5622/// formula: "=SKEW({1,1,2,10})"
5623/// expected: 1.9683567600862015
5624/// ```
5625#[derive(Debug)]
5626pub struct SkewFn;
5627/// [formualizer-docgen:schema:start]
5628/// Name: SKEW
5629/// Type: SkewFn
5630/// Min args: 1
5631/// Max args: variadic
5632/// Variadic: true
5633/// Signature: SKEW(arg1...: number@range)
5634/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
5635/// Caps: PURE, REDUCTION, NUMERIC_ONLY
5636/// [formualizer-docgen:schema:end]
5637impl Function for SkewFn {
5638 func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
5639 fn name(&self) -> &'static str {
5640 "SKEW"
5641 }
5642 fn min_args(&self) -> usize {
5643 1
5644 }
5645 fn variadic(&self) -> bool {
5646 true
5647 }
5648 fn arg_schema(&self) -> &'static [ArgSchema] {
5649 &ARG_RANGE_NUM_LENIENT_ONE[..]
5650 }
5651 fn eval<'a, 'b, 'c>(
5652 &self,
5653 args: &'c [ArgumentHandle<'a, 'b>],
5654 _ctx: &dyn FunctionContext<'b>,
5655 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5656 let nums = collect_numeric_stats(args)?;
5657 let n = nums.len();
5658
5659 // SKEW requires at least 3 data points
5660 if n < 3 {
5661 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5662 ExcelError::new_div(),
5663 )));
5664 }
5665
5666 let n_f = n as f64;
5667 let mean = nums.iter().sum::<f64>() / n_f;
5668
5669 // Calculate sample standard deviation
5670 let mut sum_sq = 0.0;
5671 for &v in &nums {
5672 let d = v - mean;
5673 sum_sq += d * d;
5674 }
5675 let stdev = (sum_sq / (n_f - 1.0)).sqrt();
5676
5677 if stdev == 0.0 {
5678 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5679 ExcelError::new_div(),
5680 )));
5681 }
5682
5683 // Calculate sum of cubed deviations normalized by stdev
5684 let mut sum_cubed = 0.0;
5685 for &v in &nums {
5686 let d = (v - mean) / stdev;
5687 sum_cubed += d * d * d;
5688 }
5689
5690 // Excel SKEW formula: n / ((n-1)*(n-2)) * sum((xi - mean)/stdev)^3
5691 let skew = (n_f / ((n_f - 1.0) * (n_f - 2.0))) * sum_cubed;
5692 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(skew)))
5693 }
5694}
5695
5696/* ─────────────────────────── KURT ──────────────────────────── */
5697
5698/// Returns the sample excess kurtosis of a numeric distribution.
5699///
5700/// `KURT` indicates tail heaviness relative to a normal distribution after Excel-style sample
5701/// correction.
5702///
5703/// # Remarks
5704/// - Requires at least four numeric values.
5705/// - Returns `#DIV/0!` when there are fewer than four numbers or zero sample standard deviation.
5706/// - Positive values suggest heavier tails; negative values suggest lighter tails.
5707/// - Non-numeric values in ranges are ignored by statistical-collection rules.
5708///
5709/// # Examples
5710///
5711/// ```yaml,sandbox
5712/// title: "Uniformly spaced values"
5713/// formula: "=KURT({1,2,3,4})"
5714/// expected: -1.2
5715/// ```
5716///
5717/// ```yaml,sandbox
5718/// title: "Heavier-tail sample"
5719/// formula: "=KURT({1,1,1,2,10,10,10,10})"
5720/// expected: -2.3069755007920767
5721/// ```
5722#[derive(Debug)]
5723pub struct KurtFn;
5724/// [formualizer-docgen:schema:start]
5725/// Name: KURT
5726/// Type: KurtFn
5727/// Min args: 1
5728/// Max args: variadic
5729/// Variadic: true
5730/// Signature: KURT(arg1...: number@range)
5731/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
5732/// Caps: PURE, REDUCTION, NUMERIC_ONLY
5733/// [formualizer-docgen:schema:end]
5734impl Function for KurtFn {
5735 func_caps!(PURE, NUMERIC_ONLY, REDUCTION);
5736 fn name(&self) -> &'static str {
5737 "KURT"
5738 }
5739 fn min_args(&self) -> usize {
5740 1
5741 }
5742 fn variadic(&self) -> bool {
5743 true
5744 }
5745 fn arg_schema(&self) -> &'static [ArgSchema] {
5746 &ARG_RANGE_NUM_LENIENT_ONE[..]
5747 }
5748 fn eval<'a, 'b, 'c>(
5749 &self,
5750 args: &'c [ArgumentHandle<'a, 'b>],
5751 _ctx: &dyn FunctionContext<'b>,
5752 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5753 let nums = collect_numeric_stats(args)?;
5754 let n = nums.len();
5755
5756 // KURT requires at least 4 data points
5757 if n < 4 {
5758 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5759 ExcelError::new_div(),
5760 )));
5761 }
5762
5763 let n_f = n as f64;
5764 let mean = nums.iter().sum::<f64>() / n_f;
5765
5766 // Calculate sample standard deviation
5767 let mut sum_sq = 0.0;
5768 for &v in &nums {
5769 let d = v - mean;
5770 sum_sq += d * d;
5771 }
5772 let stdev = (sum_sq / (n_f - 1.0)).sqrt();
5773
5774 if stdev == 0.0 {
5775 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5776 ExcelError::new_div(),
5777 )));
5778 }
5779
5780 // Calculate sum of fourth powers of deviations normalized by stdev
5781 let mut sum_fourth = 0.0;
5782 for &v in &nums {
5783 let d = (v - mean) / stdev;
5784 sum_fourth += d * d * d * d;
5785 }
5786
5787 // Excel KURT formula (excess kurtosis):
5788 // n*(n+1) / ((n-1)*(n-2)*(n-3)) * sum((xi - mean)/stdev)^4 - 3*(n-1)^2 / ((n-2)*(n-3))
5789 let term1 = (n_f * (n_f + 1.0)) / ((n_f - 1.0) * (n_f - 2.0) * (n_f - 3.0)) * sum_fourth;
5790 let term2 = (3.0 * (n_f - 1.0) * (n_f - 1.0)) / ((n_f - 2.0) * (n_f - 3.0));
5791 let kurt = term1 - term2;
5792 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(kurt)))
5793 }
5794}
5795
5796/* ─────────────────────────── FISHER ──────────────────────────── */
5797
5798/// Returns the Fisher z-transformation of a correlation-like value `x`.
5799///
5800/// `FISHER` maps `(-1, 1)` into `(-inf, +inf)` and is commonly used in correlation inference.
5801///
5802/// # Remarks
5803/// - Input must satisfy `-1 < x < 1`.
5804/// - Returns `#NUM!` when `x <= -1` or `x >= 1`.
5805/// - The transformation is `0.5 * ln((1 + x) / (1 - x))`.
5806/// - Invalid numeric coercions propagate as spreadsheet errors.
5807///
5808/// # Examples
5809///
5810/// ```yaml,sandbox
5811/// title: "Fisher transform at zero"
5812/// formula: "=FISHER(0)"
5813/// expected: 0
5814/// ```
5815///
5816/// ```yaml,sandbox
5817/// title: "Fisher transform at x=0.5"
5818/// formula: "=FISHER(0.5)"
5819/// expected: 0.5493061443340549
5820/// ```
5821#[derive(Debug)]
5822pub struct FisherFn;
5823/// [formualizer-docgen:schema:start]
5824/// Name: FISHER
5825/// Type: FisherFn
5826/// Min args: 1
5827/// Max args: 1
5828/// Variadic: false
5829/// Signature: FISHER(arg1: number@range)
5830/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
5831/// Caps: PURE, NUMERIC_ONLY
5832/// [formualizer-docgen:schema:end]
5833impl Function for FisherFn {
5834 func_caps!(PURE, NUMERIC_ONLY);
5835 fn name(&self) -> &'static str {
5836 "FISHER"
5837 }
5838 fn min_args(&self) -> usize {
5839 1
5840 }
5841 fn arg_schema(&self) -> &'static [ArgSchema] {
5842 &ARG_RANGE_NUM_LENIENT_ONE[..]
5843 }
5844 fn eval<'a, 'b, 'c>(
5845 &self,
5846 args: &'c [ArgumentHandle<'a, 'b>],
5847 _ctx: &dyn FunctionContext<'b>,
5848 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5849 let x = coerce_num(&scalar_like_value(&args[0])?)?;
5850
5851 // FISHER requires -1 < x < 1
5852 if x <= -1.0 || x >= 1.0 {
5853 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5854 ExcelError::new_num(),
5855 )));
5856 }
5857
5858 // Fisher transformation: 0.5 * ln((1 + x) / (1 - x))
5859 let fisher = 0.5 * ((1.0 + x) / (1.0 - x)).ln();
5860 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
5861 fisher,
5862 )))
5863 }
5864}
5865
5866/* ─────────────────────────── FISHERINV ──────────────────────────── */
5867
5868/// Returns the inverse Fisher transformation of `y`.
5869///
5870/// `FISHERINV` maps Fisher z-values back to the open interval `(-1, 1)`.
5871///
5872/// # Remarks
5873/// - The inverse form is `(e^(2y) - 1) / (e^(2y) + 1)`.
5874/// - Output is always strictly between `-1` and `1` for finite inputs.
5875/// - This function is useful for converting transformed correlation estimates back to r-space.
5876/// - Invalid numeric coercions propagate as spreadsheet errors.
5877///
5878/// # Examples
5879///
5880/// ```yaml,sandbox
5881/// title: "Inverse Fisher at zero"
5882/// formula: "=FISHERINV(0)"
5883/// expected: 0
5884/// ```
5885///
5886/// ```yaml,sandbox
5887/// title: "Round-trip with FISHER(0.5)"
5888/// formula: "=FISHERINV(0.5493061443340549)"
5889/// expected: 0.5
5890/// ```
5891#[derive(Debug)]
5892pub struct FisherInvFn;
5893/// [formualizer-docgen:schema:start]
5894/// Name: FISHERINV
5895/// Type: FisherInvFn
5896/// Min args: 1
5897/// Max args: 1
5898/// Variadic: false
5899/// Signature: FISHERINV(arg1: number@range)
5900/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
5901/// Caps: PURE, NUMERIC_ONLY
5902/// [formualizer-docgen:schema:end]
5903impl Function for FisherInvFn {
5904 func_caps!(PURE, NUMERIC_ONLY);
5905 fn name(&self) -> &'static str {
5906 "FISHERINV"
5907 }
5908 fn min_args(&self) -> usize {
5909 1
5910 }
5911 fn arg_schema(&self) -> &'static [ArgSchema] {
5912 &ARG_RANGE_NUM_LENIENT_ONE[..]
5913 }
5914 fn eval<'a, 'b, 'c>(
5915 &self,
5916 args: &'c [ArgumentHandle<'a, 'b>],
5917 _ctx: &dyn FunctionContext<'b>,
5918 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5919 let y = coerce_num(&scalar_like_value(&args[0])?)?;
5920
5921 // Inverse Fisher transformation: (e^(2y) - 1) / (e^(2y) + 1)
5922 let e2y = (2.0 * y).exp();
5923 let fisherinv = (e2y - 1.0) / (e2y + 1.0);
5924 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
5925 fisherinv,
5926 )))
5927 }
5928}
5929
5930/* ─────────────────────────── FORECAST.LINEAR ──────────────────────────── */
5931
5932/// Returns a predicted y-value at `x` from simple linear regression over known data.
5933///
5934/// `FORECAST.LINEAR` fits `y = intercept + slope * x` and evaluates that line at the requested x.
5935///
5936/// # Remarks
5937/// - Requires `known_y` and `known_x` arrays with the same numeric length.
5938/// - Returns `#N/A` when arrays are empty or lengths do not match.
5939/// - Returns `#DIV/0!` when `known_x` has zero variance.
5940/// - Alias `FORECAST` is supported.
5941///
5942/// # Examples
5943///
5944/// ```yaml,sandbox
5945/// title: "Predict next point on a perfect line"
5946/// formula: "=FORECAST.LINEAR(4,{2,4,6},{1,2,3})"
5947/// expected: 8
5948/// ```
5949///
5950/// ```yaml,sandbox
5951/// title: "Forecast with non-zero intercept"
5952/// formula: "=FORECAST.LINEAR(5,{3,5,7},{1,2,3})"
5953/// expected: 11
5954/// ```
5955#[derive(Debug)]
5956pub struct ForecastLinearFn;
5957/// [formualizer-docgen:schema:start]
5958/// Name: FORECAST.LINEAR
5959/// Type: ForecastLinearFn
5960/// Min args: 3
5961/// Max args: 1
5962/// Variadic: false
5963/// Signature: FORECAST.LINEAR(arg1: number@range)
5964/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
5965/// Caps: PURE, NUMERIC_ONLY
5966/// [formualizer-docgen:schema:end]
5967impl Function for ForecastLinearFn {
5968 func_caps!(PURE, NUMERIC_ONLY);
5969 fn name(&self) -> &'static str {
5970 "FORECAST.LINEAR"
5971 }
5972 fn aliases(&self) -> &'static [&'static str] {
5973 &["FORECAST"]
5974 }
5975 fn min_args(&self) -> usize {
5976 3
5977 }
5978 fn arg_schema(&self) -> &'static [ArgSchema] {
5979 &ARG_RANGE_NUM_LENIENT_ONE[..]
5980 }
5981 fn eval<'a, 'b, 'c>(
5982 &self,
5983 args: &'c [ArgumentHandle<'a, 'b>],
5984 _ctx: &dyn FunctionContext<'b>,
5985 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5986 // args[0] = x value to forecast
5987 // args[1] = known_y's
5988 // args[2] = known_x's
5989 let x = match coerce_num(&scalar_like_value(&args[0])?) {
5990 Ok(n) => n,
5991 Err(_) => {
5992 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5993 ExcelError::new_value(),
5994 )));
5995 }
5996 };
5997
5998 let y_vals = collect_numeric_stats(&args[1..2])?;
5999 let x_vals = collect_numeric_stats(&args[2..3])?;
6000
6001 // Arrays must have same length
6002 if y_vals.len() != x_vals.len() {
6003 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6004 ExcelError::new_na(),
6005 )));
6006 }
6007
6008 if y_vals.is_empty() {
6009 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6010 ExcelError::new_na(),
6011 )));
6012 }
6013
6014 let n = x_vals.len() as f64;
6015 let mean_x = x_vals.iter().sum::<f64>() / n;
6016 let mean_y = y_vals.iter().sum::<f64>() / n;
6017
6018 let mut sum_xy = 0.0;
6019 let mut sum_x2 = 0.0;
6020
6021 for i in 0..x_vals.len() {
6022 let dx = x_vals[i] - mean_x;
6023 let dy = y_vals[i] - mean_y;
6024 sum_xy += dx * dy;
6025 sum_x2 += dx * dx;
6026 }
6027
6028 if sum_x2 == 0.0 {
6029 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6030 ExcelError::new_div(),
6031 )));
6032 }
6033
6034 let slope = sum_xy / sum_x2;
6035 let intercept = mean_y - slope * mean_x;
6036 let forecast = intercept + slope * x;
6037
6038 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
6039 forecast,
6040 )))
6041 }
6042}
6043
6044/* ─────────────────────────── LINEST ──────────────────────────── */
6045
6046/// Returns linear-regression coefficients and optional fit statistics.
6047///
6048/// `LINEST` fits a straight line to known y/x pairs and returns either `[slope, intercept]` or a
6049/// larger statistics matrix.
6050///
6051/// # Remarks
6052/// - `known_y` is required; `known_x` defaults to `1..n` when omitted.
6053/// - `const` controls whether an intercept is fitted (`TRUE` by default).
6054/// - `stats=TRUE` returns a `5x2` result block; otherwise it returns `1x2`.
6055/// - Returns spreadsheet errors for mismatched lengths, empty data, or degenerate x-values.
6056///
6057/// # Examples
6058///
6059/// ```yaml,sandbox
6060/// title: "Slope and intercept only"
6061/// formula: "=LINEST({2,4,6},{1,2,3})"
6062/// expected:
6063/// - [2, 0]
6064/// ```
6065///
6066/// ```yaml,sandbox
6067/// title: "Linear fit with non-zero intercept"
6068/// formula: "=LINEST({3,5,7},{1,2,3})"
6069/// expected:
6070/// - [2, 1]
6071/// ```
6072#[derive(Debug)]
6073pub struct LinestFn;
6074/// [formualizer-docgen:schema:start]
6075/// Name: LINEST
6076/// Type: LinestFn
6077/// Min args: 1
6078/// Max args: variadic
6079/// Variadic: true
6080/// Signature: LINEST(arg1...: number@range)
6081/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
6082/// Caps: PURE, NUMERIC_ONLY
6083/// [formualizer-docgen:schema:end]
6084impl Function for LinestFn {
6085 func_caps!(PURE, NUMERIC_ONLY);
6086 fn name(&self) -> &'static str {
6087 "LINEST"
6088 }
6089 fn min_args(&self) -> usize {
6090 1
6091 }
6092 fn variadic(&self) -> bool {
6093 true
6094 }
6095 fn arg_schema(&self) -> &'static [ArgSchema] {
6096 &ARG_RANGE_NUM_LENIENT_ONE[..]
6097 }
6098 fn eval<'a, 'b, 'c>(
6099 &self,
6100 args: &'c [ArgumentHandle<'a, 'b>],
6101 _ctx: &dyn FunctionContext<'b>,
6102 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
6103 // args[0] = known_y's (required)
6104 // args[1] = known_x's (optional, defaults to {1,2,3,...})
6105 // args[2] = const (optional, default TRUE - whether to compute intercept)
6106 // args[3] = stats (optional, default FALSE - whether to return additional statistics)
6107
6108 let y_vals = collect_numeric_stats(&args[0..1])?;
6109
6110 if y_vals.is_empty() {
6111 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6112 ExcelError::new_na(),
6113 )));
6114 }
6115
6116 // Get known_x's or generate default {1, 2, 3, ...}
6117 let x_vals = if args.len() >= 2 {
6118 collect_numeric_stats(&args[1..2])?
6119 } else {
6120 (1..=y_vals.len()).map(|i| i as f64).collect()
6121 };
6122
6123 // Arrays must have same length
6124 if y_vals.len() != x_vals.len() {
6125 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6126 ExcelError::new_ref(),
6127 )));
6128 }
6129
6130 // Parse const argument (default TRUE)
6131 let use_const = if args.len() >= 3 {
6132 match scalar_like_value(&args[2])? {
6133 LiteralValue::Boolean(b) => b,
6134 LiteralValue::Number(n) => n != 0.0,
6135 LiteralValue::Int(i) => i != 0,
6136 _ => true,
6137 }
6138 } else {
6139 true
6140 };
6141
6142 // Parse stats argument (default FALSE)
6143 let return_stats = if args.len() >= 4 {
6144 match scalar_like_value(&args[3])? {
6145 LiteralValue::Boolean(b) => b,
6146 LiteralValue::Number(n) => n != 0.0,
6147 LiteralValue::Int(i) => i != 0,
6148 _ => false,
6149 }
6150 } else {
6151 false
6152 };
6153
6154 let n = x_vals.len() as f64;
6155
6156 // Calculate regression coefficients
6157 let (slope, intercept) = if use_const {
6158 // Normal linear regression with intercept
6159 let mean_x = x_vals.iter().sum::<f64>() / n;
6160 let mean_y = y_vals.iter().sum::<f64>() / n;
6161
6162 let mut sum_xy = 0.0;
6163 let mut sum_x2 = 0.0;
6164
6165 for i in 0..x_vals.len() {
6166 let dx = x_vals[i] - mean_x;
6167 let dy = y_vals[i] - mean_y;
6168 sum_xy += dx * dy;
6169 sum_x2 += dx * dx;
6170 }
6171
6172 if sum_x2 == 0.0 {
6173 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6174 ExcelError::new_div(),
6175 )));
6176 }
6177
6178 let slope = sum_xy / sum_x2;
6179 let intercept = mean_y - slope * mean_x;
6180 (slope, intercept)
6181 } else {
6182 // Regression through origin (intercept = 0)
6183 let mut sum_xy = 0.0;
6184 let mut sum_x2 = 0.0;
6185
6186 for i in 0..x_vals.len() {
6187 sum_xy += x_vals[i] * y_vals[i];
6188 sum_x2 += x_vals[i] * x_vals[i];
6189 }
6190
6191 if sum_x2 == 0.0 {
6192 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6193 ExcelError::new_div(),
6194 )));
6195 }
6196
6197 let slope = sum_xy / sum_x2;
6198 (slope, 0.0)
6199 };
6200
6201 if !return_stats {
6202 // Return just slope and intercept as 1x2 array: [[slope, intercept]]
6203 let row = vec![LiteralValue::Number(slope), LiteralValue::Number(intercept)];
6204 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Array(vec![
6205 row,
6206 ])));
6207 }
6208
6209 // Calculate additional statistics for stats=TRUE
6210 // Row 1: [slope, intercept]
6211 // Row 2: [se_slope, se_intercept]
6212 // Row 3: [r_squared, se_y]
6213 // Row 4: [F_statistic, df]
6214 // Row 5: [ss_reg, ss_resid]
6215
6216 let mean_y = y_vals.iter().sum::<f64>() / n;
6217
6218 // Calculate residuals and sums of squares
6219 let mut ss_resid = 0.0; // Sum of squared residuals
6220 let mut ss_tot = 0.0; // Total sum of squares
6221
6222 for i in 0..x_vals.len() {
6223 let y_pred = slope * x_vals[i] + intercept;
6224 let residual = y_vals[i] - y_pred;
6225 ss_resid += residual * residual;
6226 let dy_tot = y_vals[i] - mean_y;
6227 ss_tot += dy_tot * dy_tot;
6228 }
6229
6230 let ss_reg = ss_tot - ss_resid; // Regression sum of squares
6231
6232 // R-squared
6233 let r_squared = if ss_tot == 0.0 {
6234 1.0 // Perfect fit or all y values are the same
6235 } else {
6236 1.0 - (ss_resid / ss_tot)
6237 };
6238
6239 // Degrees of freedom
6240 let df = if use_const {
6241 (n as i64 - 2).max(1) as f64 // n - k - 1 where k=1 (one predictor)
6242 } else {
6243 (n as i64 - 1).max(1) as f64 // n - k when no intercept
6244 };
6245
6246 // Standard error of y estimate
6247 let se_y = if df > 0.0 {
6248 (ss_resid / df).sqrt()
6249 } else {
6250 0.0
6251 };
6252
6253 // Standard errors of coefficients
6254 let mean_x = x_vals.iter().sum::<f64>() / n;
6255 let mut sum_x2_centered = 0.0;
6256 let mut sum_x2_raw = 0.0;
6257 for &xi in &x_vals {
6258 sum_x2_centered += (xi - mean_x).powi(2);
6259 sum_x2_raw += xi * xi;
6260 }
6261
6262 let se_slope = if sum_x2_centered > 0.0 && df > 0.0 {
6263 se_y / sum_x2_centered.sqrt()
6264 } else {
6265 f64::NAN
6266 };
6267
6268 let se_intercept = if use_const && sum_x2_centered > 0.0 && df > 0.0 {
6269 se_y * (sum_x2_raw / (n * sum_x2_centered)).sqrt()
6270 } else {
6271 f64::NAN
6272 };
6273
6274 // F-statistic
6275 let f_stat = if ss_resid > 0.0 && df > 0.0 {
6276 (ss_reg / 1.0) / (ss_resid / df) // MSR / MSE
6277 } else if ss_resid == 0.0 {
6278 f64::INFINITY // Perfect fit
6279 } else {
6280 f64::NAN
6281 };
6282
6283 // Build 5x2 result array
6284 let rows = vec![
6285 vec![LiteralValue::Number(slope), LiteralValue::Number(intercept)],
6286 vec![
6287 LiteralValue::Number(se_slope),
6288 LiteralValue::Number(se_intercept),
6289 ],
6290 vec![LiteralValue::Number(r_squared), LiteralValue::Number(se_y)],
6291 vec![LiteralValue::Number(f_stat), LiteralValue::Number(df)],
6292 vec![LiteralValue::Number(ss_reg), LiteralValue::Number(ss_resid)],
6293 ];
6294
6295 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Array(rows)))
6296 }
6297}
6298
6299/* ─────────────────────────── CONFIDENCE.NORM ──────────────────────────── */
6300
6301/// Returns the half-width of a confidence interval using a normal critical value.
6302///
6303/// `CONFIDENCE.NORM` computes `z_crit * standard_dev / sqrt(size)` for two-sided intervals.
6304///
6305/// # Remarks
6306/// - `alpha` must satisfy `0 < alpha < 1`.
6307/// - `standard_dev` must be greater than `0`.
6308/// - `size` must be at least `1`.
6309/// - Returns `#NUM!` when any input is outside valid bounds.
6310///
6311/// # Examples
6312///
6313/// ```yaml,sandbox
6314/// title: "95% confidence half-width"
6315/// formula: "=CONFIDENCE.NORM(0.05,2,100)"
6316/// expected: 0.3919927977622559
6317/// ```
6318///
6319/// ```yaml,sandbox
6320/// title: "90% confidence half-width"
6321/// formula: "=CONFIDENCE.NORM(0.1,5,25)"
6322/// expected: 1.644853625133699
6323/// ```
6324#[derive(Debug)]
6325pub struct ConfidenceNormFn;
6326/// [formualizer-docgen:schema:start]
6327/// Name: CONFIDENCE.NORM
6328/// Type: ConfidenceNormFn
6329/// Min args: 3
6330/// Max args: 3
6331/// Variadic: false
6332/// Signature: CONFIDENCE.NORM(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
6333/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
6334/// Caps: PURE
6335/// [formualizer-docgen:schema:end]
6336impl Function for ConfidenceNormFn {
6337 func_caps!(PURE);
6338 fn name(&self) -> &'static str {
6339 "CONFIDENCE.NORM"
6340 }
6341 fn aliases(&self) -> &'static [&'static str] {
6342 &["CONFIDENCE"]
6343 }
6344 fn min_args(&self) -> usize {
6345 3
6346 }
6347 fn arg_schema(&self) -> &'static [ArgSchema] {
6348 use std::sync::LazyLock;
6349 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
6350 vec![
6351 ArgSchema::number_lenient_scalar(),
6352 ArgSchema::number_lenient_scalar(),
6353 ArgSchema::number_lenient_scalar(),
6354 ]
6355 });
6356 &SCHEMA[..]
6357 }
6358 fn eval<'a, 'b, 'c>(
6359 &self,
6360 args: &'c [ArgumentHandle<'a, 'b>],
6361 _ctx: &dyn FunctionContext<'b>,
6362 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
6363 let alpha = coerce_num(&scalar_like_value(&args[0])?)?;
6364 let std_dev = coerce_num(&scalar_like_value(&args[1])?)?;
6365 let size = coerce_num(&scalar_like_value(&args[2])?)?;
6366
6367 // Validate inputs
6368 if alpha <= 0.0 || alpha >= 1.0 {
6369 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6370 ExcelError::new_num(),
6371 )));
6372 }
6373 if std_dev <= 0.0 {
6374 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6375 ExcelError::new_num(),
6376 )));
6377 }
6378 if size < 1.0 {
6379 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6380 ExcelError::new_num(),
6381 )));
6382 }
6383
6384 // z_crit = NORM.S.INV(1 - alpha/2)
6385 let z_crit = match std_norm_inv(1.0 - alpha / 2.0) {
6386 Some(z) => z,
6387 None => {
6388 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6389 ExcelError::new_num(),
6390 )));
6391 }
6392 };
6393
6394 let result = z_crit * std_dev / size.sqrt();
6395 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
6396 result,
6397 )))
6398 }
6399}
6400
6401/* ─────────────────────────── CONFIDENCE.T ──────────────────────────── */
6402
6403/// Returns the half-width of a confidence interval using a t critical value.
6404///
6405/// `CONFIDENCE.T` is typically used when population standard deviation is unknown and sample size
6406/// is limited.
6407///
6408/// # Remarks
6409/// - `alpha` must satisfy `0 < alpha < 1`.
6410/// - `standard_dev` must be greater than `0`.
6411/// - `size` must be at least `2` so that `df = size - 1` is valid.
6412/// - Returns `#NUM!` when inputs are outside valid bounds.
6413///
6414/// # Examples
6415///
6416/// ```yaml,sandbox
6417/// title: "95% t-interval half-width"
6418/// formula: "=CONFIDENCE.T(0.05,2,25)"
6419/// expected: 0.8256636934020788
6420/// ```
6421///
6422/// ```yaml,sandbox
6423/// title: "90% t-interval half-width"
6424/// formula: "=CONFIDENCE.T(0.1,5,10)"
6425/// expected: 2.9158049866307585
6426/// ```
6427#[derive(Debug)]
6428pub struct ConfidenceTFn;
6429/// [formualizer-docgen:schema:start]
6430/// Name: CONFIDENCE.T
6431/// Type: ConfidenceTFn
6432/// Min args: 3
6433/// Max args: 3
6434/// Variadic: false
6435/// Signature: CONFIDENCE.T(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
6436/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
6437/// Caps: PURE
6438/// [formualizer-docgen:schema:end]
6439impl Function for ConfidenceTFn {
6440 func_caps!(PURE);
6441 fn name(&self) -> &'static str {
6442 "CONFIDENCE.T"
6443 }
6444 fn min_args(&self) -> usize {
6445 3
6446 }
6447 fn arg_schema(&self) -> &'static [ArgSchema] {
6448 use std::sync::LazyLock;
6449 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
6450 vec![
6451 ArgSchema::number_lenient_scalar(),
6452 ArgSchema::number_lenient_scalar(),
6453 ArgSchema::number_lenient_scalar(),
6454 ]
6455 });
6456 &SCHEMA[..]
6457 }
6458 fn eval<'a, 'b, 'c>(
6459 &self,
6460 args: &'c [ArgumentHandle<'a, 'b>],
6461 _ctx: &dyn FunctionContext<'b>,
6462 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
6463 let alpha = coerce_num(&scalar_like_value(&args[0])?)?;
6464 let std_dev = coerce_num(&scalar_like_value(&args[1])?)?;
6465 let size = coerce_num(&scalar_like_value(&args[2])?)?;
6466
6467 // Validate inputs - size must be >= 2 for t-distribution (df = size - 1 >= 1)
6468 if alpha <= 0.0 || alpha >= 1.0 {
6469 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6470 ExcelError::new_num(),
6471 )));
6472 }
6473 if std_dev <= 0.0 {
6474 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6475 ExcelError::new_num(),
6476 )));
6477 }
6478 if size < 2.0 {
6479 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6480 ExcelError::new_num(),
6481 )));
6482 }
6483
6484 let df = size - 1.0;
6485
6486 // t_crit = T.INV(1 - alpha/2, df)
6487 let t_crit = match t_inv(1.0 - alpha / 2.0, df) {
6488 Some(t) => t,
6489 None => {
6490 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6491 ExcelError::new_num(),
6492 )));
6493 }
6494 };
6495
6496 let result = t_crit * std_dev / size.sqrt();
6497 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
6498 result,
6499 )))
6500 }
6501}
6502
6503/* ─────────────────────────── Z.TEST ──────────────────────────── */
6504
6505/// Returns the one-tailed p-value of a z-test against hypothesized mean `x`.
6506///
6507/// `Z.TEST` evaluates whether the sample mean is significantly greater than the target value.
6508///
6509/// # Remarks
6510/// - Uses provided `sigma` when supplied; otherwise computes population standard deviation.
6511/// - Returns `#NUM!` when `sigma <= 0`.
6512/// - Returns `#DIV/0!` when implied standard deviation is zero.
6513/// - Returns `#N/A` when the data array has no numeric values.
6514///
6515/// # Examples
6516///
6517/// ```yaml,sandbox
6518/// title: "Z-test with provided sigma"
6519/// formula: "=Z.TEST({1,2,3,4,5},2,1)"
6520/// expected: 0.012673659338734137
6521/// ```
6522///
6523/// ```yaml,sandbox
6524/// title: "Z-test with sigma estimated from sample"
6525/// formula: "=Z.TEST({1,2,3,4,5},2)"
6526/// expected: 0.056923149003329065
6527/// ```
6528#[derive(Debug)]
6529pub struct ZTestFn;
6530/// [formualizer-docgen:schema:start]
6531/// Name: Z.TEST
6532/// Type: ZTestFn
6533/// Min args: 2
6534/// Max args: variadic
6535/// Variadic: true
6536/// Signature: Z.TEST(arg1: number@range, arg2: number@scalar, arg3...: number@scalar)
6537/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
6538/// Caps: PURE
6539/// [formualizer-docgen:schema:end]
6540impl Function for ZTestFn {
6541 func_caps!(PURE);
6542 fn name(&self) -> &'static str {
6543 "Z.TEST"
6544 }
6545 fn aliases(&self) -> &'static [&'static str] {
6546 &["ZTEST"]
6547 }
6548 fn min_args(&self) -> usize {
6549 2
6550 }
6551 fn variadic(&self) -> bool {
6552 true
6553 }
6554 fn arg_schema(&self) -> &'static [ArgSchema] {
6555 use std::sync::LazyLock;
6556 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
6557 vec![
6558 {
6559 let mut s = ArgSchema::number_lenient_scalar();
6560 s.shape = crate::args::ShapeKind::Range;
6561 s.coercion = formualizer_common::CoercionPolicy::NumberLenientText;
6562 s
6563 },
6564 ArgSchema::number_lenient_scalar(),
6565 ArgSchema::number_lenient_scalar(), // optional sigma
6566 ]
6567 });
6568 &SCHEMA[..]
6569 }
6570 fn eval<'a, 'b, 'c>(
6571 &self,
6572 args: &'c [ArgumentHandle<'a, 'b>],
6573 _ctx: &dyn FunctionContext<'b>,
6574 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
6575 // Collect numeric values from the array argument
6576 let data = collect_numeric_stats(&args[0..1])?;
6577
6578 if data.is_empty() {
6579 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6580 ExcelError::new_na(),
6581 )));
6582 }
6583
6584 let x = coerce_num(&scalar_like_value(&args[1])?)?;
6585
6586 let n = data.len() as f64;
6587 let mean: f64 = data.iter().sum::<f64>() / n;
6588
6589 // Calculate sigma: use provided value or compute population std dev
6590 let sigma = if args.len() > 2 {
6591 let s = coerce_num(&scalar_like_value(&args[2])?)?;
6592 if s <= 0.0 {
6593 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6594 ExcelError::new_num(),
6595 )));
6596 }
6597 s
6598 } else {
6599 // Population standard deviation
6600 let variance: f64 = data.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / n;
6601 let std_dev = variance.sqrt();
6602 if std_dev == 0.0 {
6603 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6604 ExcelError::new_div(),
6605 )));
6606 }
6607 std_dev
6608 };
6609
6610 // z = (mean - x) / (sigma / sqrt(n))
6611 let z = (mean - x) / (sigma / n.sqrt());
6612
6613 // P-value = 1 - NORM.S.DIST(z, TRUE)
6614 let p_value = 1.0 - std_norm_cdf(z);
6615
6616 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
6617 p_value,
6618 )))
6619 }
6620}
6621
6622/* ─────────────────────────── TREND ──────────────────────────── */
6623
6624/// Returns fitted y-values along a linear trend derived from known data.
6625///
6626/// `TREND` performs simple linear regression and returns predictions for `new_x` (or defaults).
6627///
6628/// # Remarks
6629/// - `known_y` is required; `known_x` defaults to `1..n` when omitted.
6630/// - `new_x` defaults to `known_x` when omitted.
6631/// - `const` defaults to `TRUE`; set to `FALSE` to force a zero intercept.
6632/// - Returns spreadsheet errors for empty data, mismatched lengths, or degenerate x-variance.
6633///
6634/// # Examples
6635///
6636/// ```yaml,sandbox
6637/// title: "Predict two future points on a line"
6638/// formula: "=TREND({2,4,6},{1,2,3},{4,5})"
6639/// expected:
6640/// - [8, 10]
6641/// ```
6642///
6643/// ```yaml,sandbox
6644/// title: "Default x-values with fitted trend"
6645/// formula: "=TREND({3,5,7})"
6646/// expected:
6647/// - [3, 5, 7]
6648/// ```
6649#[derive(Debug)]
6650pub struct TrendFn;
6651/// [formualizer-docgen:schema:start]
6652/// Name: TREND
6653/// Type: TrendFn
6654/// Min args: 1
6655/// Max args: variadic
6656/// Variadic: true
6657/// Signature: TREND(arg1...: number@range)
6658/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
6659/// Caps: PURE, NUMERIC_ONLY
6660/// [formualizer-docgen:schema:end]
6661impl Function for TrendFn {
6662 func_caps!(PURE, NUMERIC_ONLY);
6663 fn name(&self) -> &'static str {
6664 "TREND"
6665 }
6666 fn min_args(&self) -> usize {
6667 1
6668 }
6669 fn variadic(&self) -> bool {
6670 true
6671 }
6672 fn arg_schema(&self) -> &'static [ArgSchema] {
6673 &ARG_RANGE_NUM_LENIENT_ONE[..]
6674 }
6675 fn eval<'a, 'b, 'c>(
6676 &self,
6677 args: &'c [ArgumentHandle<'a, 'b>],
6678 _ctx: &dyn FunctionContext<'b>,
6679 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
6680 // TREND: args[0] = known_y's (required)
6681 // args[1] = known_x's (optional, defaults to {1,2,3,...})
6682 // args[2] = new_x's (optional, defaults to known_x's)
6683 // args[3] = const (optional, default TRUE - whether to compute intercept)
6684
6685 let y_vals = collect_numeric_stats(&args[0..1])?;
6686
6687 if y_vals.is_empty() {
6688 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6689 ExcelError::new_na(),
6690 )));
6691 }
6692
6693 // Helper to check if argument is empty/omitted
6694 // Note: Empty arguments are represented as empty text strings by the parser
6695 fn is_arg_empty(arg: &ArgumentHandle) -> bool {
6696 match scalar_like_value(arg) {
6697 Ok(LiteralValue::Empty) => true,
6698 Ok(LiteralValue::Text(s)) if s.is_empty() => true,
6699 _ => false,
6700 }
6701 }
6702
6703 // Get known_x's or generate default {1, 2, 3, ...}
6704 let x_vals = if args.len() >= 2 && !is_arg_empty(&args[1]) {
6705 collect_numeric_stats(&args[1..2])?
6706 } else {
6707 (1..=y_vals.len()).map(|i| i as f64).collect()
6708 };
6709
6710 // Arrays must have same length
6711 if y_vals.len() != x_vals.len() {
6712 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6713 ExcelError::new_ref(),
6714 )));
6715 }
6716
6717 // Get new_x's or use known_x's - check if argument is empty/omitted
6718 let new_x_vals = if args.len() >= 3 && !is_arg_empty(&args[2]) {
6719 collect_numeric_stats(&args[2..3])?
6720 } else {
6721 x_vals.clone()
6722 };
6723
6724 if new_x_vals.is_empty() {
6725 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6726 ExcelError::new_na(),
6727 )));
6728 }
6729
6730 // Parse const argument (default TRUE)
6731 let use_const = if args.len() >= 4 {
6732 match scalar_like_value(&args[3])? {
6733 LiteralValue::Boolean(b) => b,
6734 LiteralValue::Number(n) => n != 0.0,
6735 LiteralValue::Int(i) => i != 0,
6736 LiteralValue::Empty => true, // empty defaults to TRUE
6737 _ => true,
6738 }
6739 } else {
6740 true
6741 };
6742
6743 let n = x_vals.len() as f64;
6744
6745 // Calculate regression coefficients
6746 let (slope, intercept) = if use_const {
6747 // Normal linear regression with intercept
6748 let mean_x = x_vals.iter().sum::<f64>() / n;
6749 let mean_y = y_vals.iter().sum::<f64>() / n;
6750
6751 let mut sum_xy = 0.0;
6752 let mut sum_x2 = 0.0;
6753
6754 for i in 0..x_vals.len() {
6755 let dx = x_vals[i] - mean_x;
6756 let dy = y_vals[i] - mean_y;
6757 sum_xy += dx * dy;
6758 sum_x2 += dx * dx;
6759 }
6760
6761 if sum_x2 == 0.0 {
6762 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6763 ExcelError::new_div(),
6764 )));
6765 }
6766
6767 let slope = sum_xy / sum_x2;
6768 let intercept = mean_y - slope * mean_x;
6769 (slope, intercept)
6770 } else {
6771 // Regression through origin (intercept = 0)
6772 let mut sum_xy = 0.0;
6773 let mut sum_x2 = 0.0;
6774
6775 for i in 0..x_vals.len() {
6776 sum_xy += x_vals[i] * y_vals[i];
6777 sum_x2 += x_vals[i] * x_vals[i];
6778 }
6779
6780 if sum_x2 == 0.0 {
6781 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6782 ExcelError::new_div(),
6783 )));
6784 }
6785
6786 let slope = sum_xy / sum_x2;
6787 (slope, 0.0)
6788 };
6789
6790 // Calculate predicted y values for new_x's
6791 let predicted: Vec<LiteralValue> = new_x_vals
6792 .iter()
6793 .map(|&x| LiteralValue::Number(slope * x + intercept))
6794 .collect();
6795
6796 // Return as 1xN array (row vector)
6797 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Array(vec![
6798 predicted,
6799 ])))
6800 }
6801}
6802
6803/* ─────────────────────────── GROWTH ──────────────────────────── */
6804
6805/// Returns fitted values from an exponential trend model.
6806///
6807/// `GROWTH` fits `y = b * m^x` by linearizing in log space, then returns predictions for `new_x`.
6808///
6809/// # Remarks
6810/// - All known y-values must be strictly greater than `0`.
6811/// - `known_x` defaults to `1..n`; `new_x` defaults to `known_x`.
6812/// - `const` defaults to `TRUE`; set to `FALSE` to force `b = 1`.
6813/// - Returns spreadsheet errors for invalid domains, mismatched lengths, or degenerate x-variance.
6814///
6815/// # Examples
6816///
6817/// ```yaml,sandbox
6818/// title: "Exponential growth forecast"
6819/// formula: "=GROWTH({2,4,8},{1,2,3},{4,5})"
6820/// expected:
6821/// - [16, 32]
6822/// ```
6823///
6824/// ```yaml,sandbox
6825/// title: "Default x-values with perfect doubling pattern"
6826/// formula: "=GROWTH({3,6,12})"
6827/// expected:
6828/// - [3, 6, 12]
6829/// ```
6830#[derive(Debug)]
6831pub struct GrowthFn;
6832/// [formualizer-docgen:schema:start]
6833/// Name: GROWTH
6834/// Type: GrowthFn
6835/// Min args: 1
6836/// Max args: variadic
6837/// Variadic: true
6838/// Signature: GROWTH(arg1...: number@range)
6839/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
6840/// Caps: PURE, NUMERIC_ONLY
6841/// [formualizer-docgen:schema:end]
6842impl Function for GrowthFn {
6843 func_caps!(PURE, NUMERIC_ONLY);
6844 fn name(&self) -> &'static str {
6845 "GROWTH"
6846 }
6847 fn min_args(&self) -> usize {
6848 1
6849 }
6850 fn variadic(&self) -> bool {
6851 true
6852 }
6853 fn arg_schema(&self) -> &'static [ArgSchema] {
6854 &ARG_RANGE_NUM_LENIENT_ONE[..]
6855 }
6856 fn eval<'a, 'b, 'c>(
6857 &self,
6858 args: &'c [ArgumentHandle<'a, 'b>],
6859 _ctx: &dyn FunctionContext<'b>,
6860 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
6861 // GROWTH: args[0] = known_y's (required)
6862 // args[1] = known_x's (optional, defaults to {1,2,3,...})
6863 // args[2] = new_x's (optional, defaults to known_x's)
6864 // args[3] = const (optional, default TRUE - whether to compute intercept)
6865
6866 let y_vals = collect_numeric_stats(&args[0..1])?;
6867
6868 if y_vals.is_empty() {
6869 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6870 ExcelError::new_na(),
6871 )));
6872 }
6873
6874 // Check that all y values are positive (required for log transformation)
6875 for &y in &y_vals {
6876 if y <= 0.0 {
6877 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6878 ExcelError::new_num(),
6879 )));
6880 }
6881 }
6882
6883 // Helper to check if argument is empty/omitted
6884 // Note: Empty arguments are represented as empty text strings by the parser
6885 fn is_arg_empty(arg: &ArgumentHandle) -> bool {
6886 match scalar_like_value(arg) {
6887 Ok(LiteralValue::Empty) => true,
6888 Ok(LiteralValue::Text(s)) if s.is_empty() => true,
6889 _ => false,
6890 }
6891 }
6892
6893 // Get known_x's or generate default {1, 2, 3, ...}
6894 let x_vals = if args.len() >= 2 && !is_arg_empty(&args[1]) {
6895 collect_numeric_stats(&args[1..2])?
6896 } else {
6897 (1..=y_vals.len()).map(|i| i as f64).collect()
6898 };
6899
6900 // Arrays must have same length
6901 if y_vals.len() != x_vals.len() {
6902 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6903 ExcelError::new_ref(),
6904 )));
6905 }
6906
6907 // Get new_x's or use known_x's - check if argument is empty/omitted
6908 let new_x_vals = if args.len() >= 3 && !is_arg_empty(&args[2]) {
6909 collect_numeric_stats(&args[2..3])?
6910 } else {
6911 x_vals.clone()
6912 };
6913
6914 if new_x_vals.is_empty() {
6915 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6916 ExcelError::new_na(),
6917 )));
6918 }
6919
6920 // Parse const argument (default TRUE)
6921 let use_const = if args.len() >= 4 {
6922 match scalar_like_value(&args[3])? {
6923 LiteralValue::Boolean(b) => b,
6924 LiteralValue::Number(n) => n != 0.0,
6925 LiteralValue::Int(i) => i != 0,
6926 LiteralValue::Empty => true, // empty defaults to TRUE
6927 _ => true,
6928 }
6929 } else {
6930 true
6931 };
6932
6933 // Transform to log space: ln(y) = ln(b) + x*ln(m)
6934 // This is linear regression on log-transformed y values
6935 let ln_y_vals: Vec<f64> = y_vals.iter().map(|&y| y.ln()).collect();
6936
6937 let n = x_vals.len() as f64;
6938
6939 // Calculate regression coefficients in log space
6940 let (ln_m, ln_b) = if use_const {
6941 // Normal linear regression with intercept
6942 let mean_x = x_vals.iter().sum::<f64>() / n;
6943 let mean_ln_y = ln_y_vals.iter().sum::<f64>() / n;
6944
6945 let mut sum_xy = 0.0;
6946 let mut sum_x2 = 0.0;
6947
6948 for i in 0..x_vals.len() {
6949 let dx = x_vals[i] - mean_x;
6950 let dy = ln_y_vals[i] - mean_ln_y;
6951 sum_xy += dx * dy;
6952 sum_x2 += dx * dx;
6953 }
6954
6955 if sum_x2 == 0.0 {
6956 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6957 ExcelError::new_div(),
6958 )));
6959 }
6960
6961 let ln_m = sum_xy / sum_x2;
6962 let ln_b = mean_ln_y - ln_m * mean_x;
6963 (ln_m, ln_b)
6964 } else {
6965 // Regression through origin in log space (ln_b = 0, so b = 1)
6966 let mut sum_xy = 0.0;
6967 let mut sum_x2 = 0.0;
6968
6969 for i in 0..x_vals.len() {
6970 sum_xy += x_vals[i] * ln_y_vals[i];
6971 sum_x2 += x_vals[i] * x_vals[i];
6972 }
6973
6974 if sum_x2 == 0.0 {
6975 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
6976 ExcelError::new_div(),
6977 )));
6978 }
6979
6980 let ln_m = sum_xy / sum_x2;
6981 (ln_m, 0.0)
6982 };
6983
6984 // Convert back from log space: m = e^ln_m, b = e^ln_b
6985 let m = ln_m.exp();
6986 let b = ln_b.exp();
6987
6988 // Calculate predicted y values: y = b * m^x
6989 let predicted: Vec<LiteralValue> = new_x_vals
6990 .iter()
6991 .map(|&x| LiteralValue::Number(b * m.powf(x)))
6992 .collect();
6993
6994 // Return as 1xN array (row vector)
6995 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Array(vec![
6996 predicted,
6997 ])))
6998 }
6999}
7000
7001/* ─────────────────────────── LOGEST ──────────────────────────── */
7002
7003/// Returns parameters for an exponential model fitted to known data.
7004///
7005/// `LOGEST` fits `y = b * m^x` and returns either `[m, b]` or an expanded statistics matrix.
7006///
7007/// # Remarks
7008/// - All known y-values must be strictly greater than `0`.
7009/// - `known_x` defaults to `1..n` when omitted.
7010/// - `const` controls whether `b` is fitted (`TRUE` by default).
7011/// - `stats=TRUE` returns a `5x2` statistics block; otherwise returns `1x2`.
7012///
7013/// # Examples
7014///
7015/// ```yaml,sandbox
7016/// title: "Exponential base and intercept"
7017/// formula: "=LOGEST({2,4,8},{1,2,3})"
7018/// expected:
7019/// - [2, 1]
7020/// ```
7021///
7022/// ```yaml,sandbox
7023/// title: "Alternative growth series"
7024/// formula: "=LOGEST({3,6,12},{1,2,3})"
7025/// expected:
7026/// - [2, 1.5]
7027/// ```
7028#[derive(Debug)]
7029pub struct LogestFn;
7030/// [formualizer-docgen:schema:start]
7031/// Name: LOGEST
7032/// Type: LogestFn
7033/// Min args: 1
7034/// Max args: variadic
7035/// Variadic: true
7036/// Signature: LOGEST(arg1...: number@range)
7037/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
7038/// Caps: PURE, NUMERIC_ONLY
7039/// [formualizer-docgen:schema:end]
7040impl Function for LogestFn {
7041 func_caps!(PURE, NUMERIC_ONLY);
7042 fn name(&self) -> &'static str {
7043 "LOGEST"
7044 }
7045 fn min_args(&self) -> usize {
7046 1
7047 }
7048 fn variadic(&self) -> bool {
7049 true
7050 }
7051 fn arg_schema(&self) -> &'static [ArgSchema] {
7052 &ARG_RANGE_NUM_LENIENT_ONE[..]
7053 }
7054 fn eval<'a, 'b, 'c>(
7055 &self,
7056 args: &'c [ArgumentHandle<'a, 'b>],
7057 _ctx: &dyn FunctionContext<'b>,
7058 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
7059 // args[0] = known_y's (required)
7060 // args[1] = known_x's (optional, defaults to {1,2,3,...})
7061 // args[2] = const (optional, default TRUE - whether to compute b)
7062 // args[3] = stats (optional, default FALSE - whether to return additional statistics)
7063
7064 let y_vals = collect_numeric_stats(&args[0..1])?;
7065
7066 if y_vals.is_empty() {
7067 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7068 ExcelError::new_na(),
7069 )));
7070 }
7071
7072 // Check that all y values are positive (required for log transformation)
7073 for &y in &y_vals {
7074 if y <= 0.0 {
7075 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7076 ExcelError::new_num(),
7077 )));
7078 }
7079 }
7080
7081 // Get known_x's or generate default {1, 2, 3, ...}
7082 let x_vals = if args.len() >= 2 {
7083 collect_numeric_stats(&args[1..2])?
7084 } else {
7085 (1..=y_vals.len()).map(|i| i as f64).collect()
7086 };
7087
7088 // Arrays must have same length
7089 if y_vals.len() != x_vals.len() {
7090 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7091 ExcelError::new_ref(),
7092 )));
7093 }
7094
7095 // Parse const argument (default TRUE)
7096 let use_const = if args.len() >= 3 {
7097 match scalar_like_value(&args[2])? {
7098 LiteralValue::Boolean(b) => b,
7099 LiteralValue::Number(n) => n != 0.0,
7100 LiteralValue::Int(i) => i != 0,
7101 _ => true,
7102 }
7103 } else {
7104 true
7105 };
7106
7107 // Parse stats argument (default FALSE)
7108 let return_stats = if args.len() >= 4 {
7109 match scalar_like_value(&args[3])? {
7110 LiteralValue::Boolean(b) => b,
7111 LiteralValue::Number(n) => n != 0.0,
7112 LiteralValue::Int(i) => i != 0,
7113 _ => false,
7114 }
7115 } else {
7116 false
7117 };
7118
7119 // Transform to log space: ln(y) = ln(b) + x*ln(m)
7120 let ln_y_vals: Vec<f64> = y_vals.iter().map(|&y| y.ln()).collect();
7121
7122 let n = x_vals.len() as f64;
7123
7124 // Calculate regression coefficients in log space
7125 let (ln_m, ln_b) = if use_const {
7126 // Normal linear regression with intercept
7127 let mean_x = x_vals.iter().sum::<f64>() / n;
7128 let mean_ln_y = ln_y_vals.iter().sum::<f64>() / n;
7129
7130 let mut sum_xy = 0.0;
7131 let mut sum_x2 = 0.0;
7132
7133 for i in 0..x_vals.len() {
7134 let dx = x_vals[i] - mean_x;
7135 let dy = ln_y_vals[i] - mean_ln_y;
7136 sum_xy += dx * dy;
7137 sum_x2 += dx * dx;
7138 }
7139
7140 if sum_x2 == 0.0 {
7141 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7142 ExcelError::new_div(),
7143 )));
7144 }
7145
7146 let ln_m = sum_xy / sum_x2;
7147 let ln_b = mean_ln_y - ln_m * mean_x;
7148 (ln_m, ln_b)
7149 } else {
7150 // Regression through origin in log space (ln_b = 0, so b = 1)
7151 let mut sum_xy = 0.0;
7152 let mut sum_x2 = 0.0;
7153
7154 for i in 0..x_vals.len() {
7155 sum_xy += x_vals[i] * ln_y_vals[i];
7156 sum_x2 += x_vals[i] * x_vals[i];
7157 }
7158
7159 if sum_x2 == 0.0 {
7160 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7161 ExcelError::new_div(),
7162 )));
7163 }
7164
7165 let ln_m = sum_xy / sum_x2;
7166 (ln_m, 0.0)
7167 };
7168
7169 // Convert from log space to get m and b
7170 let m = ln_m.exp();
7171 let b = ln_b.exp();
7172
7173 if !return_stats {
7174 // Return just m and b as 1x2 array: [[m, b]]
7175 let row = vec![LiteralValue::Number(m), LiteralValue::Number(b)];
7176 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Array(vec![
7177 row,
7178 ])));
7179 }
7180
7181 // Calculate additional statistics for stats=TRUE
7182 // Statistics are computed in log space, then converted
7183 // Row 1: [m, b]
7184 // Row 2: [se_m, se_b] - standard errors (converted from log space)
7185 // Row 3: [r_squared, se_y] - R-squared and standard error of y estimate
7186 // Row 4: [F_statistic, df] - F-statistic and degrees of freedom
7187 // Row 5: [ss_reg, ss_resid] - regression sum of squares and residual sum of squares
7188
7189 let mean_ln_y = ln_y_vals.iter().sum::<f64>() / n;
7190
7191 // Calculate residuals and sums of squares in log space
7192 let mut ss_resid = 0.0;
7193 let mut ss_tot = 0.0;
7194
7195 for i in 0..x_vals.len() {
7196 let ln_y_pred = ln_m * x_vals[i] + ln_b;
7197 let residual = ln_y_vals[i] - ln_y_pred;
7198 ss_resid += residual * residual;
7199 let dy_tot = ln_y_vals[i] - mean_ln_y;
7200 ss_tot += dy_tot * dy_tot;
7201 }
7202
7203 let ss_reg = ss_tot - ss_resid;
7204
7205 // R-squared (same in both spaces for transformed regression)
7206 let r_squared = if ss_tot == 0.0 {
7207 1.0
7208 } else {
7209 1.0 - (ss_resid / ss_tot)
7210 };
7211
7212 // Degrees of freedom
7213 let df = if use_const {
7214 (n as i64 - 2).max(1) as f64
7215 } else {
7216 (n as i64 - 1).max(1) as f64
7217 };
7218
7219 // Standard error of y estimate (in log space)
7220 let se_ln_y = if df > 0.0 {
7221 (ss_resid / df).sqrt()
7222 } else {
7223 0.0
7224 };
7225
7226 // Standard errors of coefficients in log space
7227 let mean_x = x_vals.iter().sum::<f64>() / n;
7228 let mut sum_x2_centered = 0.0;
7229 let mut sum_x2_raw = 0.0;
7230 for &xi in &x_vals {
7231 sum_x2_centered += (xi - mean_x).powi(2);
7232 sum_x2_raw += xi * xi;
7233 }
7234
7235 let se_ln_m = if sum_x2_centered > 0.0 && df > 0.0 {
7236 se_ln_y / sum_x2_centered.sqrt()
7237 } else {
7238 f64::NAN
7239 };
7240
7241 let se_ln_b = if use_const && sum_x2_centered > 0.0 && df > 0.0 {
7242 se_ln_y * (sum_x2_raw / (n * sum_x2_centered)).sqrt()
7243 } else {
7244 f64::NAN
7245 };
7246
7247 // Convert standard errors: se_m = m * se_ln_m (delta method approximation)
7248 let se_m = m * se_ln_m;
7249 let se_b = b * se_ln_b;
7250
7251 // Standard error of y estimate - convert from log space
7252 // This is an approximation; for exponential models, se_y in original space varies with x
7253 let se_y = se_ln_y;
7254
7255 // F-statistic
7256 let f_stat = if ss_resid > 0.0 && df > 0.0 {
7257 (ss_reg / 1.0) / (ss_resid / df)
7258 } else if ss_resid == 0.0 {
7259 f64::INFINITY
7260 } else {
7261 f64::NAN
7262 };
7263
7264 // Build 5x2 result array
7265 let rows = vec![
7266 vec![LiteralValue::Number(m), LiteralValue::Number(b)],
7267 vec![LiteralValue::Number(se_m), LiteralValue::Number(se_b)],
7268 vec![LiteralValue::Number(r_squared), LiteralValue::Number(se_y)],
7269 vec![LiteralValue::Number(f_stat), LiteralValue::Number(df)],
7270 vec![LiteralValue::Number(ss_reg), LiteralValue::Number(ss_resid)],
7271 ];
7272
7273 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Array(rows)))
7274 }
7275}
7276
7277/* ─────────────────────────── PERCENTRANK ──────────────────────────── */
7278
7279/// Returns the inclusive percentile rank of `x` within a numeric data array.
7280///
7281/// `PERCENTRANK.INC` maps values to `[0, 1]` and interpolates linearly between data points.
7282///
7283/// # Remarks
7284/// - `x` must be within the observed min/max range; otherwise returns `#N/A`.
7285/// - Optional `significance` controls decimal truncation and defaults to `3`.
7286/// - `significance` must be at least `1`.
7287/// - Returns `#NUM!` for invalid setup such as empty numeric input.
7288///
7289/// # Examples
7290///
7291/// ```yaml,sandbox
7292/// title: "Exact inclusive percentile rank"
7293/// formula: "=PERCENTRANK.INC({1,2,3,4,5},3)"
7294/// expected: 0.5
7295/// ```
7296///
7297/// ```yaml,sandbox
7298/// title: "Interpolated inclusive percentile rank"
7299/// formula: "=PERCENTRANK.INC({1,2,3,4,5},2.5)"
7300/// expected: 0.375
7301/// ```
7302#[derive(Debug)]
7303pub struct PercentRankIncFn;
7304/// [formualizer-docgen:schema:start]
7305/// Name: PERCENTRANK.INC
7306/// Type: PercentRankIncFn
7307/// Min args: 2
7308/// Max args: variadic
7309/// Variadic: true
7310/// Signature: PERCENTRANK.INC(arg1...: number@range)
7311/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
7312/// Caps: PURE, NUMERIC_ONLY
7313/// [formualizer-docgen:schema:end]
7314impl Function for PercentRankIncFn {
7315 func_caps!(PURE, NUMERIC_ONLY);
7316 fn name(&self) -> &'static str {
7317 "PERCENTRANK.INC"
7318 }
7319 fn aliases(&self) -> &'static [&'static str] {
7320 &["PERCENTRANK"]
7321 }
7322 fn min_args(&self) -> usize {
7323 2
7324 }
7325 fn variadic(&self) -> bool {
7326 true
7327 }
7328 fn arg_schema(&self) -> &'static [ArgSchema] {
7329 &ARG_RANGE_NUM_LENIENT_ONE[..]
7330 }
7331 fn eval<'a, 'b, 'c>(
7332 &self,
7333 args: &'c [ArgumentHandle<'a, 'b>],
7334 _ctx: &dyn FunctionContext<'b>,
7335 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
7336 if args.len() < 2 {
7337 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7338 ExcelError::new_num(),
7339 )));
7340 }
7341
7342 // Get x value (the value to find the rank of)
7343 let x = match coerce_num(&scalar_like_value(&args[1])?) {
7344 Ok(n) => n,
7345 Err(_) => {
7346 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7347 ExcelError::new_num(),
7348 )));
7349 }
7350 };
7351
7352 // Get optional significance (default 3)
7353 let significance = if args.len() > 2 {
7354 match coerce_num(&scalar_like_value(&args[2])?) {
7355 Ok(n) => {
7356 let s = n as i32;
7357 if s < 1 {
7358 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7359 ExcelError::new_num(),
7360 )));
7361 }
7362 s as u32
7363 }
7364 Err(_) => 3,
7365 }
7366 } else {
7367 3
7368 };
7369
7370 // Collect and sort the data array
7371 let mut nums = collect_numeric_stats(&args[0..1])?;
7372 if nums.is_empty() {
7373 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7374 ExcelError::new_num(),
7375 )));
7376 }
7377 nums.sort_by(|a, b| a.partial_cmp(b).unwrap());
7378
7379 let n = nums.len();
7380
7381 // Check if x is outside the range
7382 if x < nums[0] || x > nums[n - 1] {
7383 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7384 ExcelError::new_na(),
7385 )));
7386 }
7387
7388 // Find the rank using linear interpolation
7389 // For PERCENTRANK.INC, the formula is: rank = (position) / (n-1)
7390 // where position is 0-based and uses linear interpolation
7391 let rank = if n == 1 {
7392 // Single element - rank is 0 (or 1.0 if we want, but Excel returns 0)
7393 0.0
7394 } else {
7395 let mut rank_val = 0.0;
7396 for i in 0..n - 1 {
7397 if (nums[i] - x).abs() < 1e-12 {
7398 // Exact match at position i
7399 rank_val = (i as f64) / ((n - 1) as f64);
7400 break;
7401 } else if nums[i] < x && x < nums[i + 1] {
7402 // Interpolate between positions i and i+1
7403 let frac = (x - nums[i]) / (nums[i + 1] - nums[i]);
7404 rank_val = ((i as f64) + frac) / ((n - 1) as f64);
7405 break;
7406 } else if i == n - 2 && (nums[n - 1] - x).abs() < 1e-12 {
7407 // Exact match at last position
7408 rank_val = 1.0;
7409 }
7410 }
7411 rank_val
7412 };
7413
7414 // Truncate to significance decimal places
7415 let multiplier = 10_f64.powi(significance as i32);
7416 let truncated = (rank * multiplier).trunc() / multiplier;
7417
7418 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
7419 truncated,
7420 )))
7421 }
7422}
7423
7424/// Returns the exclusive percentile rank of `x` within a numeric data array.
7425///
7426/// `PERCENTRANK.EXC` uses an open ranking scale that excludes exact `0` and `1` endpoints.
7427///
7428/// # Remarks
7429/// - `x` must lie within the observed min/max range; otherwise returns `#N/A`.
7430/// - Output is based on position divided by `n + 1`, with interpolation between points.
7431/// - Optional `significance` defaults to `3` and must be at least `1`.
7432/// - Returns `#NUM!` for invalid setup such as empty numeric input.
7433///
7434/// # Examples
7435///
7436/// ```yaml,sandbox
7437/// title: "Exact exclusive percentile rank"
7438/// formula: "=PERCENTRANK.EXC({1,2,3,4,5},3)"
7439/// expected: 0.5
7440/// ```
7441///
7442/// ```yaml,sandbox
7443/// title: "Interpolated exclusive percentile rank"
7444/// formula: "=PERCENTRANK.EXC({1,2,3,4,5},2.5)"
7445/// expected: 0.416
7446/// ```
7447#[derive(Debug)]
7448pub struct PercentRankExcFn;
7449/// [formualizer-docgen:schema:start]
7450/// Name: PERCENTRANK.EXC
7451/// Type: PercentRankExcFn
7452/// Min args: 2
7453/// Max args: variadic
7454/// Variadic: true
7455/// Signature: PERCENTRANK.EXC(arg1...: number@range)
7456/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
7457/// Caps: PURE, NUMERIC_ONLY
7458/// [formualizer-docgen:schema:end]
7459impl Function for PercentRankExcFn {
7460 func_caps!(PURE, NUMERIC_ONLY);
7461 fn name(&self) -> &'static str {
7462 "PERCENTRANK.EXC"
7463 }
7464 fn min_args(&self) -> usize {
7465 2
7466 }
7467 fn variadic(&self) -> bool {
7468 true
7469 }
7470 fn arg_schema(&self) -> &'static [ArgSchema] {
7471 &ARG_RANGE_NUM_LENIENT_ONE[..]
7472 }
7473 fn eval<'a, 'b, 'c>(
7474 &self,
7475 args: &'c [ArgumentHandle<'a, 'b>],
7476 _ctx: &dyn FunctionContext<'b>,
7477 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
7478 if args.len() < 2 {
7479 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7480 ExcelError::new_num(),
7481 )));
7482 }
7483
7484 // Get x value (the value to find the rank of)
7485 let x = match coerce_num(&scalar_like_value(&args[1])?) {
7486 Ok(n) => n,
7487 Err(_) => {
7488 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7489 ExcelError::new_num(),
7490 )));
7491 }
7492 };
7493
7494 // Get optional significance (default 3)
7495 let significance = if args.len() > 2 {
7496 match coerce_num(&scalar_like_value(&args[2])?) {
7497 Ok(n) => {
7498 let s = n as i32;
7499 if s < 1 {
7500 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7501 ExcelError::new_num(),
7502 )));
7503 }
7504 s as u32
7505 }
7506 Err(_) => 3,
7507 }
7508 } else {
7509 3
7510 };
7511
7512 // Collect and sort the data array
7513 let mut nums = collect_numeric_stats(&args[0..1])?;
7514 if nums.is_empty() {
7515 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7516 ExcelError::new_num(),
7517 )));
7518 }
7519 nums.sort_by(|a, b| a.partial_cmp(b).unwrap());
7520
7521 let n = nums.len();
7522
7523 // Check if x is outside the range
7524 if x < nums[0] || x > nums[n - 1] {
7525 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7526 ExcelError::new_na(),
7527 )));
7528 }
7529
7530 // For PERCENTRANK.EXC, the formula is: rank = position / (n+1)
7531 // where position is 1-based and uses linear interpolation
7532 let rank = {
7533 let mut rank_val = 0.0;
7534 for i in 0..n {
7535 if (nums[i] - x).abs() < 1e-12 {
7536 // Exact match at position i (1-based: i+1)
7537 rank_val = ((i + 1) as f64) / ((n + 1) as f64);
7538 break;
7539 } else if i < n - 1 && nums[i] < x && x < nums[i + 1] {
7540 // Interpolate between positions i and i+1 (1-based: i+1 and i+2)
7541 let frac = (x - nums[i]) / (nums[i + 1] - nums[i]);
7542 let position = ((i + 1) as f64) + frac;
7543 rank_val = position / ((n + 1) as f64);
7544 break;
7545 }
7546 }
7547 rank_val
7548 };
7549
7550 // Truncate to significance decimal places
7551 let multiplier = 10_f64.powi(significance as i32);
7552 let truncated = (rank * multiplier).trunc() / multiplier;
7553
7554 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
7555 truncated,
7556 )))
7557 }
7558}
7559
7560/* ─────────────────────────── FREQUENCY ──────────────────────────── */
7561
7562/// Returns a vertical frequency distribution for numeric data across bin cutoffs.
7563///
7564/// `FREQUENCY` counts values into `<= first bin`, intermediate right-closed bins, and an overflow
7565/// bucket above the final bin.
7566///
7567/// # Remarks
7568/// - Returns an array with `bins + 1` rows.
7569/// - Bins are sorted before counting.
7570/// - If `bins_array` has no numeric values, result is a single count of all data points.
7571/// - Non-numeric values in input ranges are ignored by statistical-collection rules.
7572///
7573/// # Examples
7574///
7575/// ```yaml,sandbox
7576/// title: "Frequency buckets with two bins"
7577/// formula: "=FREQUENCY({1,2,3,4,5},{2,4})"
7578/// expected:
7579/// - [2]
7580/// - [2]
7581/// - [1]
7582/// ```
7583///
7584/// ```yaml,sandbox
7585/// title: "Frequency with repeated values"
7586/// formula: "=FREQUENCY({1,1,2,2,3},{1,2})"
7587/// expected:
7588/// - [2]
7589/// - [2]
7590/// - [1]
7591/// ```
7592#[derive(Debug)]
7593pub struct FrequencyFn;
7594/// [formualizer-docgen:schema:start]
7595/// Name: FREQUENCY
7596/// Type: FrequencyFn
7597/// Min args: 2
7598/// Max args: 1
7599/// Variadic: false
7600/// Signature: FREQUENCY(arg1: number@range)
7601/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
7602/// Caps: PURE, NUMERIC_ONLY
7603/// [formualizer-docgen:schema:end]
7604impl Function for FrequencyFn {
7605 func_caps!(PURE, NUMERIC_ONLY);
7606 fn name(&self) -> &'static str {
7607 "FREQUENCY"
7608 }
7609 fn min_args(&self) -> usize {
7610 2
7611 }
7612 fn variadic(&self) -> bool {
7613 false
7614 }
7615 fn arg_schema(&self) -> &'static [ArgSchema] {
7616 &ARG_RANGE_NUM_LENIENT_ONE[..]
7617 }
7618 fn eval<'a, 'b, 'c>(
7619 &self,
7620 args: &'c [ArgumentHandle<'a, 'b>],
7621 _ctx: &dyn FunctionContext<'b>,
7622 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
7623 if args.len() < 2 {
7624 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7625 ExcelError::new_num(),
7626 )));
7627 }
7628
7629 // Collect data array
7630 let data = collect_numeric_stats(&args[0..1])?;
7631
7632 // Collect bins array
7633 let mut bins = collect_numeric_stats(&args[1..2])?;
7634
7635 // Handle empty bins - return single count of all data
7636 if bins.is_empty() {
7637 let rows = vec![vec![LiteralValue::Number(data.len() as f64)]];
7638 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Array(rows)));
7639 }
7640
7641 // Sort bins
7642 bins.sort_by(|a, b| a.partial_cmp(b).unwrap());
7643
7644 // Calculate frequencies
7645 // Result has bins.len() + 1 elements
7646 let mut frequencies = vec![0usize; bins.len() + 1];
7647
7648 for &value in &data {
7649 // Find which bin the value belongs to
7650 let mut found = false;
7651 for (i, &bin) in bins.iter().enumerate() {
7652 if i == 0 {
7653 // First bin: count values <= bins[0]
7654 if value <= bin {
7655 frequencies[0] += 1;
7656 found = true;
7657 break;
7658 }
7659 } else {
7660 // Intermediate bins: count values > bins[i-1] AND <= bins[i]
7661 if value <= bin {
7662 frequencies[i] += 1;
7663 found = true;
7664 break;
7665 }
7666 }
7667 }
7668 // Last bin: values > bins[last]
7669 if !found {
7670 frequencies[bins.len()] += 1;
7671 }
7672 }
7673
7674 // Return as vertical array (column vector)
7675 let rows: Vec<Vec<LiteralValue>> = frequencies
7676 .into_iter()
7677 .map(|f| vec![LiteralValue::Number(f as f64)])
7678 .collect();
7679
7680 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Array(rows)))
7681 }
7682}
7683
7684/* ─────────────────────────── T.DIST.2T ──────────────────────────── */
7685
7686/// Returns the two-tailed Student's t probability beyond `x`.
7687///
7688/// `T.DIST.2T` computes `P(|T| > x)` for the specified degrees of freedom.
7689///
7690/// # Remarks
7691/// - Requires `x >= 0` and `deg_freedom >= 1`.
7692/// - Represents a two-sided tail area.
7693/// - Returns `#NUM!` when arguments are outside valid ranges.
7694/// - Invalid numeric coercions propagate as spreadsheet errors.
7695///
7696/// # Examples
7697///
7698/// ```yaml,sandbox
7699/// title: "Two-tailed t probability at zero"
7700/// formula: "=T.DIST.2T(0,10)"
7701/// expected: 1
7702/// ```
7703///
7704/// ```yaml,sandbox
7705/// title: "Two-tailed t probability at x=2"
7706/// formula: "=T.DIST.2T(2,10)"
7707/// expected: 0.0733880342639167
7708/// ```
7709#[derive(Debug)]
7710pub struct TDist2TFn;
7711/// [formualizer-docgen:schema:start]
7712/// Name: T.DIST.2T
7713/// Type: TDist2TFn
7714/// Min args: 2
7715/// Max args: 2
7716/// Variadic: false
7717/// Signature: T.DIST.2T(arg1: number@scalar, arg2: number@scalar)
7718/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
7719/// Caps: PURE
7720/// [formualizer-docgen:schema:end]
7721impl Function for TDist2TFn {
7722 func_caps!(PURE);
7723 fn name(&self) -> &'static str {
7724 "T.DIST.2T"
7725 }
7726 fn min_args(&self) -> usize {
7727 2
7728 }
7729 fn arg_schema(&self) -> &'static [ArgSchema] {
7730 use std::sync::LazyLock;
7731 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
7732 vec![
7733 ArgSchema::number_lenient_scalar(),
7734 ArgSchema::number_lenient_scalar(),
7735 ]
7736 });
7737 &SCHEMA[..]
7738 }
7739 fn eval<'a, 'b, 'c>(
7740 &self,
7741 args: &'c [ArgumentHandle<'a, 'b>],
7742 _ctx: &dyn FunctionContext<'b>,
7743 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
7744 let x = coerce_num(&scalar_like_value(&args[0])?)?;
7745 let df = coerce_num(&scalar_like_value(&args[1])?)?;
7746
7747 // x must be non-negative for T.DIST.2T, df must be >= 1
7748 if x < 0.0 || df < 1.0 {
7749 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7750 ExcelError::new_num(),
7751 )));
7752 }
7753
7754 // Two-tailed: P(|T| > x) = 2 * (1 - t_cdf(x, df))
7755 let p = 2.0 * (1.0 - t_cdf(x, df));
7756 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(p)))
7757 }
7758}
7759
7760/* ─────────────────────────── T.INV.2T ──────────────────────────── */
7761
7762/// Returns the positive t critical value for a two-tailed probability.
7763///
7764/// `T.INV.2T` solves for `t` such that `P(|T| > t) = probability`.
7765///
7766/// # Remarks
7767/// - `probability` must satisfy `0 < probability <= 1`.
7768/// - `deg_freedom` must be at least `1`.
7769/// - Returns `#NUM!` for invalid probability or degree-of-freedom arguments.
7770/// - Alias `TINV` is supported.
7771///
7772/// # Examples
7773///
7774/// ```yaml,sandbox
7775/// title: "Maximum two-tailed probability"
7776/// formula: "=T.INV.2T(1,10)"
7777/// expected: 0
7778/// ```
7779///
7780/// ```yaml,sandbox
7781/// title: "95% two-sided critical value"
7782/// formula: "=T.INV.2T(0.05,10)"
7783/// expected: 2.228138851986273
7784/// ```
7785#[derive(Debug)]
7786pub struct TInv2TFn;
7787/// [formualizer-docgen:schema:start]
7788/// Name: T.INV.2T
7789/// Type: TInv2TFn
7790/// Min args: 2
7791/// Max args: 2
7792/// Variadic: false
7793/// Signature: T.INV.2T(arg1: number@scalar, arg2: number@scalar)
7794/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
7795/// Caps: PURE
7796/// [formualizer-docgen:schema:end]
7797impl Function for TInv2TFn {
7798 func_caps!(PURE);
7799 fn name(&self) -> &'static str {
7800 "T.INV.2T"
7801 }
7802 fn aliases(&self) -> &'static [&'static str] {
7803 &["TINV"]
7804 }
7805 fn min_args(&self) -> usize {
7806 2
7807 }
7808 fn arg_schema(&self) -> &'static [ArgSchema] {
7809 use std::sync::LazyLock;
7810 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
7811 vec![
7812 ArgSchema::number_lenient_scalar(),
7813 ArgSchema::number_lenient_scalar(),
7814 ]
7815 });
7816 &SCHEMA[..]
7817 }
7818 fn eval<'a, 'b, 'c>(
7819 &self,
7820 args: &'c [ArgumentHandle<'a, 'b>],
7821 _ctx: &dyn FunctionContext<'b>,
7822 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
7823 let p = coerce_num(&scalar_like_value(&args[0])?)?;
7824 let df = coerce_num(&scalar_like_value(&args[1])?)?;
7825
7826 // probability must be in (0, 1], df >= 1
7827 if p <= 0.0 || p > 1.0 || df < 1.0 {
7828 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7829 ExcelError::new_num(),
7830 )));
7831 }
7832
7833 // For two-tailed: we want t such that P(|T| > t) = p
7834 // P(|T| > t) = 2 * (1 - F(t)) where F is CDF
7835 // So 1 - F(t) = p/2, meaning F(t) = 1 - p/2
7836 // Thus t = t_inv(1 - p/2, df)
7837 match t_inv(1.0 - p / 2.0, df) {
7838 Some(result) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
7839 result,
7840 ))),
7841 None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7842 ExcelError::new_num(),
7843 ))),
7844 }
7845 }
7846}
7847
7848/* ─────────────────────────── T.TEST ──────────────────────────── */
7849
7850/// Returns the p-value from a Student t-test comparing two numeric samples.
7851///
7852/// `T.TEST` supports paired, equal-variance two-sample, and unequal-variance (Welch) modes.
7853///
7854/// # Remarks
7855/// - `tails` must be `1` (one-tailed) or `2` (two-tailed).
7856/// - `type` must be `1` (paired), `2` (two-sample equal variance), or `3` (Welch).
7857/// - Returns `#N/A` when paired mode arrays have different lengths.
7858/// - Returns `#NUM!` or `#DIV/0!` for invalid setup or degenerate variance conditions.
7859///
7860/// # Examples
7861///
7862/// ```yaml,sandbox
7863/// title: "Two-tailed equal-variance test with identical samples"
7864/// formula: "=T.TEST({1,2,3},{1,2,3},2,2)"
7865/// expected: 1
7866/// ```
7867///
7868/// ```yaml,sandbox
7869/// title: "One-tailed Welch test with identical samples"
7870/// formula: "=T.TEST({1,2,3},{1,2,3},1,3)"
7871/// expected: 0.5
7872/// ```
7873#[derive(Debug)]
7874pub struct TTestFn;
7875/// [formualizer-docgen:schema:start]
7876/// Name: T.TEST
7877/// Type: TTestFn
7878/// Min args: 4
7879/// Max args: 4
7880/// Variadic: false
7881/// Signature: T.TEST(arg1: number@range, arg2: number@range, arg3: number@scalar, arg4: number@scalar)
7882/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg4{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
7883/// Caps: PURE
7884/// [formualizer-docgen:schema:end]
7885impl Function for TTestFn {
7886 func_caps!(PURE);
7887 fn name(&self) -> &'static str {
7888 "T.TEST"
7889 }
7890 fn aliases(&self) -> &'static [&'static str] {
7891 &["TTEST"]
7892 }
7893 fn min_args(&self) -> usize {
7894 4
7895 }
7896 fn arg_schema(&self) -> &'static [ArgSchema] {
7897 use std::sync::LazyLock;
7898 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
7899 vec![
7900 {
7901 let mut s = ArgSchema::number_lenient_scalar();
7902 s.shape = crate::args::ShapeKind::Range;
7903 s.coercion = formualizer_common::CoercionPolicy::NumberLenientText;
7904 s
7905 },
7906 {
7907 let mut s = ArgSchema::number_lenient_scalar();
7908 s.shape = crate::args::ShapeKind::Range;
7909 s.coercion = formualizer_common::CoercionPolicy::NumberLenientText;
7910 s
7911 },
7912 ArgSchema::number_lenient_scalar(), // tails
7913 ArgSchema::number_lenient_scalar(), // type
7914 ]
7915 });
7916 &SCHEMA[..]
7917 }
7918 fn eval<'a, 'b, 'c>(
7919 &self,
7920 args: &'c [ArgumentHandle<'a, 'b>],
7921 _ctx: &dyn FunctionContext<'b>,
7922 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
7923 let array1 = collect_numeric_stats(&args[0..1])?;
7924 let array2 = collect_numeric_stats(&args[1..2])?;
7925 let tails = coerce_num(&scalar_like_value(&args[2])?)? as i32;
7926 let test_type = coerce_num(&scalar_like_value(&args[3])?)? as i32;
7927
7928 // Validate tails (1 or 2) and type (1, 2, or 3)
7929 if !(1..=2).contains(&tails) || !(1..=3).contains(&test_type) {
7930 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7931 ExcelError::new_num(),
7932 )));
7933 }
7934
7935 let n1 = array1.len();
7936 let n2 = array2.len();
7937
7938 // For paired test, arrays must have same length
7939 if test_type == 1 && n1 != n2 {
7940 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7941 ExcelError::new_na(),
7942 )));
7943 }
7944
7945 // Need at least 2 data points for meaningful t-test
7946 if n1 < 2 || n2 < 2 {
7947 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7948 ExcelError::new_num(),
7949 )));
7950 }
7951
7952 let (t_stat, df) = match test_type {
7953 1 => {
7954 // Paired t-test
7955 let n = n1 as f64;
7956 let diffs: Vec<f64> = array1
7957 .iter()
7958 .zip(array2.iter())
7959 .map(|(a, b)| a - b)
7960 .collect();
7961 let mean_diff = diffs.iter().sum::<f64>() / n;
7962 let var_diff =
7963 diffs.iter().map(|d| (d - mean_diff).powi(2)).sum::<f64>() / (n - 1.0);
7964 if var_diff == 0.0 {
7965 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7966 ExcelError::new_div(),
7967 )));
7968 }
7969 let se = (var_diff / n).sqrt();
7970 (mean_diff / se, n - 1.0)
7971 }
7972 2 => {
7973 // Two-sample equal variance (pooled)
7974 let n1f = n1 as f64;
7975 let n2f = n2 as f64;
7976 let mean1 = array1.iter().sum::<f64>() / n1f;
7977 let mean2 = array2.iter().sum::<f64>() / n2f;
7978 let var1 = array1.iter().map(|x| (x - mean1).powi(2)).sum::<f64>() / (n1f - 1.0);
7979 let var2 = array2.iter().map(|x| (x - mean2).powi(2)).sum::<f64>() / (n2f - 1.0);
7980
7981 // Pooled variance
7982 let sp2 = ((n1f - 1.0) * var1 + (n2f - 1.0) * var2) / (n1f + n2f - 2.0);
7983 if sp2 == 0.0 {
7984 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
7985 ExcelError::new_div(),
7986 )));
7987 }
7988 let se = (sp2 * (1.0 / n1f + 1.0 / n2f)).sqrt();
7989 ((mean1 - mean2) / se, n1f + n2f - 2.0)
7990 }
7991 3 => {
7992 // Welch's t-test (unequal variance)
7993 let n1f = n1 as f64;
7994 let n2f = n2 as f64;
7995 let mean1 = array1.iter().sum::<f64>() / n1f;
7996 let mean2 = array2.iter().sum::<f64>() / n2f;
7997 let var1 = array1.iter().map(|x| (x - mean1).powi(2)).sum::<f64>() / (n1f - 1.0);
7998 let var2 = array2.iter().map(|x| (x - mean2).powi(2)).sum::<f64>() / (n2f - 1.0);
7999
8000 let s1_n = var1 / n1f;
8001 let s2_n = var2 / n2f;
8002 let se = (s1_n + s2_n).sqrt();
8003 if se == 0.0 {
8004 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8005 ExcelError::new_div(),
8006 )));
8007 }
8008
8009 // Welch-Satterthwaite degrees of freedom
8010 let df_num = (s1_n + s2_n).powi(2);
8011 let df_denom = s1_n.powi(2) / (n1f - 1.0) + s2_n.powi(2) / (n2f - 1.0);
8012 let df = if df_denom == 0.0 {
8013 1.0
8014 } else {
8015 df_num / df_denom
8016 };
8017 ((mean1 - mean2) / se, df)
8018 }
8019 _ => unreachable!(),
8020 };
8021
8022 // Calculate p-value based on tails
8023 let t_abs = t_stat.abs();
8024 let p = if tails == 1 {
8025 1.0 - t_cdf(t_abs, df)
8026 } else {
8027 2.0 * (1.0 - t_cdf(t_abs, df))
8028 };
8029
8030 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(p)))
8031 }
8032}
8033
8034/* ─────────────────────────── F.TEST ──────────────────────────── */
8035
8036/// Returns the two-tailed p-value from an F-test comparing sample variances.
8037///
8038/// `F.TEST` evaluates whether two samples have significantly different variances.
8039///
8040/// # Remarks
8041/// - Each array must contain at least two numeric values.
8042/// - Uses sample variances and computes a two-tailed probability.
8043/// - Returns `#DIV/0!` when either sample variance is zero.
8044/// - Alias `FTEST` is supported.
8045///
8046/// # Examples
8047///
8048/// ```yaml,sandbox
8049/// title: "Identical samples yield p-value 1"
8050/// formula: "=F.TEST({1,2,3,4},{1,2,3,4})"
8051/// expected: 1
8052/// ```
8053///
8054/// ```yaml,sandbox
8055/// title: "Different variances example"
8056/// formula: "=F.TEST({1,2,3,4},{1,1,1,5})"
8057/// expected: 0.5466810975407987
8058/// ```
8059#[derive(Debug)]
8060pub struct FTestFn;
8061/// [formualizer-docgen:schema:start]
8062/// Name: F.TEST
8063/// Type: FTestFn
8064/// Min args: 2
8065/// Max args: 2
8066/// Variadic: false
8067/// Signature: F.TEST(arg1: number@range, arg2: number@range)
8068/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
8069/// Caps: PURE
8070/// [formualizer-docgen:schema:end]
8071impl Function for FTestFn {
8072 func_caps!(PURE);
8073 fn name(&self) -> &'static str {
8074 "F.TEST"
8075 }
8076 fn aliases(&self) -> &'static [&'static str] {
8077 &["FTEST"]
8078 }
8079 fn min_args(&self) -> usize {
8080 2
8081 }
8082 fn arg_schema(&self) -> &'static [ArgSchema] {
8083 use std::sync::LazyLock;
8084 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
8085 vec![
8086 {
8087 let mut s = ArgSchema::number_lenient_scalar();
8088 s.shape = crate::args::ShapeKind::Range;
8089 s.coercion = formualizer_common::CoercionPolicy::NumberLenientText;
8090 s
8091 },
8092 {
8093 let mut s = ArgSchema::number_lenient_scalar();
8094 s.shape = crate::args::ShapeKind::Range;
8095 s.coercion = formualizer_common::CoercionPolicy::NumberLenientText;
8096 s
8097 },
8098 ]
8099 });
8100 &SCHEMA[..]
8101 }
8102 fn eval<'a, 'b, 'c>(
8103 &self,
8104 args: &'c [ArgumentHandle<'a, 'b>],
8105 _ctx: &dyn FunctionContext<'b>,
8106 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
8107 let array1 = collect_numeric_stats(&args[0..1])?;
8108 let array2 = collect_numeric_stats(&args[1..2])?;
8109
8110 let n1 = array1.len();
8111 let n2 = array2.len();
8112
8113 // Need at least 2 points in each array
8114 if n1 < 2 || n2 < 2 {
8115 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8116 ExcelError::new_div(),
8117 )));
8118 }
8119
8120 let n1f = n1 as f64;
8121 let n2f = n2 as f64;
8122
8123 let mean1 = array1.iter().sum::<f64>() / n1f;
8124 let mean2 = array2.iter().sum::<f64>() / n2f;
8125
8126 let var1 = array1.iter().map(|x| (x - mean1).powi(2)).sum::<f64>() / (n1f - 1.0);
8127 let var2 = array2.iter().map(|x| (x - mean2).powi(2)).sum::<f64>() / (n2f - 1.0);
8128
8129 // Handle zero variance
8130 if var1 == 0.0 || var2 == 0.0 {
8131 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8132 ExcelError::new_div(),
8133 )));
8134 }
8135
8136 // F-statistic: Excel's F.TEST uses var1/var2 (order matters for degrees of freedom)
8137 // and returns two-tailed p-value
8138 let f = var1 / var2;
8139 let df1 = n1f - 1.0;
8140 let df2 = n2f - 1.0;
8141
8142 // Two-tailed p-value: min(F.DIST(f), 1-F.DIST(f)) * 2
8143 let p_lower = f_cdf(f, df1, df2);
8144 let p_upper = 1.0 - p_lower;
8145 let p = 2.0 * p_lower.min(p_upper);
8146
8147 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(p)))
8148 }
8149}
8150
8151/* ─────────────────────────── CHISQ.TEST ──────────────────────────── */
8152
8153/// Returns the right-tail p-value from a chi-square goodness-of-fit style comparison.
8154///
8155/// `CHISQ.TEST` compares observed and expected values and computes `1 - CHISQ.DIST(...)`.
8156///
8157/// # Remarks
8158/// - `actual_range` and `expected_range` must contain the same number of numeric points.
8159/// - Expected values must be strictly greater than `0`.
8160/// - Requires at least two categories (`df >= 1`).
8161/// - Returns `#N/A` for length mismatches or empty inputs, and `#NUM!` for invalid expected values.
8162///
8163/// # Examples
8164///
8165/// ```yaml,sandbox
8166/// title: "Perfect match between observed and expected"
8167/// formula: "=CHISQ.TEST({20,30,50},{20,30,50})"
8168/// expected: 1
8169/// ```
8170///
8171/// ```yaml,sandbox
8172/// title: "Two-category chi-square test"
8173/// formula: "=CHISQ.TEST({18,22},{20,20})"
8174/// expected: 0.5270892568655381
8175/// ```
8176#[derive(Debug)]
8177pub struct ChisqTestFn;
8178/// [formualizer-docgen:schema:start]
8179/// Name: CHISQ.TEST
8180/// Type: ChisqTestFn
8181/// Min args: 2
8182/// Max args: 2
8183/// Variadic: false
8184/// Signature: CHISQ.TEST(arg1: number@range, arg2: number@range)
8185/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
8186/// Caps: PURE
8187/// [formualizer-docgen:schema:end]
8188impl Function for ChisqTestFn {
8189 func_caps!(PURE);
8190 fn name(&self) -> &'static str {
8191 "CHISQ.TEST"
8192 }
8193 fn aliases(&self) -> &'static [&'static str] {
8194 &["CHITEST"]
8195 }
8196 fn min_args(&self) -> usize {
8197 2
8198 }
8199 fn arg_schema(&self) -> &'static [ArgSchema] {
8200 use std::sync::LazyLock;
8201 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
8202 vec![
8203 {
8204 let mut s = ArgSchema::number_lenient_scalar();
8205 s.shape = crate::args::ShapeKind::Range;
8206 s.coercion = formualizer_common::CoercionPolicy::NumberLenientText;
8207 s
8208 },
8209 {
8210 let mut s = ArgSchema::number_lenient_scalar();
8211 s.shape = crate::args::ShapeKind::Range;
8212 s.coercion = formualizer_common::CoercionPolicy::NumberLenientText;
8213 s
8214 },
8215 ]
8216 });
8217 &SCHEMA[..]
8218 }
8219 fn eval<'a, 'b, 'c>(
8220 &self,
8221 args: &'c [ArgumentHandle<'a, 'b>],
8222 _ctx: &dyn FunctionContext<'b>,
8223 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
8224 let actual = collect_numeric_stats(&args[0..1])?;
8225 let expected = collect_numeric_stats(&args[1..2])?;
8226
8227 // Arrays must have same length
8228 if actual.len() != expected.len() {
8229 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8230 ExcelError::new_na(),
8231 )));
8232 }
8233
8234 if actual.is_empty() {
8235 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8236 ExcelError::new_na(),
8237 )));
8238 }
8239
8240 // Calculate chi-squared statistic: sum((observed - expected)^2 / expected)
8241 let mut chi_sq = 0.0;
8242 for (obs, exp) in actual.iter().zip(expected.iter()) {
8243 if *exp <= 0.0 {
8244 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8245 ExcelError::new_num(),
8246 )));
8247 }
8248 chi_sq += (obs - exp).powi(2) / exp;
8249 }
8250
8251 // Degrees of freedom = number of categories - 1
8252 let df = (actual.len() - 1) as f64;
8253
8254 if df < 1.0 {
8255 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8256 ExcelError::new_num(),
8257 )));
8258 }
8259
8260 // P-value = 1 - CHISQ.DIST(chi_sq, df, TRUE) = right-tail probability
8261 let p = 1.0 - chisq_cdf(chi_sq, df);
8262
8263 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(p)))
8264 }
8265}
8266
8267/* ═══════════════════════════════════════════════════════════════════════════
8268 FZ-PAR-01: Statistical compatibility batch
8269 AVERAGEA, MAXA, MINA, STDEVA, STDEVPA, VARA, VARPA, SKEW.P,
8270 T.DIST.RT, CHISQ.DIST.RT, CHISQ.INV.RT, F.DIST.RT, F.INV.RT,
8271 BETA.INV, BINOM.DIST.RANGE, BINOM.INV, GAMMA, GAMMA.INV,
8272 GAMMALN, GAMMALN.PRECISE
8273═══════════════════════════════════════════════════════════════════════════ */
8274
8275/// Collect numeric inputs applying Excel "A"-variant semantics:
8276/// - Range references: include numbers as-is, booleans as 0/1, text as 0. Errors propagate.
8277/// Blank cells are skipped.
8278/// - Direct scalar arguments: same coercion as standard collect_numeric_stats.
8279fn collect_numeric_a(args: &[ArgumentHandle]) -> Result<Vec<f64>, ExcelError> {
8280 let mut out = Vec::new();
8281 for a in args {
8282 if let Some(arr) = a.inline_array_literal()? {
8283 for row in arr.into_iter() {
8284 for cell in row.into_iter() {
8285 match cell {
8286 LiteralValue::Error(e) => return Err(e),
8287 LiteralValue::Number(n) => out.push(n),
8288 LiteralValue::Int(i) => out.push(i as f64),
8289 LiteralValue::Boolean(b) => out.push(if b { 1.0 } else { 0.0 }),
8290 LiteralValue::Text(_) => out.push(0.0),
8291 _ => {}
8292 }
8293 }
8294 }
8295 continue;
8296 }
8297
8298 if let Ok(view) = a.range_view() {
8299 view.for_each_cell(&mut |v| {
8300 match v {
8301 LiteralValue::Error(e) => return Err(e.clone()),
8302 LiteralValue::Number(n) => out.push(*n),
8303 LiteralValue::Int(i) => out.push(*i as f64),
8304 LiteralValue::Boolean(b) => out.push(if *b { 1.0 } else { 0.0 }),
8305 LiteralValue::Text(_) => out.push(0.0),
8306 LiteralValue::Empty => {} // skip blanks
8307 _ => {}
8308 }
8309 Ok(())
8310 })?;
8311 } else {
8312 let v = scalar_like_value(a)?;
8313 match v {
8314 LiteralValue::Error(e) => return Err(e),
8315 other => {
8316 if let Ok(n) = coerce_num(&other) {
8317 out.push(n);
8318 }
8319 }
8320 }
8321 }
8322 }
8323 Ok(out)
8324}
8325
8326/// Helper: inverse of the regularized incomplete beta function.
8327/// Given p = I_x(a,b), find x. Uses Newton-Raphson with beta_i / beta PDF.
8328fn beta_inv_helper(p: f64, a: f64, b: f64) -> Option<f64> {
8329 if p <= 0.0 {
8330 return Some(0.0);
8331 }
8332 if p >= 1.0 {
8333 return Some(1.0);
8334 }
8335 if a <= 0.0 || b <= 0.0 {
8336 return None;
8337 }
8338
8339 // Initial guess from normal approximation (Abramowitz & Stegun 26.5.22)
8340 let mut x = 0.5f64;
8341
8342 // Newton-Raphson
8343 let ln_beta_ab = ln_gamma(a) + ln_gamma(b) - ln_gamma(a + b);
8344 for _ in 0..100 {
8345 let cdf = beta_i(x, a, b);
8346 // Beta PDF: x^(a-1) * (1-x)^(b-1) / B(a,b)
8347 let pdf = if x > 0.0 && x < 1.0 {
8348 ((a - 1.0) * x.ln() + (b - 1.0) * (1.0 - x).ln() - ln_beta_ab).exp()
8349 } else {
8350 1e-30
8351 };
8352 if pdf.abs() < 1e-30 {
8353 break;
8354 }
8355 let delta = (cdf - p) / pdf;
8356 let new_x = (x - delta).clamp(1e-15, 1.0 - 1e-15);
8357 if (new_x - x).abs() < 1e-14 {
8358 x = new_x;
8359 break;
8360 }
8361 x = new_x;
8362 }
8363
8364 Some(x)
8365}
8366
8367/// Helper: inverse of GAMMA.DIST CDF. Given p = P(alpha, x/beta), find x.
8368fn gamma_inv_helper(p: f64, alpha: f64, beta: f64) -> Option<f64> {
8369 if p <= 0.0 {
8370 return Some(0.0);
8371 }
8372 if p >= 1.0 {
8373 return None;
8374 }
8375 if alpha <= 0.0 || beta <= 0.0 {
8376 return None;
8377 }
8378
8379 // Initial guess
8380 let mut x = alpha * beta;
8381 if p < 0.5 {
8382 x = x.min(beta);
8383 }
8384
8385 // Newton-Raphson on the standardized gamma CDF (gamma_p)
8386 for _ in 0..100 {
8387 let z = x / beta;
8388 let cdf = gamma_p(alpha, z);
8389 // Gamma PDF: z^(alpha-1) * e^(-z) / Gamma(alpha) / beta
8390 let pdf = if z > 0.0 {
8391 ((alpha - 1.0) * z.ln() - z - ln_gamma(alpha)).exp() / beta
8392 } else {
8393 1e-30
8394 };
8395 if pdf.abs() < 1e-30 {
8396 break;
8397 }
8398 let delta = (cdf - p) / pdf;
8399 let new_x = (x - delta).max(1e-15);
8400 if (new_x - x).abs() < 1e-12 * x.max(1e-15) {
8401 x = new_x;
8402 break;
8403 }
8404 x = new_x;
8405 }
8406
8407 Some(x)
8408}
8409
8410/* ─────────────────────────── AVERAGEA ──────────────────────────── */
8411
8412/// Returns the arithmetic mean while treating logical values and text as numeric inputs.
8413///
8414/// # Formula example
8415/// ```excel
8416/// # returns: 1
8417/// =AVERAGEA(TRUE,2,"x")
8418/// ```
8419///
8420/// ```yaml,sandbox
8421/// title: "Average with logical and text coercion"
8422/// formula: '=AVERAGEA(TRUE,2,"x")'
8423/// expected: 1
8424/// ```
8425///
8426/// ```yaml,docs
8427/// related:
8428/// - AVERAGE
8429/// - MAXA
8430/// - MINA
8431/// faq:
8432/// - q: "How does AVERAGEA treat text and booleans?"
8433/// a: "TRUE counts as 1, FALSE and text count as 0, and blanks are ignored."
8434/// ```
8435#[derive(Debug)]
8436pub struct AverageAFn;
8437/// [formualizer-docgen:schema:start]
8438/// Name: AVERAGEA
8439/// Type: AverageAFn
8440/// Min args: 1
8441/// Max args: variadic
8442/// Variadic: true
8443/// Signature: AVERAGEA(arg1...: number@range)
8444/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
8445/// Caps: PURE, REDUCTION
8446/// [formualizer-docgen:schema:end]
8447impl Function for AverageAFn {
8448 func_caps!(PURE, REDUCTION);
8449 fn name(&self) -> &'static str {
8450 "AVERAGEA"
8451 }
8452 fn min_args(&self) -> usize {
8453 1
8454 }
8455 fn variadic(&self) -> bool {
8456 true
8457 }
8458 fn arg_schema(&self) -> &'static [ArgSchema] {
8459 &ARG_RANGE_NUM_LENIENT_ONE[..]
8460 }
8461 fn eval<'a, 'b, 'c>(
8462 &self,
8463 args: &'c [ArgumentHandle<'a, 'b>],
8464 _ctx: &dyn FunctionContext<'b>,
8465 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
8466 let nums = collect_numeric_a(args)?;
8467 if nums.is_empty() {
8468 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8469 ExcelError::new_div(),
8470 )));
8471 }
8472 let avg = nums.iter().sum::<f64>() / nums.len() as f64;
8473 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(avg)))
8474 }
8475}
8476
8477/* ─────────────────────────── MAXA ──────────────────────────── */
8478
8479/// Returns the largest value after applying A-variant coercion rules.
8480///
8481/// # Formula example
8482/// ```excel
8483/// # returns: 1
8484/// =MAXA(TRUE,-2,"x")
8485/// ```
8486///
8487/// ```yaml,sandbox
8488/// title: "Maximum with logical and text coercion"
8489/// formula: '=MAXA(TRUE,-2,"x")'
8490/// expected: 1
8491/// ```
8492///
8493/// ```yaml,docs
8494/// related:
8495/// - MAX
8496/// - MINA
8497/// - AVERAGEA
8498/// faq:
8499/// - q: "What do text values contribute to MAXA?"
8500/// a: "Text contributes 0, so negative numeric inputs can still be smaller than text in the aggregate."
8501/// ```
8502#[derive(Debug)]
8503pub struct MaxAFn;
8504/// [formualizer-docgen:schema:start]
8505/// Name: MAXA
8506/// Type: MaxAFn
8507/// Min args: 1
8508/// Max args: variadic
8509/// Variadic: true
8510/// Signature: MAXA(arg1...: number@range)
8511/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
8512/// Caps: PURE, REDUCTION
8513/// [formualizer-docgen:schema:end]
8514impl Function for MaxAFn {
8515 func_caps!(PURE, REDUCTION);
8516 fn name(&self) -> &'static str {
8517 "MAXA"
8518 }
8519 fn min_args(&self) -> usize {
8520 1
8521 }
8522 fn variadic(&self) -> bool {
8523 true
8524 }
8525 fn arg_schema(&self) -> &'static [ArgSchema] {
8526 &ARG_RANGE_NUM_LENIENT_ONE[..]
8527 }
8528 fn eval<'a, 'b, 'c>(
8529 &self,
8530 args: &'c [ArgumentHandle<'a, 'b>],
8531 _ctx: &dyn FunctionContext<'b>,
8532 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
8533 let nums = collect_numeric_a(args)?;
8534 if nums.is_empty() {
8535 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0)));
8536 }
8537 let mx = nums.iter().copied().fold(f64::NEG_INFINITY, f64::max);
8538 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(mx)))
8539 }
8540}
8541
8542/* ─────────────────────────── MINA ──────────────────────────── */
8543
8544/// Returns the smallest value after applying A-variant coercion rules.
8545///
8546/// # Formula example
8547/// ```excel
8548/// # returns: -2
8549/// =MINA(TRUE,-2,"x")
8550/// ```
8551///
8552/// ```yaml,sandbox
8553/// title: "Minimum with logical and text coercion"
8554/// formula: '=MINA(TRUE,-2,"x")'
8555/// expected: -2
8556/// ```
8557///
8558/// ```yaml,docs
8559/// related:
8560/// - MIN
8561/// - MAXA
8562/// - AVERAGEA
8563/// faq:
8564/// - q: "Do text values affect MINA?"
8565/// a: "Yes. Text is coerced to 0, so it can become the minimum when all numeric inputs are positive."
8566/// ```
8567#[derive(Debug)]
8568pub struct MinAFn;
8569/// [formualizer-docgen:schema:start]
8570/// Name: MINA
8571/// Type: MinAFn
8572/// Min args: 1
8573/// Max args: variadic
8574/// Variadic: true
8575/// Signature: MINA(arg1...: number@range)
8576/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
8577/// Caps: PURE, REDUCTION
8578/// [formualizer-docgen:schema:end]
8579impl Function for MinAFn {
8580 func_caps!(PURE, REDUCTION);
8581 fn name(&self) -> &'static str {
8582 "MINA"
8583 }
8584 fn min_args(&self) -> usize {
8585 1
8586 }
8587 fn variadic(&self) -> bool {
8588 true
8589 }
8590 fn arg_schema(&self) -> &'static [ArgSchema] {
8591 &ARG_RANGE_NUM_LENIENT_ONE[..]
8592 }
8593 fn eval<'a, 'b, 'c>(
8594 &self,
8595 args: &'c [ArgumentHandle<'a, 'b>],
8596 _ctx: &dyn FunctionContext<'b>,
8597 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
8598 let nums = collect_numeric_a(args)?;
8599 if nums.is_empty() {
8600 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0)));
8601 }
8602 let mn = nums.iter().copied().fold(f64::INFINITY, f64::min);
8603 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(mn)))
8604 }
8605}
8606
8607/* ─────────────────────────── STDEVA ──────────────────────────── */
8608
8609/// Returns the sample standard deviation using A-variant coercion semantics.
8610///
8611/// # Formula example
8612/// ```excel
8613/// # returns: 1
8614/// =STDEVA(TRUE,2,"x")
8615/// ```
8616///
8617/// ```yaml,sandbox
8618/// title: "Sample deviation with coerced values"
8619/// formula: '=STDEVA(TRUE,2,"x")'
8620/// expected: 1
8621/// ```
8622///
8623/// ```yaml,docs
8624/// related:
8625/// - STDEV.P
8626/// - STDEVPA
8627/// - VARA
8628/// faq:
8629/// - q: "When does STDEVA return #DIV/0!?"
8630/// a: "It returns #DIV/0! when fewer than two coerced values remain after evaluation."
8631/// ```
8632#[derive(Debug)]
8633pub struct StdevAFn;
8634/// [formualizer-docgen:schema:start]
8635/// Name: STDEVA
8636/// Type: StdevAFn
8637/// Min args: 1
8638/// Max args: variadic
8639/// Variadic: true
8640/// Signature: STDEVA(arg1...: number@range)
8641/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
8642/// Caps: PURE, REDUCTION
8643/// [formualizer-docgen:schema:end]
8644impl Function for StdevAFn {
8645 func_caps!(PURE, REDUCTION);
8646 fn name(&self) -> &'static str {
8647 "STDEVA"
8648 }
8649 fn min_args(&self) -> usize {
8650 1
8651 }
8652 fn variadic(&self) -> bool {
8653 true
8654 }
8655 fn arg_schema(&self) -> &'static [ArgSchema] {
8656 &ARG_RANGE_NUM_LENIENT_ONE[..]
8657 }
8658 fn eval<'a, 'b, 'c>(
8659 &self,
8660 args: &'c [ArgumentHandle<'a, 'b>],
8661 _ctx: &dyn FunctionContext<'b>,
8662 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
8663 let nums = collect_numeric_a(args)?;
8664 let n = nums.len();
8665 if n < 2 {
8666 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8667 ExcelError::new_div(),
8668 )));
8669 }
8670 let mean = nums.iter().sum::<f64>() / n as f64;
8671 let ss: f64 = nums.iter().map(|v| (v - mean).powi(2)).sum();
8672 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
8673 (ss / (n - 1) as f64).sqrt(),
8674 )))
8675 }
8676}
8677
8678/* ─────────────────────────── STDEVPA ──────────────────────────── */
8679
8680/// Returns the population standard deviation using A-variant coercion semantics.
8681///
8682/// # Formula example
8683/// ```excel
8684/// # returns: 0.816496580927726
8685/// =STDEVPA(TRUE,2,"x")
8686/// ```
8687///
8688/// ```yaml,sandbox
8689/// title: "Population deviation with coerced values"
8690/// formula: '=STDEVPA(TRUE,2,"x")'
8691/// expected: 0.816496580927726
8692/// ```
8693///
8694/// ```yaml,docs
8695/// related:
8696/// - STDEVA
8697/// - VARPA
8698/// - STDEV.P
8699/// faq:
8700/// - q: "What is the difference between STDEVA and STDEVPA?"
8701/// a: "STDEVA uses the sample denominator n-1, while STDEVPA uses the population denominator n."
8702/// ```
8703#[derive(Debug)]
8704pub struct StdevPAFn;
8705/// [formualizer-docgen:schema:start]
8706/// Name: STDEVPA
8707/// Type: StdevPAFn
8708/// Min args: 1
8709/// Max args: variadic
8710/// Variadic: true
8711/// Signature: STDEVPA(arg1...: number@range)
8712/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
8713/// Caps: PURE, REDUCTION
8714/// [formualizer-docgen:schema:end]
8715impl Function for StdevPAFn {
8716 func_caps!(PURE, REDUCTION);
8717 fn name(&self) -> &'static str {
8718 "STDEVPA"
8719 }
8720 fn min_args(&self) -> usize {
8721 1
8722 }
8723 fn variadic(&self) -> bool {
8724 true
8725 }
8726 fn arg_schema(&self) -> &'static [ArgSchema] {
8727 &ARG_RANGE_NUM_LENIENT_ONE[..]
8728 }
8729 fn eval<'a, 'b, 'c>(
8730 &self,
8731 args: &'c [ArgumentHandle<'a, 'b>],
8732 _ctx: &dyn FunctionContext<'b>,
8733 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
8734 let nums = collect_numeric_a(args)?;
8735 let n = nums.len();
8736 if n == 0 {
8737 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8738 ExcelError::new_div(),
8739 )));
8740 }
8741 let mean = nums.iter().sum::<f64>() / n as f64;
8742 let ss: f64 = nums.iter().map(|v| (v - mean).powi(2)).sum();
8743 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
8744 (ss / n as f64).sqrt(),
8745 )))
8746 }
8747}
8748
8749/* ─────────────────────────── VARA ──────────────────────────── */
8750
8751/// Returns the sample variance using A-variant coercion semantics.
8752///
8753/// # Formula example
8754/// ```excel
8755/// # returns: 1
8756/// =VARA(TRUE,2,"x")
8757/// ```
8758///
8759/// ```yaml,sandbox
8760/// title: "Sample variance with coerced values"
8761/// formula: '=VARA(TRUE,2,"x")'
8762/// expected: 1
8763/// ```
8764///
8765/// ```yaml,docs
8766/// related:
8767/// - VARPA
8768/// - STDEVA
8769/// - AVERAGEA
8770/// faq:
8771/// - q: "How are blanks handled in VARA?"
8772/// a: "Blanks are ignored, while booleans and text are coerced under A-variant rules."
8773/// ```
8774#[derive(Debug)]
8775pub struct VarAFn;
8776/// [formualizer-docgen:schema:start]
8777/// Name: VARA
8778/// Type: VarAFn
8779/// Min args: 1
8780/// Max args: variadic
8781/// Variadic: true
8782/// Signature: VARA(arg1...: number@range)
8783/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
8784/// Caps: PURE, REDUCTION
8785/// [formualizer-docgen:schema:end]
8786impl Function for VarAFn {
8787 func_caps!(PURE, REDUCTION);
8788 fn name(&self) -> &'static str {
8789 "VARA"
8790 }
8791 fn min_args(&self) -> usize {
8792 1
8793 }
8794 fn variadic(&self) -> bool {
8795 true
8796 }
8797 fn arg_schema(&self) -> &'static [ArgSchema] {
8798 &ARG_RANGE_NUM_LENIENT_ONE[..]
8799 }
8800 fn eval<'a, 'b, 'c>(
8801 &self,
8802 args: &'c [ArgumentHandle<'a, 'b>],
8803 _ctx: &dyn FunctionContext<'b>,
8804 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
8805 let nums = collect_numeric_a(args)?;
8806 let n = nums.len();
8807 if n < 2 {
8808 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8809 ExcelError::new_div(),
8810 )));
8811 }
8812 let mean = nums.iter().sum::<f64>() / n as f64;
8813 let ss: f64 = nums.iter().map(|v| (v - mean).powi(2)).sum();
8814 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
8815 ss / (n - 1) as f64,
8816 )))
8817 }
8818}
8819
8820/* ─────────────────────────── VARPA ──────────────────────────── */
8821
8822/// Returns the population variance using A-variant coercion semantics.
8823///
8824/// # Formula example
8825/// ```excel
8826/// # returns: 0.6666666666666666
8827/// =VARPA(TRUE,2,"x")
8828/// ```
8829///
8830/// ```yaml,sandbox
8831/// title: "Population variance with coerced values"
8832/// formula: '=VARPA(TRUE,2,"x")'
8833/// expected: 0.6666666666666666
8834/// ```
8835///
8836/// ```yaml,docs
8837/// related:
8838/// - VARA
8839/// - STDEVPA
8840/// - STDEV.P
8841/// faq:
8842/// - q: "When does VARPA return #DIV/0!?"
8843/// a: "It returns #DIV/0! when no coerced values remain after evaluation."
8844/// ```
8845#[derive(Debug)]
8846pub struct VarPAFn;
8847/// [formualizer-docgen:schema:start]
8848/// Name: VARPA
8849/// Type: VarPAFn
8850/// Min args: 1
8851/// Max args: variadic
8852/// Variadic: true
8853/// Signature: VARPA(arg1...: number@range)
8854/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
8855/// Caps: PURE, REDUCTION
8856/// [formualizer-docgen:schema:end]
8857impl Function for VarPAFn {
8858 func_caps!(PURE, REDUCTION);
8859 fn name(&self) -> &'static str {
8860 "VARPA"
8861 }
8862 fn min_args(&self) -> usize {
8863 1
8864 }
8865 fn variadic(&self) -> bool {
8866 true
8867 }
8868 fn arg_schema(&self) -> &'static [ArgSchema] {
8869 &ARG_RANGE_NUM_LENIENT_ONE[..]
8870 }
8871 fn eval<'a, 'b, 'c>(
8872 &self,
8873 args: &'c [ArgumentHandle<'a, 'b>],
8874 _ctx: &dyn FunctionContext<'b>,
8875 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
8876 let nums = collect_numeric_a(args)?;
8877 let n = nums.len();
8878 if n == 0 {
8879 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8880 ExcelError::new_div(),
8881 )));
8882 }
8883 let mean = nums.iter().sum::<f64>() / n as f64;
8884 let ss: f64 = nums.iter().map(|v| (v - mean).powi(2)).sum();
8885 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
8886 ss / n as f64,
8887 )))
8888 }
8889}
8890
8891/* ─────────────────────────── SKEW.P ──────────────────────────── */
8892
8893/// Returns the population skewness of a numeric data set.
8894///
8895/// # Formula example
8896/// ```excel
8897/// # returns: 0
8898/// =SKEW.P(1,2,3)
8899/// ```
8900///
8901/// ```yaml,sandbox
8902/// title: "Symmetric data has zero skew"
8903/// formula: '=SKEW.P(1,2,3)'
8904/// expected: 0
8905/// ```
8906///
8907/// ```yaml,docs
8908/// related:
8909/// - SKEW
8910/// - KURT
8911/// - AVERAGE
8912/// faq:
8913/// - q: "When does SKEW.P return #DIV/0!?"
8914/// a: "It returns #DIV/0! when fewer than three numeric values are available or the population standard deviation is zero."
8915/// ```
8916#[derive(Debug)]
8917pub struct SkewPFn;
8918/// [formualizer-docgen:schema:start]
8919/// Name: SKEW.P
8920/// Type: SkewPFn
8921/// Min args: 1
8922/// Max args: variadic
8923/// Variadic: true
8924/// Signature: SKEW.P(arg1...: number@range)
8925/// Arg schema: arg1{kinds=number,required=true,shape=range,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
8926/// Caps: PURE, REDUCTION, NUMERIC_ONLY
8927/// [formualizer-docgen:schema:end]
8928impl Function for SkewPFn {
8929 func_caps!(PURE, REDUCTION, NUMERIC_ONLY);
8930 fn name(&self) -> &'static str {
8931 "SKEW.P"
8932 }
8933 fn min_args(&self) -> usize {
8934 1
8935 }
8936 fn variadic(&self) -> bool {
8937 true
8938 }
8939 fn arg_schema(&self) -> &'static [ArgSchema] {
8940 &ARG_RANGE_NUM_LENIENT_ONE[..]
8941 }
8942 fn eval<'a, 'b, 'c>(
8943 &self,
8944 args: &'c [ArgumentHandle<'a, 'b>],
8945 _ctx: &dyn FunctionContext<'b>,
8946 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
8947 let nums = collect_numeric_stats(args)?;
8948 let n = nums.len();
8949 if n < 3 {
8950 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8951 ExcelError::new_div(),
8952 )));
8953 }
8954 let n_f = n as f64;
8955 let mean = nums.iter().sum::<f64>() / n_f;
8956 let mut sum_sq = 0.0;
8957 for &v in &nums {
8958 sum_sq += (v - mean).powi(2);
8959 }
8960 let stdev_pop = (sum_sq / n_f).sqrt();
8961 if stdev_pop == 0.0 {
8962 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
8963 ExcelError::new_div(),
8964 )));
8965 }
8966 let mut sum_cubed = 0.0;
8967 for &v in &nums {
8968 sum_cubed += ((v - mean) / stdev_pop).powi(3);
8969 }
8970 // Population skewness: (1/n) * sum((xi - mean)/sigma)^3
8971 let skew = sum_cubed / n_f;
8972 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(skew)))
8973 }
8974}
8975
8976/* ─────────────────────────── T.DIST.RT ──────────────────────────── */
8977
8978/// Returns the right-tailed Student's t-distribution probability.
8979///
8980/// # Formula example
8981/// ```excel
8982/// # returns: 0.5
8983/// =T.DIST.RT(0,10)
8984/// ```
8985///
8986/// ```yaml,sandbox
8987/// title: "Zero lies at the midpoint of the t distribution"
8988/// formula: '=T.DIST.RT(0,10)'
8989/// expected: 0.5
8990/// ```
8991///
8992/// ```yaml,docs
8993/// related:
8994/// - T.DIST.2T
8995/// - T.INV
8996/// - T.INV.2T
8997/// faq:
8998/// - q: "When does T.DIST.RT return #NUM!?"
8999/// a: "It returns #NUM! when the degrees of freedom are less than 1."
9000/// ```
9001#[derive(Debug)]
9002pub struct TDistRtFn;
9003/// [formualizer-docgen:schema:start]
9004/// Name: T.DIST.RT
9005/// Type: TDistRtFn
9006/// Min args: 2
9007/// Max args: 2
9008/// Variadic: false
9009/// Signature: T.DIST.RT(arg1: number@scalar, arg2: number@scalar)
9010/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
9011/// Caps: PURE
9012/// [formualizer-docgen:schema:end]
9013impl Function for TDistRtFn {
9014 func_caps!(PURE);
9015 fn name(&self) -> &'static str {
9016 "T.DIST.RT"
9017 }
9018 fn min_args(&self) -> usize {
9019 2
9020 }
9021 fn arg_schema(&self) -> &'static [ArgSchema] {
9022 use std::sync::LazyLock;
9023 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
9024 vec![
9025 ArgSchema::number_lenient_scalar(),
9026 ArgSchema::number_lenient_scalar(),
9027 ]
9028 });
9029 &SCHEMA[..]
9030 }
9031 fn eval<'a, 'b, 'c>(
9032 &self,
9033 args: &'c [ArgumentHandle<'a, 'b>],
9034 _ctx: &dyn FunctionContext<'b>,
9035 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
9036 let x = coerce_num(&scalar_like_value(&args[0])?)?;
9037 let df = coerce_num(&scalar_like_value(&args[1])?)?;
9038 if df < 1.0 {
9039 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9040 ExcelError::new_num(),
9041 )));
9042 }
9043 let result = 1.0 - t_cdf(x, df);
9044 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
9045 result,
9046 )))
9047 }
9048}
9049
9050/* ─────────────────────────── CHISQ.DIST.RT ──────────────────────────── */
9051
9052/// Returns the right-tailed chi-squared distribution probability.
9053///
9054/// # Formula example
9055/// ```excel
9056/// # returns: 0.36787944117144233
9057/// =CHISQ.DIST.RT(2,2)
9058/// ```
9059///
9060/// ```yaml,sandbox
9061/// title: "Right-tail chi-squared probability"
9062/// formula: '=CHISQ.DIST.RT(2,2)'
9063/// expected: 0.36787944117144233
9064/// ```
9065///
9066/// ```yaml,docs
9067/// related:
9068/// - CHISQ.INV.RT
9069/// - CHISQ.TEST
9070/// - CHISQ.DIST
9071/// faq:
9072/// - q: "Which inputs return #NUM!?"
9073/// a: "Negative x values and degrees of freedom below 1 return #NUM!."
9074/// ```
9075#[derive(Debug)]
9076pub struct ChisqDistRtFn;
9077/// [formualizer-docgen:schema:start]
9078/// Name: CHISQ.DIST.RT
9079/// Type: ChisqDistRtFn
9080/// Min args: 2
9081/// Max args: 2
9082/// Variadic: false
9083/// Signature: CHISQ.DIST.RT(arg1: number@scalar, arg2: number@scalar)
9084/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
9085/// Caps: PURE
9086/// [formualizer-docgen:schema:end]
9087impl Function for ChisqDistRtFn {
9088 func_caps!(PURE);
9089 fn name(&self) -> &'static str {
9090 "CHISQ.DIST.RT"
9091 }
9092 fn min_args(&self) -> usize {
9093 2
9094 }
9095 fn arg_schema(&self) -> &'static [ArgSchema] {
9096 use std::sync::LazyLock;
9097 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
9098 vec![
9099 ArgSchema::number_lenient_scalar(),
9100 ArgSchema::number_lenient_scalar(),
9101 ]
9102 });
9103 &SCHEMA[..]
9104 }
9105 fn eval<'a, 'b, 'c>(
9106 &self,
9107 args: &'c [ArgumentHandle<'a, 'b>],
9108 _ctx: &dyn FunctionContext<'b>,
9109 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
9110 let x = coerce_num(&scalar_like_value(&args[0])?)?;
9111 let df = coerce_num(&scalar_like_value(&args[1])?)?;
9112 if df < 1.0 || x < 0.0 {
9113 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9114 ExcelError::new_num(),
9115 )));
9116 }
9117 let result = 1.0 - chisq_cdf(x, df);
9118 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
9119 result,
9120 )))
9121 }
9122}
9123
9124/* ─────────────────────────── CHISQ.INV.RT ──────────────────────────── */
9125
9126/// Returns the inverse of the right-tailed chi-squared distribution.
9127///
9128/// # Formula example
9129/// ```excel
9130/// # returns: 1.3862943611198906
9131/// =CHISQ.INV.RT(0.5,2)
9132/// ```
9133///
9134/// ```yaml,sandbox
9135/// title: "Median right-tail inverse for 2 degrees of freedom"
9136/// formula: '=CHISQ.INV.RT(0.5,2)'
9137/// expected: 1.3862943611198906
9138/// ```
9139///
9140/// ```yaml,docs
9141/// related:
9142/// - CHISQ.DIST.RT
9143/// - CHISQ.INV
9144/// - CHISQ.TEST
9145/// faq:
9146/// - q: "What p-values are valid for CHISQ.INV.RT?"
9147/// a: "p must lie in the range 0 to 1, and p=0 returns #NUM! because the right-tail inverse diverges."
9148/// ```
9149#[derive(Debug)]
9150pub struct ChisqInvRtFn;
9151/// [formualizer-docgen:schema:start]
9152/// Name: CHISQ.INV.RT
9153/// Type: ChisqInvRtFn
9154/// Min args: 2
9155/// Max args: 2
9156/// Variadic: false
9157/// Signature: CHISQ.INV.RT(arg1: number@scalar, arg2: number@scalar)
9158/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
9159/// Caps: PURE
9160/// [formualizer-docgen:schema:end]
9161impl Function for ChisqInvRtFn {
9162 func_caps!(PURE);
9163 fn name(&self) -> &'static str {
9164 "CHISQ.INV.RT"
9165 }
9166 fn min_args(&self) -> usize {
9167 2
9168 }
9169 fn arg_schema(&self) -> &'static [ArgSchema] {
9170 use std::sync::LazyLock;
9171 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
9172 vec![
9173 ArgSchema::number_lenient_scalar(),
9174 ArgSchema::number_lenient_scalar(),
9175 ]
9176 });
9177 &SCHEMA[..]
9178 }
9179 fn eval<'a, 'b, 'c>(
9180 &self,
9181 args: &'c [ArgumentHandle<'a, 'b>],
9182 _ctx: &dyn FunctionContext<'b>,
9183 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
9184 let p = coerce_num(&scalar_like_value(&args[0])?)?;
9185 let df = coerce_num(&scalar_like_value(&args[1])?)?;
9186 if df < 1.0 || !(0.0..=1.0).contains(&p) {
9187 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9188 ExcelError::new_num(),
9189 )));
9190 }
9191 // Right-tail: CHISQ.INV.RT(p, df) = CHISQ.INV(1-p, df)
9192 if p == 0.0 {
9193 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9194 ExcelError::new_num(),
9195 )));
9196 }
9197 match chisq_inv(1.0 - p, df) {
9198 Some(result) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
9199 result,
9200 ))),
9201 None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9202 ExcelError::new_num(),
9203 ))),
9204 }
9205 }
9206}
9207
9208/* ─────────────────────────── F.DIST.RT ──────────────────────────── */
9209
9210/// Returns the right-tailed F-distribution probability.
9211///
9212/// # Formula example
9213/// ```excel
9214/// # returns: 1
9215/// =F.DIST.RT(0,5,10)
9216/// ```
9217///
9218/// ```yaml,sandbox
9219/// title: "Zero leaves the entire right tail"
9220/// formula: '=F.DIST.RT(0,5,10)'
9221/// expected: 1
9222/// ```
9223///
9224/// ```yaml,docs
9225/// related:
9226/// - F.INV.RT
9227/// - F.TEST
9228/// - F.DIST
9229/// faq:
9230/// - q: "Which inputs return #NUM!?"
9231/// a: "Negative x values or degrees of freedom below 1 return #NUM!."
9232/// ```
9233#[derive(Debug)]
9234pub struct FDistRtFn;
9235/// [formualizer-docgen:schema:start]
9236/// Name: F.DIST.RT
9237/// Type: FDistRtFn
9238/// Min args: 3
9239/// Max args: 3
9240/// Variadic: false
9241/// Signature: F.DIST.RT(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
9242/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
9243/// Caps: PURE
9244/// [formualizer-docgen:schema:end]
9245impl Function for FDistRtFn {
9246 func_caps!(PURE);
9247 fn name(&self) -> &'static str {
9248 "F.DIST.RT"
9249 }
9250 fn min_args(&self) -> usize {
9251 3
9252 }
9253 fn arg_schema(&self) -> &'static [ArgSchema] {
9254 use std::sync::LazyLock;
9255 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
9256 vec![
9257 ArgSchema::number_lenient_scalar(),
9258 ArgSchema::number_lenient_scalar(),
9259 ArgSchema::number_lenient_scalar(),
9260 ]
9261 });
9262 &SCHEMA[..]
9263 }
9264 fn eval<'a, 'b, 'c>(
9265 &self,
9266 args: &'c [ArgumentHandle<'a, 'b>],
9267 _ctx: &dyn FunctionContext<'b>,
9268 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
9269 let x = coerce_num(&scalar_like_value(&args[0])?)?;
9270 let d1 = coerce_num(&scalar_like_value(&args[1])?)?;
9271 let d2 = coerce_num(&scalar_like_value(&args[2])?)?;
9272 if d1 < 1.0 || d2 < 1.0 || x < 0.0 {
9273 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9274 ExcelError::new_num(),
9275 )));
9276 }
9277 let result = 1.0 - f_cdf(x, d1, d2);
9278 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
9279 result,
9280 )))
9281 }
9282}
9283
9284/* ─────────────────────────── F.INV.RT ──────────────────────────── */
9285
9286/// Returns the inverse of the right-tailed F-distribution.
9287///
9288/// # Formula example
9289/// ```excel
9290/// # returns: 0
9291/// =F.INV.RT(1,5,10)
9292/// ```
9293///
9294/// ```yaml,sandbox
9295/// title: "A full right tail maps to zero"
9296/// formula: '=F.INV.RT(1,5,10)'
9297/// expected: 0
9298/// ```
9299///
9300/// ```yaml,docs
9301/// related:
9302/// - F.DIST.RT
9303/// - F.INV
9304/// - F.TEST
9305/// faq:
9306/// - q: "What p-values are valid for F.INV.RT?"
9307/// a: "p must lie in the range 0 to 1, and p=0 returns #NUM! because the inverse diverges."
9308/// ```
9309#[derive(Debug)]
9310pub struct FInvRtFn;
9311/// [formualizer-docgen:schema:start]
9312/// Name: F.INV.RT
9313/// Type: FInvRtFn
9314/// Min args: 3
9315/// Max args: 3
9316/// Variadic: false
9317/// Signature: F.INV.RT(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
9318/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
9319/// Caps: PURE
9320/// [formualizer-docgen:schema:end]
9321impl Function for FInvRtFn {
9322 func_caps!(PURE);
9323 fn name(&self) -> &'static str {
9324 "F.INV.RT"
9325 }
9326 fn min_args(&self) -> usize {
9327 3
9328 }
9329 fn arg_schema(&self) -> &'static [ArgSchema] {
9330 use std::sync::LazyLock;
9331 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
9332 vec![
9333 ArgSchema::number_lenient_scalar(),
9334 ArgSchema::number_lenient_scalar(),
9335 ArgSchema::number_lenient_scalar(),
9336 ]
9337 });
9338 &SCHEMA[..]
9339 }
9340 fn eval<'a, 'b, 'c>(
9341 &self,
9342 args: &'c [ArgumentHandle<'a, 'b>],
9343 _ctx: &dyn FunctionContext<'b>,
9344 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
9345 let p = coerce_num(&scalar_like_value(&args[0])?)?;
9346 let d1 = coerce_num(&scalar_like_value(&args[1])?)?;
9347 let d2 = coerce_num(&scalar_like_value(&args[2])?)?;
9348 if d1 < 1.0 || d2 < 1.0 || !(0.0..=1.0).contains(&p) {
9349 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9350 ExcelError::new_num(),
9351 )));
9352 }
9353 if p == 0.0 {
9354 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9355 ExcelError::new_num(),
9356 )));
9357 }
9358 // F.INV.RT(1, d1, d2) = 0 (entire right tail)
9359 if p == 1.0 {
9360 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0)));
9361 }
9362 // F.INV.RT(p, d1, d2) = F.INV(1-p, d1, d2)
9363 match f_inv(1.0 - p, d1, d2) {
9364 Some(result) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
9365 result,
9366 ))),
9367 None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9368 ExcelError::new_num(),
9369 ))),
9370 }
9371 }
9372}
9373
9374/* ─────────────────────────── BETA.INV ──────────────────────────── */
9375
9376/// Returns the inverse cumulative beta distribution, optionally scaled to custom bounds.
9377///
9378/// # Formula example
9379/// ```excel
9380/// # returns: 0.5
9381/// =BETA.INV(0.5,2,2)
9382/// ```
9383///
9384/// ```yaml,sandbox
9385/// title: "Symmetric beta inverse at the median"
9386/// formula: '=BETA.INV(0.5,2,2)'
9387/// expected: 0.5
9388/// ```
9389///
9390/// ```yaml,docs
9391/// related:
9392/// - BETA.DIST
9393/// - GAMMA.INV
9394/// - NORM.INV
9395/// faq:
9396/// - q: "When does BETA.INV return #NUM!?"
9397/// a: "It returns #NUM! for non-positive alpha or beta, invalid bounds, or probabilities outside 0..1."
9398/// ```
9399#[derive(Debug)]
9400pub struct BetaInvFn;
9401/// [formualizer-docgen:schema:start]
9402/// Name: BETA.INV
9403/// Type: BetaInvFn
9404/// Min args: 3
9405/// Max args: variadic
9406/// Variadic: true
9407/// Signature: BETA.INV(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar, arg5...: number@scalar)
9408/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg4{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg5{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
9409/// Caps: PURE
9410/// [formualizer-docgen:schema:end]
9411impl Function for BetaInvFn {
9412 func_caps!(PURE);
9413 fn name(&self) -> &'static str {
9414 "BETA.INV"
9415 }
9416 fn min_args(&self) -> usize {
9417 3
9418 }
9419 fn variadic(&self) -> bool {
9420 true
9421 }
9422 fn arg_schema(&self) -> &'static [ArgSchema] {
9423 use std::sync::LazyLock;
9424 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
9425 vec![
9426 ArgSchema::number_lenient_scalar(),
9427 ArgSchema::number_lenient_scalar(),
9428 ArgSchema::number_lenient_scalar(),
9429 ArgSchema::number_lenient_scalar(),
9430 ArgSchema::number_lenient_scalar(),
9431 ]
9432 });
9433 &SCHEMA[..]
9434 }
9435 fn eval<'a, 'b, 'c>(
9436 &self,
9437 args: &'c [ArgumentHandle<'a, 'b>],
9438 _ctx: &dyn FunctionContext<'b>,
9439 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
9440 let p = coerce_num(&scalar_like_value(&args[0])?)?;
9441 let alpha = coerce_num(&scalar_like_value(&args[1])?)?;
9442 let beta_param = coerce_num(&scalar_like_value(&args[2])?)?;
9443 let a_bound = if args.len() > 3 {
9444 coerce_num(&scalar_like_value(&args[3])?)?
9445 } else {
9446 0.0
9447 };
9448 let b_bound = if args.len() > 4 {
9449 coerce_num(&scalar_like_value(&args[4])?)?
9450 } else {
9451 1.0
9452 };
9453
9454 if alpha <= 0.0 || beta_param <= 0.0 || a_bound >= b_bound || !(0.0..=1.0).contains(&p) {
9455 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9456 ExcelError::new_num(),
9457 )));
9458 }
9459
9460 match beta_inv_helper(p, alpha, beta_param) {
9461 Some(x_std) => {
9462 let result = a_bound + x_std * (b_bound - a_bound);
9463 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
9464 result,
9465 )))
9466 }
9467 None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9468 ExcelError::new_num(),
9469 ))),
9470 }
9471 }
9472}
9473
9474/* ─────────────────────────── BINOM.DIST.RANGE ──────────────────────────── */
9475
9476/// Returns the probability that a binomial random variable falls within a range of successes.
9477///
9478/// # Formula example
9479/// ```excel
9480/// # returns: 0.1171875
9481/// =BINOM.DIST.RANGE(10,0.5,3,3)
9482/// ```
9483///
9484/// ```yaml,sandbox
9485/// title: "Probability of exactly three successes"
9486/// formula: '=BINOM.DIST.RANGE(10,0.5,3,3)'
9487/// expected: 0.1171875
9488/// ```
9489///
9490/// ```yaml,docs
9491/// related:
9492/// - BINOM.DIST
9493/// - BINOM.INV
9494/// - POISSON.DIST
9495/// faq:
9496/// - q: "What happens if the upper bound is omitted?"
9497/// a: "The function treats the lower and upper bounds as the same value, yielding the probability of exactly that many successes."
9498/// ```
9499#[derive(Debug)]
9500pub struct BinomDistRangeFn;
9501/// [formualizer-docgen:schema:start]
9502/// Name: BINOM.DIST.RANGE
9503/// Type: BinomDistRangeFn
9504/// Min args: 3
9505/// Max args: variadic
9506/// Variadic: true
9507/// Signature: BINOM.DIST.RANGE(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4...: number@scalar)
9508/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg4{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
9509/// Caps: PURE
9510/// [formualizer-docgen:schema:end]
9511impl Function for BinomDistRangeFn {
9512 func_caps!(PURE);
9513 fn name(&self) -> &'static str {
9514 "BINOM.DIST.RANGE"
9515 }
9516 fn min_args(&self) -> usize {
9517 3
9518 }
9519 fn variadic(&self) -> bool {
9520 true
9521 }
9522 fn arg_schema(&self) -> &'static [ArgSchema] {
9523 use std::sync::LazyLock;
9524 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
9525 vec![
9526 ArgSchema::number_lenient_scalar(),
9527 ArgSchema::number_lenient_scalar(),
9528 ArgSchema::number_lenient_scalar(),
9529 ArgSchema::number_lenient_scalar(),
9530 ]
9531 });
9532 &SCHEMA[..]
9533 }
9534 fn eval<'a, 'b, 'c>(
9535 &self,
9536 args: &'c [ArgumentHandle<'a, 'b>],
9537 _ctx: &dyn FunctionContext<'b>,
9538 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
9539 let n = coerce_num(&scalar_like_value(&args[0])?)?.trunc() as i64;
9540 let p = coerce_num(&scalar_like_value(&args[1])?)?;
9541 let s = coerce_num(&scalar_like_value(&args[2])?)?.trunc() as i64;
9542 let s2 = if args.len() > 3 {
9543 coerce_num(&scalar_like_value(&args[3])?)?.trunc() as i64
9544 } else {
9545 s
9546 };
9547
9548 if n < 0 || !(0.0..=1.0).contains(&p) || s < 0 || s > n || s2 < s || s2 > n {
9549 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9550 ExcelError::new_num(),
9551 )));
9552 }
9553
9554 let mut sum = 0.0;
9555 for k in s..=s2 {
9556 let ln_prob = ln_binom(n, k) + (k as f64) * p.ln() + ((n - k) as f64) * (1.0 - p).ln();
9557 sum += ln_prob.exp();
9558 }
9559 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(sum)))
9560 }
9561}
9562
9563/* ─────────────────────────── BINOM.INV ──────────────────────────── */
9564
9565/// Returns the smallest number of successes whose cumulative binomial probability meets a threshold.
9566///
9567/// # Formula example
9568/// ```excel
9569/// # returns: 5
9570/// =BINOM.INV(10,0.5,0.5)
9571/// ```
9572///
9573/// ```yaml,sandbox
9574/// title: "Median success threshold"
9575/// formula: '=BINOM.INV(10,0.5,0.5)'
9576/// expected: 5
9577/// ```
9578///
9579/// ```yaml,docs
9580/// related:
9581/// - BINOM.DIST
9582/// - BINOM.DIST.RANGE
9583/// - CRITBINOM
9584/// faq:
9585/// - q: "Is CRITBINOM supported?"
9586/// a: "Yes. CRITBINOM is registered as an alias of BINOM.INV."
9587/// ```
9588#[derive(Debug)]
9589pub struct BinomInvFn;
9590/// [formualizer-docgen:schema:start]
9591/// Name: BINOM.INV
9592/// Type: BinomInvFn
9593/// Min args: 3
9594/// Max args: 3
9595/// Variadic: false
9596/// Signature: BINOM.INV(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
9597/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
9598/// Caps: PURE
9599/// [formualizer-docgen:schema:end]
9600impl Function for BinomInvFn {
9601 func_caps!(PURE);
9602 fn name(&self) -> &'static str {
9603 "BINOM.INV"
9604 }
9605 fn aliases(&self) -> &'static [&'static str] {
9606 &["CRITBINOM"]
9607 }
9608 fn min_args(&self) -> usize {
9609 3
9610 }
9611 fn arg_schema(&self) -> &'static [ArgSchema] {
9612 use std::sync::LazyLock;
9613 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
9614 vec![
9615 ArgSchema::number_lenient_scalar(),
9616 ArgSchema::number_lenient_scalar(),
9617 ArgSchema::number_lenient_scalar(),
9618 ]
9619 });
9620 &SCHEMA[..]
9621 }
9622 fn eval<'a, 'b, 'c>(
9623 &self,
9624 args: &'c [ArgumentHandle<'a, 'b>],
9625 _ctx: &dyn FunctionContext<'b>,
9626 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
9627 let n = coerce_num(&scalar_like_value(&args[0])?)?.trunc() as i64;
9628 let p = coerce_num(&scalar_like_value(&args[1])?)?;
9629 let alpha = coerce_num(&scalar_like_value(&args[2])?)?;
9630
9631 if n < 0 || !(0.0..=1.0).contains(&p) || !(0.0..=1.0).contains(&alpha) {
9632 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9633 ExcelError::new_num(),
9634 )));
9635 }
9636
9637 // Find smallest k such that BINOM.DIST(k, n, p, TRUE) >= alpha
9638 let mut cum = 0.0;
9639 for k in 0..=n {
9640 let ln_prob = ln_binom(n, k) + (k as f64) * p.ln() + ((n - k) as f64) * (1.0 - p).ln();
9641 cum += ln_prob.exp();
9642 if cum >= alpha {
9643 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
9644 k as f64,
9645 )));
9646 }
9647 }
9648 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
9649 n as f64,
9650 )))
9651 }
9652}
9653
9654/* ─────────────────────────── GAMMA ──────────────────────────── */
9655
9656/// Returns the value of the gamma function.
9657///
9658/// # Formula example
9659/// ```excel
9660/// # returns: 24
9661/// =GAMMA(5)
9662/// ```
9663///
9664/// ```yaml,sandbox
9665/// title: "Gamma extends factorials"
9666/// formula: '=GAMMA(5)'
9667/// expected: 24
9668/// ```
9669///
9670/// ```yaml,docs
9671/// related:
9672/// - GAMMALN
9673/// - GAMMA.INV
9674/// - FACT
9675/// faq:
9676/// - q: "When does GAMMA return #NUM!?"
9677/// a: "It returns #NUM! for zero and negative integers, where the gamma function has poles."
9678/// ```
9679#[derive(Debug)]
9680pub struct GammaFn;
9681/// [formualizer-docgen:schema:start]
9682/// Name: GAMMA
9683/// Type: GammaFn
9684/// Min args: 1
9685/// Max args: 1
9686/// Variadic: false
9687/// Signature: GAMMA(arg1: number@scalar)
9688/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
9689/// Caps: PURE
9690/// [formualizer-docgen:schema:end]
9691impl Function for GammaFn {
9692 func_caps!(PURE);
9693 fn name(&self) -> &'static str {
9694 "GAMMA"
9695 }
9696 fn min_args(&self) -> usize {
9697 1
9698 }
9699 fn arg_schema(&self) -> &'static [ArgSchema] {
9700 use std::sync::LazyLock;
9701 static SCHEMA: LazyLock<Vec<ArgSchema>> =
9702 LazyLock::new(|| vec![ArgSchema::number_lenient_scalar()]);
9703 &SCHEMA[..]
9704 }
9705 fn eval<'a, 'b, 'c>(
9706 &self,
9707 args: &'c [ArgumentHandle<'a, 'b>],
9708 _ctx: &dyn FunctionContext<'b>,
9709 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
9710 let x = coerce_num(&scalar_like_value(&args[0])?)?;
9711 // GAMMA(0) and negative integers are #NUM!
9712 if x == 0.0 || (x < 0.0 && x == x.trunc()) {
9713 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9714 ExcelError::new_num(),
9715 )));
9716 }
9717 let result = ln_gamma(x).exp();
9718 if result.is_infinite() || result.is_nan() {
9719 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9720 ExcelError::new_num(),
9721 )));
9722 }
9723 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
9724 result,
9725 )))
9726 }
9727}
9728
9729/* ─────────────────────────── GAMMA.INV ──────────────────────────── */
9730
9731/// Returns the inverse cumulative gamma distribution.
9732///
9733/// # Formula example
9734/// ```excel
9735/// # returns: 0.6931471805599453
9736/// =GAMMA.INV(0.5,1,1)
9737/// ```
9738///
9739/// ```yaml,sandbox
9740/// title: "Exponential special case"
9741/// formula: '=GAMMA.INV(0.5,1,1)'
9742/// expected: 0.6931471805599453
9743/// ```
9744///
9745/// ```yaml,docs
9746/// related:
9747/// - GAMMA.DIST
9748/// - GAMMA
9749/// - BETA.INV
9750/// faq:
9751/// - q: "Is GAMMAINV supported?"
9752/// a: "Yes. GAMMAINV is registered as an alias of GAMMA.INV."
9753/// ```
9754#[derive(Debug)]
9755pub struct GammaInvFn;
9756/// [formualizer-docgen:schema:start]
9757/// Name: GAMMA.INV
9758/// Type: GammaInvFn
9759/// Min args: 3
9760/// Max args: 3
9761/// Variadic: false
9762/// Signature: GAMMA.INV(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
9763/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
9764/// Caps: PURE
9765/// [formualizer-docgen:schema:end]
9766impl Function for GammaInvFn {
9767 func_caps!(PURE);
9768 fn name(&self) -> &'static str {
9769 "GAMMA.INV"
9770 }
9771 fn aliases(&self) -> &'static [&'static str] {
9772 &["GAMMAINV"]
9773 }
9774 fn min_args(&self) -> usize {
9775 3
9776 }
9777 fn arg_schema(&self) -> &'static [ArgSchema] {
9778 use std::sync::LazyLock;
9779 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
9780 vec![
9781 ArgSchema::number_lenient_scalar(),
9782 ArgSchema::number_lenient_scalar(),
9783 ArgSchema::number_lenient_scalar(),
9784 ]
9785 });
9786 &SCHEMA[..]
9787 }
9788 fn eval<'a, 'b, 'c>(
9789 &self,
9790 args: &'c [ArgumentHandle<'a, 'b>],
9791 _ctx: &dyn FunctionContext<'b>,
9792 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
9793 let p = coerce_num(&scalar_like_value(&args[0])?)?;
9794 let alpha = coerce_num(&scalar_like_value(&args[1])?)?;
9795 let beta = coerce_num(&scalar_like_value(&args[2])?)?;
9796
9797 if alpha <= 0.0 || beta <= 0.0 || !(0.0..=1.0).contains(&p) {
9798 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9799 ExcelError::new_num(),
9800 )));
9801 }
9802
9803 match gamma_inv_helper(p, alpha, beta) {
9804 Some(result) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
9805 result,
9806 ))),
9807 None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9808 ExcelError::new_num(),
9809 ))),
9810 }
9811 }
9812}
9813
9814/* ─────────────────────────── GAMMALN ──────────────────────────── */
9815
9816/// Returns the natural logarithm of the gamma function.
9817///
9818/// # Formula example
9819/// ```excel
9820/// # returns: 3.1780538303479458
9821/// =GAMMALN(5)
9822/// ```
9823///
9824/// ```yaml,sandbox
9825/// title: "Log gamma at 5"
9826/// formula: '=GAMMALN(5)'
9827/// expected: 3.1780538303479458
9828/// ```
9829///
9830/// ```yaml,docs
9831/// related:
9832/// - GAMMALN.PRECISE
9833/// - GAMMA
9834/// - LN
9835/// faq:
9836/// - q: "When does GAMMALN return #NUM!?"
9837/// a: "It returns #NUM! for zero or negative inputs."
9838/// ```
9839#[derive(Debug)]
9840pub struct GammaLnFn;
9841/// [formualizer-docgen:schema:start]
9842/// Name: GAMMALN
9843/// Type: GammaLnFn
9844/// Min args: 1
9845/// Max args: 1
9846/// Variadic: false
9847/// Signature: GAMMALN(arg1: number@scalar)
9848/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
9849/// Caps: PURE
9850/// [formualizer-docgen:schema:end]
9851impl Function for GammaLnFn {
9852 func_caps!(PURE);
9853 fn name(&self) -> &'static str {
9854 "GAMMALN"
9855 }
9856 fn min_args(&self) -> usize {
9857 1
9858 }
9859 fn arg_schema(&self) -> &'static [ArgSchema] {
9860 use std::sync::LazyLock;
9861 static SCHEMA: LazyLock<Vec<ArgSchema>> =
9862 LazyLock::new(|| vec![ArgSchema::number_lenient_scalar()]);
9863 &SCHEMA[..]
9864 }
9865 fn eval<'a, 'b, 'c>(
9866 &self,
9867 args: &'c [ArgumentHandle<'a, 'b>],
9868 _ctx: &dyn FunctionContext<'b>,
9869 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
9870 let x = coerce_num(&scalar_like_value(&args[0])?)?;
9871 if x <= 0.0 {
9872 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9873 ExcelError::new_num(),
9874 )));
9875 }
9876 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
9877 ln_gamma(x),
9878 )))
9879 }
9880}
9881
9882/* ─────────────────────────── GAMMALN.PRECISE ──────────────────────────── */
9883
9884/// Returns the natural logarithm of the gamma function using Excel's precise naming variant.
9885///
9886/// # Formula example
9887/// ```excel
9888/// # returns: 3.1780538303479458
9889/// =GAMMALN.PRECISE(5)
9890/// ```
9891///
9892/// ```yaml,sandbox
9893/// title: "Precise log gamma at 5"
9894/// formula: '=GAMMALN.PRECISE(5)'
9895/// expected: 3.1780538303479458
9896/// ```
9897///
9898/// ```yaml,docs
9899/// related:
9900/// - GAMMALN
9901/// - GAMMA
9902/// - LN
9903/// faq:
9904/// - q: "How does GAMMALN.PRECISE differ here?"
9905/// a: "This implementation uses the same core log-gamma calculation as GAMMALN, matching Excel's function naming split."
9906/// ```
9907#[derive(Debug)]
9908pub struct GammaLnPreciseFn;
9909/// [formualizer-docgen:schema:start]
9910/// Name: GAMMALN.PRECISE
9911/// Type: GammaLnPreciseFn
9912/// Min args: 1
9913/// Max args: 1
9914/// Variadic: false
9915/// Signature: GAMMALN.PRECISE(arg1: number@scalar)
9916/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
9917/// Caps: PURE
9918/// [formualizer-docgen:schema:end]
9919impl Function for GammaLnPreciseFn {
9920 func_caps!(PURE);
9921 fn name(&self) -> &'static str {
9922 "GAMMALN.PRECISE"
9923 }
9924 fn min_args(&self) -> usize {
9925 1
9926 }
9927 fn arg_schema(&self) -> &'static [ArgSchema] {
9928 use std::sync::LazyLock;
9929 static SCHEMA: LazyLock<Vec<ArgSchema>> =
9930 LazyLock::new(|| vec![ArgSchema::number_lenient_scalar()]);
9931 &SCHEMA[..]
9932 }
9933 fn eval<'a, 'b, 'c>(
9934 &self,
9935 args: &'c [ArgumentHandle<'a, 'b>],
9936 _ctx: &dyn FunctionContext<'b>,
9937 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
9938 let x = coerce_num(&scalar_like_value(&args[0])?)?;
9939 if x <= 0.0 {
9940 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
9941 ExcelError::new_num(),
9942 )));
9943 }
9944 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
9945 ln_gamma(x),
9946 )))
9947 }
9948}
9949
9950pub fn register_builtins() {
9951 use std::sync::Arc;
9952 crate::function_registry::register_function(Arc::new(ForecastLinearFn));
9953 crate::function_registry::register_function(Arc::new(LinestFn));
9954 crate::function_registry::register_function(Arc::new(LARGE));
9955 crate::function_registry::register_function(Arc::new(SMALL));
9956 crate::function_registry::register_function(Arc::new(MEDIAN));
9957 crate::function_registry::register_function(Arc::new(StdevSample));
9958 crate::function_registry::register_function(Arc::new(StdevPop));
9959 crate::function_registry::register_function(Arc::new(VarSample));
9960 crate::function_registry::register_function(Arc::new(VarPop));
9961 crate::function_registry::register_function(Arc::new(PercentileInc));
9962 crate::function_registry::register_function(Arc::new(PercentileExc));
9963 crate::function_registry::register_function(Arc::new(QuartileInc));
9964 crate::function_registry::register_function(Arc::new(QuartileExc));
9965 crate::function_registry::register_function(Arc::new(RankEqFn));
9966 crate::function_registry::register_function(Arc::new(RankAvgFn));
9967 crate::function_registry::register_function(Arc::new(ModeSingleFn));
9968 crate::function_registry::register_function(Arc::new(ModeMultiFn));
9969 crate::function_registry::register_function(Arc::new(ProductFn));
9970 crate::function_registry::register_function(Arc::new(GeomeanFn));
9971 crate::function_registry::register_function(Arc::new(HarmeanFn));
9972 crate::function_registry::register_function(Arc::new(AvedevFn));
9973 crate::function_registry::register_function(Arc::new(DevsqFn));
9974 crate::function_registry::register_function(Arc::new(MaxIfsFn));
9975 crate::function_registry::register_function(Arc::new(MinIfsFn));
9976 crate::function_registry::register_function(Arc::new(TrimmeanFn));
9977 crate::function_registry::register_function(Arc::new(CorrelFn));
9978 crate::function_registry::register_function(Arc::new(SlopeFn));
9979 crate::function_registry::register_function(Arc::new(InterceptFn));
9980 // Covariance and correlation functions
9981 crate::function_registry::register_function(Arc::new(CovariancePFn));
9982 crate::function_registry::register_function(Arc::new(CovarianceSFn));
9983 crate::function_registry::register_function(Arc::new(PearsonFn));
9984 crate::function_registry::register_function(Arc::new(RsqFn));
9985 crate::function_registry::register_function(Arc::new(SteyxFn));
9986 crate::function_registry::register_function(Arc::new(SkewFn));
9987 crate::function_registry::register_function(Arc::new(KurtFn));
9988 crate::function_registry::register_function(Arc::new(FisherFn));
9989 crate::function_registry::register_function(Arc::new(FisherInvFn));
9990 // Statistical distributions
9991 crate::function_registry::register_function(Arc::new(NormSDistFn));
9992 crate::function_registry::register_function(Arc::new(NormSInvFn));
9993 crate::function_registry::register_function(Arc::new(NormDistFn));
9994 crate::function_registry::register_function(Arc::new(NormInvFn));
9995 crate::function_registry::register_function(Arc::new(LognormDistFn));
9996 crate::function_registry::register_function(Arc::new(LognormInvFn));
9997 crate::function_registry::register_function(Arc::new(PhiFn));
9998 crate::function_registry::register_function(Arc::new(GaussFn));
9999 crate::function_registry::register_function(Arc::new(StandardizeFn));
10000 crate::function_registry::register_function(Arc::new(TDistFn));
10001 crate::function_registry::register_function(Arc::new(TInvFn));
10002 crate::function_registry::register_function(Arc::new(ChisqDistFn));
10003 crate::function_registry::register_function(Arc::new(ChisqInvFn));
10004 crate::function_registry::register_function(Arc::new(FDistFn));
10005 crate::function_registry::register_function(Arc::new(FInvFn));
10006 // Discrete distributions
10007 crate::function_registry::register_function(Arc::new(BinomDistFn));
10008 crate::function_registry::register_function(Arc::new(PoissonDistFn));
10009 crate::function_registry::register_function(Arc::new(ExponDistFn));
10010 crate::function_registry::register_function(Arc::new(GammaDistFn));
10011 // Additional distributions
10012 crate::function_registry::register_function(Arc::new(WeibullDistFn));
10013 crate::function_registry::register_function(Arc::new(BetaDistFn));
10014 crate::function_registry::register_function(Arc::new(NegbinomDistFn));
10015 crate::function_registry::register_function(Arc::new(HypgeomDistFn));
10016 // Confidence intervals and hypothesis testing
10017 crate::function_registry::register_function(Arc::new(ConfidenceNormFn));
10018 crate::function_registry::register_function(Arc::new(ConfidenceTFn));
10019 crate::function_registry::register_function(Arc::new(ZTestFn));
10020 // Regression and trend functions
10021 crate::function_registry::register_function(Arc::new(TrendFn));
10022 crate::function_registry::register_function(Arc::new(GrowthFn));
10023 crate::function_registry::register_function(Arc::new(LogestFn));
10024 // Percent rank and frequency functions
10025 crate::function_registry::register_function(Arc::new(PercentRankIncFn));
10026 crate::function_registry::register_function(Arc::new(PercentRankExcFn));
10027 crate::function_registry::register_function(Arc::new(FrequencyFn));
10028 // Hypothesis testing functions
10029 crate::function_registry::register_function(Arc::new(TDist2TFn));
10030 crate::function_registry::register_function(Arc::new(TInv2TFn));
10031 crate::function_registry::register_function(Arc::new(TTestFn));
10032 crate::function_registry::register_function(Arc::new(FTestFn));
10033 crate::function_registry::register_function(Arc::new(ChisqTestFn));
10034 // FZ-PAR-01 batch
10035 crate::function_registry::register_function(Arc::new(AverageAFn));
10036 crate::function_registry::register_function(Arc::new(MaxAFn));
10037 crate::function_registry::register_function(Arc::new(MinAFn));
10038 crate::function_registry::register_function(Arc::new(StdevAFn));
10039 crate::function_registry::register_function(Arc::new(StdevPAFn));
10040 crate::function_registry::register_function(Arc::new(VarAFn));
10041 crate::function_registry::register_function(Arc::new(VarPAFn));
10042 crate::function_registry::register_function(Arc::new(SkewPFn));
10043 crate::function_registry::register_function(Arc::new(TDistRtFn));
10044 crate::function_registry::register_function(Arc::new(ChisqDistRtFn));
10045 crate::function_registry::register_function(Arc::new(ChisqInvRtFn));
10046 crate::function_registry::register_function(Arc::new(FDistRtFn));
10047 crate::function_registry::register_function(Arc::new(FInvRtFn));
10048 crate::function_registry::register_function(Arc::new(BetaInvFn));
10049 crate::function_registry::register_function(Arc::new(BinomDistRangeFn));
10050 crate::function_registry::register_function(Arc::new(BinomInvFn));
10051 crate::function_registry::register_function(Arc::new(GammaFn));
10052 crate::function_registry::register_function(Arc::new(GammaInvFn));
10053 crate::function_registry::register_function(Arc::new(GammaLnFn));
10054 crate::function_registry::register_function(Arc::new(GammaLnPreciseFn));
10055}
10056
10057#[cfg(test)]
10058mod tests_basic_stats {
10059 use super::*;
10060 use crate::test_workbook::TestWorkbook;
10061 use crate::traits::ArgumentHandle;
10062 use formualizer_common::LiteralValue;
10063 use formualizer_parse::parser::{ASTNode, ASTNodeType};
10064 fn interp(wb: &TestWorkbook) -> crate::interpreter::Interpreter<'_> {
10065 wb.interpreter()
10066 }
10067 fn arr(vals: Vec<f64>) -> ASTNode {
10068 ASTNode::new(
10069 ASTNodeType::Literal(LiteralValue::Array(vec![
10070 vals.into_iter().map(LiteralValue::Number).collect(),
10071 ])),
10072 None,
10073 )
10074 }
10075 #[test]
10076 fn median_even() {
10077 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(MEDIAN));
10078 let ctx = interp(&wb);
10079 let node = arr(vec![1.0, 3.0, 5.0, 7.0]);
10080 let f = ctx.context.get_function("", "MEDIAN").unwrap();
10081 let out = f
10082 .dispatch(
10083 &[ArgumentHandle::new(&node, &ctx)],
10084 &ctx.function_context(None),
10085 )
10086 .unwrap();
10087 assert_eq!(out, LiteralValue::Number(4.0));
10088 }
10089 #[test]
10090 fn median_odd() {
10091 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(MEDIAN));
10092 let ctx = interp(&wb);
10093 let node = arr(vec![1.0, 9.0, 5.0]);
10094 let f = ctx.context.get_function("", "MEDIAN").unwrap();
10095 let out = f
10096 .dispatch(
10097 &[ArgumentHandle::new(&node, &ctx)],
10098 &ctx.function_context(None),
10099 )
10100 .unwrap();
10101 assert_eq!(out, LiteralValue::Number(5.0));
10102 }
10103 #[test]
10104 fn large_basic() {
10105 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(LARGE));
10106 let ctx = interp(&wb);
10107 let nums = arr(vec![10.0, 20.0, 30.0]);
10108 let k = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(2.0)), None);
10109 let f = ctx.context.get_function("", "LARGE").unwrap();
10110 let out = f
10111 .dispatch(
10112 &[
10113 ArgumentHandle::new(&nums, &ctx),
10114 ArgumentHandle::new(&k, &ctx),
10115 ],
10116 &ctx.function_context(None),
10117 )
10118 .unwrap();
10119 assert_eq!(out, LiteralValue::Number(20.0));
10120 }
10121 #[test]
10122 fn small_basic() {
10123 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(SMALL));
10124 let ctx = interp(&wb);
10125 let nums = arr(vec![10.0, 20.0, 30.0]);
10126 let k = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(2.0)), None);
10127 let f = ctx.context.get_function("", "SMALL").unwrap();
10128 let out = f
10129 .dispatch(
10130 &[
10131 ArgumentHandle::new(&nums, &ctx),
10132 ArgumentHandle::new(&k, &ctx),
10133 ],
10134 &ctx.function_context(None),
10135 )
10136 .unwrap();
10137 assert_eq!(out, LiteralValue::Number(20.0));
10138 }
10139 #[test]
10140 fn percentile_inc_quarter() {
10141 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(PercentileInc));
10142 let ctx = interp(&wb);
10143 let nums = arr(vec![1.0, 2.0, 3.0, 4.0]);
10144 let p = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(0.25)), None);
10145 let f = ctx.context.get_function("", "PERCENTILE.INC").unwrap();
10146 match f
10147 .dispatch(
10148 &[
10149 ArgumentHandle::new(&nums, &ctx),
10150 ArgumentHandle::new(&p, &ctx),
10151 ],
10152 &ctx.function_context(None),
10153 )
10154 .unwrap()
10155 .into_literal()
10156 {
10157 LiteralValue::Number(v) => assert!((v - 1.75).abs() < 1e-9),
10158 other => panic!("unexpected {other:?}"),
10159 }
10160 }
10161 #[test]
10162 fn rank_eq_descending() {
10163 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(RankEqFn));
10164 let ctx = interp(&wb);
10165 // target 5 among {10,5,1} descending => ranks 1,2,3 => expect 2
10166 let target = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(5.0)), None);
10167 let arr_node = arr(vec![10.0, 5.0, 1.0]);
10168 let f = ctx.context.get_function("", "RANK.EQ").unwrap();
10169 let out = f
10170 .dispatch(
10171 &[
10172 ArgumentHandle::new(&target, &ctx),
10173 ArgumentHandle::new(&arr_node, &ctx),
10174 ],
10175 &ctx.function_context(None),
10176 )
10177 .unwrap();
10178 assert_eq!(out, LiteralValue::Number(2.0));
10179 }
10180 #[test]
10181 fn rank_eq_ascending_order_arg() {
10182 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(RankEqFn));
10183 let ctx = interp(&wb);
10184 // ascending order=1: array {1,5,10}; target 5 => rank 2
10185 let target = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(5.0)), None);
10186 let arr_node = arr(vec![1.0, 5.0, 10.0]);
10187 let order = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(1.0)), None);
10188 let f = ctx.context.get_function("", "RANK.EQ").unwrap();
10189 let out = f
10190 .dispatch(
10191 &[
10192 ArgumentHandle::new(&target, &ctx),
10193 ArgumentHandle::new(&arr_node, &ctx),
10194 ArgumentHandle::new(&order, &ctx),
10195 ],
10196 &ctx.function_context(None),
10197 )
10198 .unwrap();
10199 assert_eq!(out, LiteralValue::Number(2.0));
10200 }
10201 #[test]
10202 fn rank_avg_ties() {
10203 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(RankAvgFn));
10204 let ctx = interp(&wb);
10205 // descending array {5,5,1} target 5 positions 1 and 2 avg -> 1.5
10206 let target = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(5.0)), None);
10207 let arr_node = arr(vec![5.0, 5.0, 1.0]);
10208 let f = ctx.context.get_function("", "RANK.AVG").unwrap();
10209 let out = f
10210 .dispatch(
10211 &[
10212 ArgumentHandle::new(&target, &ctx),
10213 ArgumentHandle::new(&arr_node, &ctx),
10214 ],
10215 &ctx.function_context(None),
10216 )
10217 .unwrap()
10218 .into_literal();
10219 match out {
10220 LiteralValue::Number(v) => assert!((v - 1.5).abs() < 1e-12),
10221 other => panic!("expected number got {other:?}"),
10222 }
10223 }
10224 #[test]
10225 fn stdev_var_sample_population() {
10226 let wb = TestWorkbook::new()
10227 .with_function(std::sync::Arc::new(StdevSample))
10228 .with_function(std::sync::Arc::new(StdevPop))
10229 .with_function(std::sync::Arc::new(VarSample))
10230 .with_function(std::sync::Arc::new(VarPop));
10231 let ctx = interp(&wb);
10232 let arr_node = arr(vec![2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0]); // variance population = 4, sample = 4.571428...
10233 let stdev_p = ctx.context.get_function("", "STDEV.P").unwrap();
10234 let stdev_s = ctx.context.get_function("", "STDEV.S").unwrap();
10235 let var_p = ctx.context.get_function("", "VAR.P").unwrap();
10236 let var_s = ctx.context.get_function("", "VAR.S").unwrap();
10237 let args = [ArgumentHandle::new(&arr_node, &ctx)];
10238 match var_p
10239 .dispatch(&args, &ctx.function_context(None))
10240 .unwrap()
10241 .into_literal()
10242 {
10243 LiteralValue::Number(v) => assert!((v - 4.0).abs() < 1e-12),
10244 other => panic!("unexpected {other:?}"),
10245 }
10246 match var_s
10247 .dispatch(&args, &ctx.function_context(None))
10248 .unwrap()
10249 .into_literal()
10250 {
10251 LiteralValue::Number(v) => assert!((v - 4.571428571428571).abs() < 1e-9),
10252 other => panic!("unexpected {other:?}"),
10253 }
10254 match stdev_p
10255 .dispatch(&args, &ctx.function_context(None))
10256 .unwrap()
10257 .into_literal()
10258 {
10259 LiteralValue::Number(v) => assert!((v - 2.0).abs() < 1e-12),
10260 other => panic!("unexpected {other:?}"),
10261 }
10262 match stdev_s
10263 .dispatch(&args, &ctx.function_context(None))
10264 .unwrap()
10265 .into_literal()
10266 {
10267 LiteralValue::Number(v) => assert!((v - 2.138089935).abs() < 1e-9),
10268 other => panic!("unexpected {other:?}"),
10269 }
10270 }
10271 #[test]
10272 fn quartile_inc_exc() {
10273 let wb = TestWorkbook::new()
10274 .with_function(std::sync::Arc::new(QuartileInc))
10275 .with_function(std::sync::Arc::new(QuartileExc));
10276 let ctx = interp(&wb);
10277 let arr_node = arr(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
10278 let q1 = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(1.0)), None);
10279 let q2 = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(2.0)), None);
10280 let f_inc = ctx.context.get_function("", "QUARTILE.INC").unwrap();
10281 let f_exc = ctx.context.get_function("", "QUARTILE.EXC").unwrap();
10282 let arg_inc_q1 = [
10283 ArgumentHandle::new(&arr_node, &ctx),
10284 ArgumentHandle::new(&q1, &ctx),
10285 ];
10286 let arg_inc_q2 = [
10287 ArgumentHandle::new(&arr_node, &ctx),
10288 ArgumentHandle::new(&q2, &ctx),
10289 ];
10290 match f_inc
10291 .dispatch(&arg_inc_q1, &ctx.function_context(None))
10292 .unwrap()
10293 .into_literal()
10294 {
10295 LiteralValue::Number(v) => assert!((v - 2.0).abs() < 1e-12),
10296 other => panic!("unexpected {other:?}"),
10297 }
10298 match f_inc
10299 .dispatch(&arg_inc_q2, &ctx.function_context(None))
10300 .unwrap()
10301 .into_literal()
10302 {
10303 LiteralValue::Number(v) => assert!((v - 3.0).abs() < 1e-12),
10304 other => panic!("unexpected {other:?}"),
10305 }
10306 // QUARTILE.EXC Q1 for 5-point set uses exclusive percentile => 1.5
10307 match f_exc
10308 .dispatch(&arg_inc_q1, &ctx.function_context(None))
10309 .unwrap()
10310 .into_literal()
10311 {
10312 LiteralValue::Number(v) => assert!((v - 1.5).abs() < 1e-12),
10313 other => panic!("unexpected {other:?}"),
10314 }
10315 match f_exc
10316 .dispatch(&arg_inc_q2, &ctx.function_context(None))
10317 .unwrap()
10318 .into_literal()
10319 {
10320 LiteralValue::Number(v) => assert!((v - 3.0).abs() < 1e-12),
10321 other => panic!("unexpected {other:?}"),
10322 }
10323 }
10324
10325 // --- eval()/dispatch equivalence tests for variance / stdev ---
10326 #[test]
10327 fn fold_equivalence_var_stdev() {
10328 use crate::function::Function as _; // trait import
10329 let wb = TestWorkbook::new()
10330 .with_function(std::sync::Arc::new(VarSample))
10331 .with_function(std::sync::Arc::new(VarPop))
10332 .with_function(std::sync::Arc::new(StdevSample))
10333 .with_function(std::sync::Arc::new(StdevPop));
10334 let ctx = interp(&wb);
10335 let arr_node = arr(vec![1.0, 2.0, 5.0, 5.0, 9.0]);
10336 let args = [ArgumentHandle::new(&arr_node, &ctx)];
10337
10338 let var_s_fn = VarSample; // concrete instance to call eval()
10339 let var_p_fn = VarPop;
10340 let stdev_s_fn = StdevSample;
10341 let stdev_p_fn = StdevPop;
10342
10343 let fctx = ctx.function_context(None);
10344 // Dispatch results (will use fold path)
10345 let disp_var_s = ctx
10346 .context
10347 .get_function("", "VAR.S")
10348 .unwrap()
10349 .dispatch(&args, &fctx)
10350 .unwrap()
10351 .into_literal();
10352 let disp_var_p = ctx
10353 .context
10354 .get_function("", "VAR.P")
10355 .unwrap()
10356 .dispatch(&args, &fctx)
10357 .unwrap()
10358 .into_literal();
10359 let disp_stdev_s = ctx
10360 .context
10361 .get_function("", "STDEV.S")
10362 .unwrap()
10363 .dispatch(&args, &fctx)
10364 .unwrap()
10365 .into_literal();
10366 let disp_stdev_p = ctx
10367 .context
10368 .get_function("", "STDEV.P")
10369 .unwrap()
10370 .dispatch(&args, &fctx)
10371 .unwrap()
10372 .into_literal();
10373
10374 // Scalar path results
10375 let scalar_var_s = var_s_fn.eval(&args, &fctx).unwrap().into_literal();
10376 let scalar_var_p = var_p_fn.eval(&args, &fctx).unwrap().into_literal();
10377 let scalar_stdev_s = stdev_s_fn.eval(&args, &fctx).unwrap().into_literal();
10378 let scalar_stdev_p = stdev_p_fn.eval(&args, &fctx).unwrap().into_literal();
10379
10380 fn assert_close(a: &LiteralValue, b: &LiteralValue) {
10381 match (a, b) {
10382 (LiteralValue::Number(x), LiteralValue::Number(y)) => {
10383 assert!((x - y).abs() < 1e-12, "mismatch {x} vs {y}")
10384 }
10385 _ => assert_eq!(a, b),
10386 }
10387 }
10388 assert_close(&disp_var_s, &scalar_var_s);
10389 assert_close(&disp_var_p, &scalar_var_p);
10390 assert_close(&disp_stdev_s, &scalar_stdev_s);
10391 assert_close(&disp_stdev_p, &scalar_stdev_p);
10392 }
10393
10394 #[test]
10395 fn fold_equivalence_edge_cases() {
10396 use crate::function::Function as _;
10397 let wb = TestWorkbook::new()
10398 .with_function(std::sync::Arc::new(VarSample))
10399 .with_function(std::sync::Arc::new(VarPop))
10400 .with_function(std::sync::Arc::new(StdevSample))
10401 .with_function(std::sync::Arc::new(StdevPop));
10402 let ctx = interp(&wb);
10403 // Single numeric element -> sample variance/div0, population variance 0
10404 let single = arr(vec![42.0]);
10405 let args_single = [ArgumentHandle::new(&single, &ctx)];
10406 let fctx = ctx.function_context(None);
10407 let disp_var_s = ctx
10408 .context
10409 .get_function("", "VAR.S")
10410 .unwrap()
10411 .dispatch(&args_single, &fctx)
10412 .unwrap();
10413 let scalar_var_s = VarSample.eval(&args_single, &fctx).unwrap().into_literal();
10414 assert_eq!(disp_var_s, scalar_var_s);
10415 let disp_var_p = ctx
10416 .context
10417 .get_function("", "VAR.P")
10418 .unwrap()
10419 .dispatch(&args_single, &fctx)
10420 .unwrap();
10421 let scalar_var_p = VarPop.eval(&args_single, &fctx).unwrap().into_literal();
10422 assert_eq!(disp_var_p, scalar_var_p);
10423 let disp_stdev_p = ctx
10424 .context
10425 .get_function("", "STDEV.P")
10426 .unwrap()
10427 .dispatch(&args_single, &fctx)
10428 .unwrap();
10429 let scalar_stdev_p = StdevPop.eval(&args_single, &fctx).unwrap().into_literal();
10430 assert_eq!(disp_stdev_p, scalar_stdev_p);
10431 let disp_stdev_s = ctx
10432 .context
10433 .get_function("", "STDEV.S")
10434 .unwrap()
10435 .dispatch(&args_single, &fctx)
10436 .unwrap();
10437 let scalar_stdev_s = StdevSample
10438 .eval(&args_single, &fctx)
10439 .unwrap()
10440 .into_literal();
10441 assert_eq!(disp_stdev_s, scalar_stdev_s);
10442 }
10443
10444 #[test]
10445 fn legacy_aliases_match_modern() {
10446 let wb = TestWorkbook::new()
10447 .with_function(std::sync::Arc::new(PercentileInc))
10448 .with_function(std::sync::Arc::new(QuartileInc))
10449 .with_function(std::sync::Arc::new(RankEqFn));
10450 let ctx = interp(&wb);
10451 let arr_node = arr(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
10452 let p = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(0.4)), None);
10453 let q2 = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(2.0)), None);
10454 let target = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(4.0)), None);
10455 let args_p = [
10456 ArgumentHandle::new(&arr_node, &ctx),
10457 ArgumentHandle::new(&p, &ctx),
10458 ];
10459 let args_q = [
10460 ArgumentHandle::new(&arr_node, &ctx),
10461 ArgumentHandle::new(&q2, &ctx),
10462 ];
10463 let args_rank = [
10464 ArgumentHandle::new(&target, &ctx),
10465 ArgumentHandle::new(&arr_node, &ctx),
10466 ];
10467 let modern_p = ctx
10468 .context
10469 .get_function("", "PERCENTILE.INC")
10470 .unwrap()
10471 .dispatch(&args_p, &ctx.function_context(None))
10472 .unwrap()
10473 .into_literal();
10474 let legacy_p = ctx
10475 .context
10476 .get_function("", "PERCENTILE")
10477 .unwrap()
10478 .dispatch(&args_p, &ctx.function_context(None))
10479 .unwrap()
10480 .into_literal();
10481 assert_eq!(modern_p, legacy_p);
10482 let modern_q = ctx
10483 .context
10484 .get_function("", "QUARTILE.INC")
10485 .unwrap()
10486 .dispatch(&args_q, &ctx.function_context(None))
10487 .unwrap()
10488 .into_literal();
10489 let legacy_q = ctx
10490 .context
10491 .get_function("", "QUARTILE")
10492 .unwrap()
10493 .dispatch(&args_q, &ctx.function_context(None))
10494 .unwrap()
10495 .into_literal();
10496 assert_eq!(modern_q, legacy_q);
10497 let modern_rank = ctx
10498 .context
10499 .get_function("", "RANK.EQ")
10500 .unwrap()
10501 .dispatch(&args_rank, &ctx.function_context(None))
10502 .unwrap()
10503 .into_literal();
10504 let legacy_rank = ctx
10505 .context
10506 .get_function("", "RANK")
10507 .unwrap()
10508 .dispatch(&args_rank, &ctx.function_context(None))
10509 .unwrap()
10510 .into_literal();
10511 assert_eq!(modern_rank, legacy_rank);
10512 }
10513
10514 #[test]
10515 fn mode_single_basic_and_alias() {
10516 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(ModeSingleFn));
10517 let ctx = interp(&wb);
10518 let arr_node = arr(vec![5.0, 2.0, 2.0, 3.0, 3.0, 3.0]);
10519 let args = [ArgumentHandle::new(&arr_node, &ctx)];
10520 let mode_sngl = ctx
10521 .context
10522 .get_function("", "MODE.SNGL")
10523 .unwrap()
10524 .dispatch(&args, &ctx.function_context(None))
10525 .unwrap()
10526 .into_literal();
10527 assert_eq!(mode_sngl, LiteralValue::Number(3.0));
10528 let mode_alias = ctx
10529 .context
10530 .get_function("", "MODE")
10531 .unwrap()
10532 .dispatch(&args, &ctx.function_context(None))
10533 .unwrap()
10534 .into_literal();
10535 assert_eq!(mode_alias, mode_sngl);
10536 }
10537
10538 #[test]
10539 fn mode_single_no_duplicates() {
10540 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(ModeSingleFn));
10541 let ctx = interp(&wb);
10542 let arr_node = arr(vec![1.0, 2.0, 3.0]);
10543 let args = [ArgumentHandle::new(&arr_node, &ctx)];
10544 let out = ctx
10545 .context
10546 .get_function("", "MODE.SNGL")
10547 .unwrap()
10548 .dispatch(&args, &ctx.function_context(None))
10549 .unwrap()
10550 .into_literal();
10551 match out {
10552 LiteralValue::Error(e) => assert!(e.to_string().contains("#N/A")),
10553 _ => panic!("expected #N/A"),
10554 }
10555 }
10556
10557 #[test]
10558 fn mode_multi_basic() {
10559 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(ModeMultiFn));
10560 let ctx = interp(&wb);
10561 let arr_node = arr(vec![2.0, 3.0, 2.0, 4.0, 3.0, 5.0, 2.0, 3.0]);
10562 let args = [ArgumentHandle::new(&arr_node, &ctx)];
10563 let out = ctx
10564 .context
10565 .get_function("", "MODE.MULT")
10566 .unwrap()
10567 .dispatch(&args, &ctx.function_context(None))
10568 .unwrap()
10569 .into_literal();
10570 let expected = LiteralValue::Array(vec![
10571 vec![LiteralValue::Number(2.0)],
10572 vec![LiteralValue::Number(3.0)],
10573 ]);
10574 assert_eq!(out, expected);
10575 }
10576
10577 #[test]
10578 fn large_small_fold_vs_scalar() {
10579 let wb = TestWorkbook::new()
10580 .with_function(std::sync::Arc::new(LARGE))
10581 .with_function(std::sync::Arc::new(SMALL));
10582 let ctx = interp(&wb);
10583 let arr_node = arr(vec![10.0, 5.0, 7.0, 12.0, 9.0]);
10584 let k_node = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(2.0)), None);
10585 let args = [
10586 ArgumentHandle::new(&arr_node, &ctx),
10587 ArgumentHandle::new(&k_node, &ctx),
10588 ];
10589 let f_large = ctx.context.get_function("", "LARGE").unwrap();
10590 let disp_large = f_large
10591 .dispatch(&args, &ctx.function_context(None))
10592 .unwrap()
10593 .into_literal();
10594 let scalar_large = LARGE
10595 .eval(&args, &ctx.function_context(None))
10596 .unwrap()
10597 .into_literal();
10598 assert_eq!(disp_large, scalar_large);
10599 let f_small = ctx.context.get_function("", "SMALL").unwrap();
10600 let disp_small = f_small
10601 .dispatch(&args, &ctx.function_context(None))
10602 .unwrap()
10603 .into_literal();
10604 let scalar_small = SMALL
10605 .eval(&args, &ctx.function_context(None))
10606 .unwrap()
10607 .into_literal();
10608 assert_eq!(disp_small, scalar_small);
10609 }
10610
10611 #[test]
10612 fn mode_fold_vs_scalar() {
10613 let wb = TestWorkbook::new()
10614 .with_function(std::sync::Arc::new(ModeSingleFn))
10615 .with_function(std::sync::Arc::new(ModeMultiFn));
10616 let ctx = interp(&wb);
10617 let arr_node = arr(vec![2.0, 3.0, 2.0, 4.0, 3.0, 3.0, 2.0]);
10618 let args = [ArgumentHandle::new(&arr_node, &ctx)];
10619 let f_single = ctx.context.get_function("", "MODE.SNGL").unwrap();
10620 let disp_single = f_single
10621 .dispatch(&args, &ctx.function_context(None))
10622 .unwrap()
10623 .into_literal();
10624 let scalar_single = ModeSingleFn
10625 .eval(&args, &ctx.function_context(None))
10626 .unwrap()
10627 .into_literal();
10628 assert_eq!(disp_single, scalar_single);
10629 let f_multi = ctx.context.get_function("", "MODE.MULT").unwrap();
10630 let disp_multi = f_multi
10631 .dispatch(&args, &ctx.function_context(None))
10632 .unwrap()
10633 .into_literal();
10634 let scalar_multi = ModeMultiFn
10635 .eval(&args, &ctx.function_context(None))
10636 .unwrap()
10637 .into_literal();
10638 assert_eq!(disp_multi, scalar_multi);
10639 }
10640
10641 #[test]
10642 fn median_fold_vs_scalar_even() {
10643 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(MEDIAN));
10644 let ctx = interp(&wb);
10645 let arr_node = arr(vec![7.0, 1.0, 9.0, 5.0]); // sorted: 1,5,7,9 median=(5+7)/2=6
10646 let args = [ArgumentHandle::new(&arr_node, &ctx)];
10647 let f_med = ctx.context.get_function("", "MEDIAN").unwrap();
10648 let disp = f_med
10649 .dispatch(&args, &ctx.function_context(None))
10650 .unwrap()
10651 .into_literal();
10652 let scalar = MEDIAN
10653 .eval(&args, &ctx.function_context(None))
10654 .unwrap()
10655 .into_literal();
10656 assert_eq!(disp, scalar);
10657 assert_eq!(disp, LiteralValue::Number(6.0));
10658 }
10659
10660 #[test]
10661 fn median_fold_vs_scalar_odd() {
10662 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(MEDIAN));
10663 let ctx = interp(&wb);
10664 let arr_node = arr(vec![9.0, 2.0, 5.0]); // sorted 2,5,9 median=5
10665 let args = [ArgumentHandle::new(&arr_node, &ctx)];
10666 let f_med = ctx.context.get_function("", "MEDIAN").unwrap();
10667 let disp = f_med
10668 .dispatch(&args, &ctx.function_context(None))
10669 .unwrap()
10670 .into_literal();
10671 let scalar = MEDIAN
10672 .eval(&args, &ctx.function_context(None))
10673 .unwrap()
10674 .into_literal();
10675 assert_eq!(disp, scalar);
10676 assert_eq!(disp, LiteralValue::Number(5.0));
10677 }
10678
10679 // Newly added edge case tests for statistical semantics.
10680 #[test]
10681 fn percentile_inc_edges() {
10682 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(PercentileInc));
10683 let ctx = interp(&wb);
10684 let arr_node = arr(vec![10.0, 20.0, 30.0, 40.0]);
10685 let p0 = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(0.0)), None);
10686 let p1 = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(1.0)), None);
10687 let f = ctx.context.get_function("", "PERCENTILE.INC").unwrap();
10688 let args0 = [
10689 ArgumentHandle::new(&arr_node, &ctx),
10690 ArgumentHandle::new(&p0, &ctx),
10691 ];
10692 let args1 = [
10693 ArgumentHandle::new(&arr_node, &ctx),
10694 ArgumentHandle::new(&p1, &ctx),
10695 ];
10696 assert_eq!(
10697 f.dispatch(&args0, &ctx.function_context(None))
10698 .unwrap()
10699 .into_literal(),
10700 LiteralValue::Number(10.0)
10701 );
10702 assert_eq!(
10703 f.dispatch(&args1, &ctx.function_context(None))
10704 .unwrap()
10705 .into_literal(),
10706 LiteralValue::Number(40.0)
10707 );
10708 }
10709
10710 #[test]
10711 fn percentile_exc_invalid() {
10712 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(PercentileExc));
10713 let ctx = interp(&wb);
10714 let arr_node = arr(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
10715 let p_bad0 = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(0.0)), None);
10716 let p_bad1 = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(1.0)), None);
10717 let f = ctx.context.get_function("", "PERCENTILE.EXC").unwrap();
10718 for bad in [&p_bad0, &p_bad1] {
10719 let args = [
10720 ArgumentHandle::new(&arr_node, &ctx),
10721 ArgumentHandle::new(bad, &ctx),
10722 ];
10723 match f
10724 .dispatch(&args, &ctx.function_context(None))
10725 .unwrap()
10726 .into_literal()
10727 {
10728 LiteralValue::Error(e) => assert!(e.to_string().contains("#NUM!")),
10729 other => panic!("expected #NUM! got {other:?}"),
10730 }
10731 }
10732 }
10733
10734 #[test]
10735 fn quartile_invalids() {
10736 let wb = TestWorkbook::new()
10737 .with_function(std::sync::Arc::new(QuartileInc))
10738 .with_function(std::sync::Arc::new(QuartileExc));
10739 let ctx = interp(&wb);
10740 let arr_node = arr(vec![1.0, 2.0, 3.0]);
10741 // QUARTILE.INC invalid q=5
10742 let q5 = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(5.0)), None);
10743 let args_bad_inc = [
10744 ArgumentHandle::new(&arr_node, &ctx),
10745 ArgumentHandle::new(&q5, &ctx),
10746 ];
10747 match ctx
10748 .context
10749 .get_function("", "QUARTILE.INC")
10750 .unwrap()
10751 .dispatch(&args_bad_inc, &ctx.function_context(None))
10752 .unwrap()
10753 .into_literal()
10754 {
10755 LiteralValue::Error(e) => assert!(e.to_string().contains("#NUM!")),
10756 other => panic!("expected #NUM! {other:?}"),
10757 }
10758 // QUARTILE.EXC invalid q=0
10759 let q0 = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(0.0)), None);
10760 let args_bad_exc = [
10761 ArgumentHandle::new(&arr_node, &ctx),
10762 ArgumentHandle::new(&q0, &ctx),
10763 ];
10764 match ctx
10765 .context
10766 .get_function("", "QUARTILE.EXC")
10767 .unwrap()
10768 .dispatch(&args_bad_exc, &ctx.function_context(None))
10769 .unwrap()
10770 .into_literal()
10771 {
10772 LiteralValue::Error(e) => assert!(e.to_string().contains("#NUM!")),
10773 other => panic!("expected #NUM! {other:?}"),
10774 }
10775 }
10776
10777 #[test]
10778 fn rank_target_not_found() {
10779 let wb = TestWorkbook::new()
10780 .with_function(std::sync::Arc::new(RankEqFn))
10781 .with_function(std::sync::Arc::new(RankAvgFn));
10782 let ctx = interp(&wb);
10783 let arr_node = arr(vec![1.0, 2.0, 3.0]);
10784 let target = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(4.0)), None);
10785 let args = [
10786 ArgumentHandle::new(&target, &ctx),
10787 ArgumentHandle::new(&arr_node, &ctx),
10788 ];
10789 for name in ["RANK.EQ", "RANK.AVG"] {
10790 match ctx
10791 .context
10792 .get_function("", name)
10793 .unwrap()
10794 .dispatch(&args, &ctx.function_context(None))
10795 .unwrap()
10796 .into_literal()
10797 {
10798 LiteralValue::Error(e) => assert!(e.to_string().contains("#N/A")),
10799 other => panic!("expected #N/A {other:?}"),
10800 }
10801 }
10802 }
10803
10804 #[test]
10805 fn mode_mult_ordering() {
10806 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(ModeMultiFn));
10807 let ctx = interp(&wb);
10808 // Two modes with same frequency; ensure ascending ordering in array result
10809 let arr_node = arr(vec![5.0, 2.0, 2.0, 5.0, 3.0, 7.0, 5.0, 2.0]); // 2 and 5 appear 4 times each
10810 let args = [ArgumentHandle::new(&arr_node, &ctx)];
10811 let out = ctx
10812 .context
10813 .get_function("", "MODE.MULT")
10814 .unwrap()
10815 .dispatch(&args, &ctx.function_context(None))
10816 .unwrap()
10817 .into_literal();
10818 match out {
10819 LiteralValue::Array(rows) => {
10820 let vals: Vec<f64> = rows
10821 .into_iter()
10822 .map(|r| {
10823 if let LiteralValue::Number(n) = r[0] {
10824 n
10825 } else {
10826 panic!("expected number")
10827 }
10828 })
10829 .collect();
10830 assert_eq!(vals, vec![2.0, 5.0]);
10831 }
10832 other => panic!("expected array {other:?}"),
10833 }
10834 }
10835
10836 #[test]
10837 fn boolean_and_text_in_range_are_ignored() {
10838 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(StdevPop));
10839 let ctx = interp(&wb);
10840 let mixed = ASTNode::new(
10841 ASTNodeType::Literal(LiteralValue::Array(vec![vec![
10842 LiteralValue::Number(1.0),
10843 LiteralValue::Text("ABC".into()),
10844 LiteralValue::Boolean(true),
10845 LiteralValue::Number(4.0),
10846 ]])),
10847 None,
10848 );
10849 let f = ctx.context.get_function("", "STDEV.P").unwrap();
10850 let out = f
10851 .dispatch(
10852 &[ArgumentHandle::new(&mixed, &ctx)],
10853 &ctx.function_context(None),
10854 )
10855 .unwrap()
10856 .into_literal();
10857 // NOTE: Inline array literal is treated as a direct scalar argument (not a range reference),
10858 // so boolean TRUE is coerced to 1. Dataset becomes {1,1,4}; population stdev = sqrt(6/3)=sqrt(2).
10859 match out {
10860 LiteralValue::Number(v) => {
10861 assert!((v - 2f64.sqrt()).abs() < 1e-12, "expected sqrt(2) got {v}")
10862 }
10863 other => panic!("unexpected {other:?}"),
10864 }
10865 }
10866
10867 #[test]
10868 fn boolean_direct_arg_coerces() {
10869 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(StdevPop));
10870 let ctx = interp(&wb);
10871 let one = ASTNode::new(ASTNodeType::Literal(LiteralValue::Number(1.0)), None);
10872 let t = ASTNode::new(ASTNodeType::Literal(LiteralValue::Boolean(true)), None);
10873 let f = ctx.context.get_function("", "STDEV.P").unwrap();
10874 let args = [
10875 ArgumentHandle::new(&one, &ctx),
10876 ArgumentHandle::new(&t, &ctx),
10877 ];
10878 let out = f
10879 .dispatch(&args, &ctx.function_context(None))
10880 .unwrap()
10881 .into_literal();
10882 assert_eq!(out, LiteralValue::Number(0.0));
10883 }
10884}