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