formualizer_eval/builtins/math/combinatorics.rs
1use super::super::utils::{ARG_NUM_LENIENT_ONE, ARG_NUM_LENIENT_TWO, coerce_num};
2use crate::args::ArgSchema;
3use crate::function::Function;
4use crate::traits::{ArgumentHandle, CalcValue, FunctionContext};
5use formualizer_common::{ExcelError, LiteralValue};
6use formualizer_macros::func_caps;
7
8#[derive(Debug)]
9pub struct FactFn;
10/// Returns the factorial of a non-negative integer.
11///
12/// `FACT` truncates fractional inputs toward zero before computing the factorial.
13///
14/// # Remarks
15/// - Non-numeric values that cannot be coerced return `#VALUE!`.
16/// - Negative inputs return `#NUM!`.
17/// - Results above `170!` overflow Excel-compatible limits and return `#NUM!`.
18///
19/// # Examples
20///
21/// ```yaml,sandbox
22/// title: "Basic factorial"
23/// formula: "=FACT(5)"
24/// expected: 120
25/// ```
26///
27/// ```yaml,sandbox
28/// title: "Fractional input is truncated"
29/// formula: "=FACT(5.9)"
30/// expected: 120
31/// ```
32///
33/// ```yaml,sandbox
34/// title: "Negative input returns numeric error"
35/// formula: "=FACT(-1)"
36/// expected: "#NUM!"
37/// ```
38///
39/// ```yaml,docs
40/// related:
41/// - FACTDOUBLE
42/// - GAMMALN
43/// - COMBIN
44/// faq:
45/// - q: "Why does FACT(171) return #NUM!?"
46/// a: "Excel-compatible factorial support is capped at 170!; larger values overflow and return #NUM!."
47/// - q: "Does FACT round fractional inputs?"
48/// a: "No. It truncates toward zero before computing the factorial."
49/// ```
50///
51/// [formualizer-docgen:schema:start]
52/// Name: FACT
53/// Type: FactFn
54/// Min args: 1
55/// Max args: 1
56/// Variadic: false
57/// Signature: FACT(arg1: number@scalar)
58/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
59/// Caps: PURE
60/// [formualizer-docgen:schema:end]
61impl Function for FactFn {
62 func_caps!(PURE);
63 fn name(&self) -> &'static str {
64 "FACT"
65 }
66 fn min_args(&self) -> usize {
67 1
68 }
69 fn arg_schema(&self) -> &'static [ArgSchema] {
70 &ARG_NUM_LENIENT_ONE[..]
71 }
72 fn eval<'a, 'b, 'c>(
73 &self,
74 args: &'c [ArgumentHandle<'a, 'b>],
75 _: &dyn FunctionContext<'b>,
76 ) -> Result<CalcValue<'b>, ExcelError> {
77 let v = args[0].value()?.into_literal();
78 let n = match v {
79 LiteralValue::Error(e) => return Ok(CalcValue::Scalar(LiteralValue::Error(e))),
80 other => coerce_num(&other)?,
81 };
82
83 // Excel truncates to integer
84 let n = n.trunc() as i64;
85
86 if n < 0 {
87 return Ok(CalcValue::Scalar(
88 LiteralValue::Error(ExcelError::new_num()),
89 ));
90 }
91
92 // Factorial calculation (Excel supports up to 170!)
93 if n > 170 {
94 return Ok(CalcValue::Scalar(
95 LiteralValue::Error(ExcelError::new_num()),
96 ));
97 }
98
99 let mut result = 1.0_f64;
100 for i in 2..=(n as u64) {
101 result *= i as f64;
102 }
103
104 Ok(CalcValue::Scalar(LiteralValue::Number(result)))
105 }
106}
107
108#[derive(Debug)]
109pub struct GcdFn;
110/// Returns the greatest common divisor of one or more integers.
111///
112/// `GCD` truncates each argument toward zero before calculating the divisor.
113///
114/// # Remarks
115/// - Inputs must be between `0` and `9.99999999e9` after truncation, or `#NUM!` is returned.
116/// - Negative values return `#NUM!`.
117/// - Any argument error propagates immediately.
118///
119/// # Examples
120///
121/// ```yaml,sandbox
122/// title: "Greatest common divisor of two numbers"
123/// formula: "=GCD(24, 36)"
124/// expected: 12
125/// ```
126///
127/// ```yaml,sandbox
128/// title: "Variadic and fractional arguments"
129/// formula: "=GCD(18.9, 6, 30)"
130/// expected: 6
131/// ```
132///
133/// ```yaml,sandbox
134/// title: "Negative values are invalid"
135/// formula: "=GCD(-2, 4)"
136/// expected: "#NUM!"
137/// ```
138///
139/// ```yaml,docs
140/// related:
141/// - LCM
142/// - MOD
143/// - QUOTIENT
144/// faq:
145/// - q: "What happens to decimal inputs in GCD?"
146/// a: "Each argument is truncated toward zero before the divisor is computed."
147/// - q: "When does GCD return #NUM!?"
148/// a: "Negative values or values outside the supported bound return #NUM!."
149/// ```
150///
151/// [formualizer-docgen:schema:start]
152/// Name: GCD
153/// Type: GcdFn
154/// Min args: 1
155/// Max args: variadic
156/// Variadic: true
157/// Signature: GCD(arg1: number@scalar, arg2...: number@scalar)
158/// 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}
159/// Caps: PURE
160/// [formualizer-docgen:schema:end]
161impl Function for GcdFn {
162 func_caps!(PURE);
163 fn name(&self) -> &'static str {
164 "GCD"
165 }
166 fn min_args(&self) -> usize {
167 1
168 }
169 fn variadic(&self) -> bool {
170 true
171 }
172 fn arg_schema(&self) -> &'static [ArgSchema] {
173 &ARG_NUM_LENIENT_TWO[..]
174 }
175 fn eval<'a, 'b, 'c>(
176 &self,
177 args: &'c [ArgumentHandle<'a, 'b>],
178 _: &dyn FunctionContext<'b>,
179 ) -> Result<CalcValue<'b>, ExcelError> {
180 fn gcd(a: u64, b: u64) -> u64 {
181 if b == 0 { a } else { gcd(b, a % b) }
182 }
183
184 let mut result: Option<u64> = None;
185
186 for arg in args {
187 let v = arg.value()?.into_literal();
188 let n = match v {
189 LiteralValue::Error(e) => return Ok(CalcValue::Scalar(LiteralValue::Error(e))),
190 other => coerce_num(&other)?,
191 };
192
193 // Excel truncates and requires non-negative
194 let n = n.trunc();
195 if !(0.0..=9.99999999e9).contains(&n) {
196 return Ok(CalcValue::Scalar(
197 LiteralValue::Error(ExcelError::new_num()),
198 ));
199 }
200 let n = n as u64;
201
202 result = Some(match result {
203 None => n,
204 Some(r) => gcd(r, n),
205 });
206 }
207
208 Ok(CalcValue::Scalar(LiteralValue::Number(
209 result.unwrap_or(0) as f64
210 )))
211 }
212}
213
214#[derive(Debug)]
215pub struct LcmFn;
216/// Returns the least common multiple of one or more integers.
217///
218/// `LCM` truncates fractional arguments toward zero and combines values iteratively.
219///
220/// # Remarks
221/// - Inputs must be non-negative and within the supported Excel-compatible range.
222/// - If any input is `0`, the resulting least common multiple is `0`.
223/// - Any argument error propagates immediately.
224///
225/// # Examples
226///
227/// ```yaml,sandbox
228/// title: "Least common multiple for two integers"
229/// formula: "=LCM(4, 6)"
230/// expected: 12
231/// ```
232///
233/// ```yaml,sandbox
234/// title: "Fractional values are truncated"
235/// formula: "=LCM(6.8, 8.2)"
236/// expected: 24
237/// ```
238///
239/// ```yaml,sandbox
240/// title: "Negative values return numeric error"
241/// formula: "=LCM(-3, 6)"
242/// expected: "#NUM!"
243/// ```
244///
245/// ```yaml,docs
246/// related:
247/// - GCD
248/// - MOD
249/// - MROUND
250/// faq:
251/// - q: "Why does LCM return 0 when one argument is 0?"
252/// a: "LCM is defined as 0 if any truncated input is 0 in this implementation."
253/// - q: "Are fractional inputs accepted?"
254/// a: "Yes, but they are truncated toward zero before the LCM calculation."
255/// ```
256///
257/// [formualizer-docgen:schema:start]
258/// Name: LCM
259/// Type: LcmFn
260/// Min args: 1
261/// Max args: variadic
262/// Variadic: true
263/// Signature: LCM(arg1: number@scalar, arg2...: number@scalar)
264/// 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}
265/// Caps: PURE
266/// [formualizer-docgen:schema:end]
267impl Function for LcmFn {
268 func_caps!(PURE);
269 fn name(&self) -> &'static str {
270 "LCM"
271 }
272 fn min_args(&self) -> usize {
273 1
274 }
275 fn variadic(&self) -> bool {
276 true
277 }
278 fn arg_schema(&self) -> &'static [ArgSchema] {
279 &ARG_NUM_LENIENT_TWO[..]
280 }
281 fn eval<'a, 'b, 'c>(
282 &self,
283 args: &'c [ArgumentHandle<'a, 'b>],
284 _: &dyn FunctionContext<'b>,
285 ) -> Result<CalcValue<'b>, ExcelError> {
286 fn gcd(a: u64, b: u64) -> u64 {
287 if b == 0 { a } else { gcd(b, a % b) }
288 }
289 fn lcm(a: u64, b: u64) -> u64 {
290 if a == 0 || b == 0 {
291 0
292 } else {
293 (a / gcd(a, b)) * b
294 }
295 }
296
297 let mut result: Option<u64> = None;
298
299 for arg in args {
300 let v = arg.value()?.into_literal();
301 let n = match v {
302 LiteralValue::Error(e) => return Ok(CalcValue::Scalar(LiteralValue::Error(e))),
303 other => coerce_num(&other)?,
304 };
305
306 let n = n.trunc();
307 if !(0.0..=9.99999999e9).contains(&n) {
308 return Ok(CalcValue::Scalar(
309 LiteralValue::Error(ExcelError::new_num()),
310 ));
311 }
312 let n = n as u64;
313
314 result = Some(match result {
315 None => n,
316 Some(r) => lcm(r, n),
317 });
318 }
319
320 Ok(CalcValue::Scalar(LiteralValue::Number(
321 result.unwrap_or(0) as f64
322 )))
323 }
324}
325
326#[derive(Debug)]
327pub struct CombinFn;
328/// Returns the number of combinations for selecting `k` items from `n`.
329///
330/// `COMBIN` evaluates `n` choose `k` using truncated integer inputs.
331///
332/// # Remarks
333/// - Fractional inputs are truncated toward zero before evaluation.
334/// - If `n < 0`, `k < 0`, or `k > n`, the function returns `#NUM!`.
335/// - Argument errors propagate directly.
336///
337/// # Examples
338///
339/// ```yaml,sandbox
340/// title: "Basic combinations"
341/// formula: "=COMBIN(5, 2)"
342/// expected: 10
343/// ```
344///
345/// ```yaml,sandbox
346/// title: "Fractional arguments are truncated"
347/// formula: "=COMBIN(6.9, 3.2)"
348/// expected: 20
349/// ```
350///
351/// ```yaml,sandbox
352/// title: "Invalid k returns numeric error"
353/// formula: "=COMBIN(3, 5)"
354/// expected: "#NUM!"
355/// ```
356///
357/// ```yaml,docs
358/// related:
359/// - COMBINA
360/// - PERMUT
361/// - FACT
362/// faq:
363/// - q: "When does COMBIN return #NUM!?"
364/// a: "It returns #NUM! if n or k is negative, or if k is greater than n after truncation."
365/// - q: "Can COMBIN take non-integer values?"
366/// a: "Yes, but both inputs are truncated toward zero before evaluation."
367/// ```
368///
369/// [formualizer-docgen:schema:start]
370/// Name: COMBIN
371/// Type: CombinFn
372/// Min args: 2
373/// Max args: 2
374/// Variadic: false
375/// Signature: COMBIN(arg1: number@scalar, arg2: number@scalar)
376/// 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}
377/// Caps: PURE
378/// [formualizer-docgen:schema:end]
379impl Function for CombinFn {
380 func_caps!(PURE);
381 fn name(&self) -> &'static str {
382 "COMBIN"
383 }
384 fn min_args(&self) -> usize {
385 2
386 }
387 fn arg_schema(&self) -> &'static [ArgSchema] {
388 &ARG_NUM_LENIENT_TWO[..]
389 }
390 fn eval<'a, 'b, 'c>(
391 &self,
392 args: &'c [ArgumentHandle<'a, 'b>],
393 _: &dyn FunctionContext<'b>,
394 ) -> Result<CalcValue<'b>, ExcelError> {
395 // Check minimum required arguments
396 if args.len() < 2 {
397 return Ok(CalcValue::Scalar(LiteralValue::Error(
398 ExcelError::new_value(),
399 )));
400 }
401
402 let n_val = args[0].value()?.into_literal();
403 let k_val = args[1].value()?.into_literal();
404
405 let n = match n_val {
406 LiteralValue::Error(e) => return Ok(CalcValue::Scalar(LiteralValue::Error(e))),
407 other => coerce_num(&other)?,
408 };
409 let k = match k_val {
410 LiteralValue::Error(e) => return Ok(CalcValue::Scalar(LiteralValue::Error(e))),
411 other => coerce_num(&other)?,
412 };
413
414 let n = n.trunc() as i64;
415 let k = k.trunc() as i64;
416
417 if n < 0 || k < 0 || k > n {
418 return Ok(CalcValue::Scalar(
419 LiteralValue::Error(ExcelError::new_num()),
420 ));
421 }
422
423 // Calculate C(n, k) = n! / (k! * (n-k)!)
424 // Use the more efficient formula: C(n, k) = product of (n-i)/(i+1) for i in 0..k
425 let k = k.min(n - k) as u64; // Use symmetry for efficiency
426 let n = n as u64;
427
428 let mut result = 1.0_f64;
429 for i in 0..k {
430 result = result * (n - i) as f64 / (i + 1) as f64;
431 }
432
433 Ok(CalcValue::Scalar(LiteralValue::Number(result.round())))
434 }
435}
436
437#[derive(Debug)]
438pub struct PermutFn;
439/// Returns the number of permutations for selecting and ordering `k` items from `n`.
440///
441/// `PERMUT` computes `n!/(n-k)!` after truncating both inputs toward zero.
442///
443/// # Remarks
444/// - Fractional inputs are truncated to integers.
445/// - If `n < 0`, `k < 0`, or `k > n`, the function returns `#NUM!`.
446/// - Argument errors propagate directly.
447///
448/// # Examples
449///
450/// ```yaml,sandbox
451/// title: "Basic permutations"
452/// formula: "=PERMUT(5, 2)"
453/// expected: 20
454/// ```
455///
456/// ```yaml,sandbox
457/// title: "Fractional arguments are truncated"
458/// formula: "=PERMUT(7.9, 3.1)"
459/// expected: 210
460/// ```
461///
462/// ```yaml,sandbox
463/// title: "Out-of-range k returns numeric error"
464/// formula: "=PERMUT(4, 6)"
465/// expected: "#NUM!"
466/// ```
467///
468/// ```yaml,docs
469/// related:
470/// - COMBIN
471/// - FACT
472/// - MULTINOMIAL
473/// faq:
474/// - q: "Why does PERMUT fail when k is larger than n?"
475/// a: "Permutations require choosing at most n items, so k > n returns #NUM!."
476/// - q: "How are decimal inputs handled?"
477/// a: "PERMUT truncates n and k toward zero before computing n!/(n-k)!."
478/// ```
479///
480/// [formualizer-docgen:schema:start]
481/// Name: PERMUT
482/// Type: PermutFn
483/// Min args: 2
484/// Max args: 2
485/// Variadic: false
486/// Signature: PERMUT(arg1: number@scalar, arg2: number@scalar)
487/// 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}
488/// Caps: PURE
489/// [formualizer-docgen:schema:end]
490impl Function for PermutFn {
491 func_caps!(PURE);
492 fn name(&self) -> &'static str {
493 "PERMUT"
494 }
495 fn min_args(&self) -> usize {
496 2
497 }
498 fn arg_schema(&self) -> &'static [ArgSchema] {
499 &ARG_NUM_LENIENT_TWO[..]
500 }
501 fn eval<'a, 'b, 'c>(
502 &self,
503 args: &'c [ArgumentHandle<'a, 'b>],
504 _: &dyn FunctionContext<'b>,
505 ) -> Result<CalcValue<'b>, ExcelError> {
506 // Check minimum required arguments
507 if args.len() < 2 {
508 return Ok(CalcValue::Scalar(LiteralValue::Error(
509 ExcelError::new_value(),
510 )));
511 }
512
513 let n_val = args[0].value()?.into_literal();
514 let k_val = args[1].value()?.into_literal();
515
516 let n = match n_val {
517 LiteralValue::Error(e) => return Ok(CalcValue::Scalar(LiteralValue::Error(e))),
518 other => coerce_num(&other)?,
519 };
520 let k = match k_val {
521 LiteralValue::Error(e) => return Ok(CalcValue::Scalar(LiteralValue::Error(e))),
522 other => coerce_num(&other)?,
523 };
524
525 let n = n.trunc() as i64;
526 let k = k.trunc() as i64;
527
528 if n < 0 || k < 0 || k > n {
529 return Ok(CalcValue::Scalar(
530 LiteralValue::Error(ExcelError::new_num()),
531 ));
532 }
533
534 // P(n, k) = n! / (n-k)! = n * (n-1) * ... * (n-k+1)
535 let mut result = 1.0_f64;
536 for i in 0..k {
537 result *= (n - i) as f64;
538 }
539
540 Ok(CalcValue::Scalar(LiteralValue::Number(result)))
541 }
542}
543
544pub fn register_builtins() {
545 use std::sync::Arc;
546 crate::function_registry::register_function(Arc::new(FactFn));
547 crate::function_registry::register_function(Arc::new(GcdFn));
548 crate::function_registry::register_function(Arc::new(LcmFn));
549 crate::function_registry::register_function(Arc::new(CombinFn));
550 crate::function_registry::register_function(Arc::new(PermutFn));
551}
552
553#[cfg(test)]
554mod tests {
555 use super::*;
556 use crate::test_workbook::TestWorkbook;
557 use crate::traits::ArgumentHandle;
558 use formualizer_parse::parser::{ASTNode, ASTNodeType};
559
560 fn interp(wb: &TestWorkbook) -> crate::interpreter::Interpreter<'_> {
561 wb.interpreter()
562 }
563 fn lit(v: LiteralValue) -> ASTNode {
564 ASTNode::new(ASTNodeType::Literal(v), None)
565 }
566
567 #[test]
568 fn fact_basic() {
569 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(FactFn));
570 let ctx = interp(&wb);
571 let n = lit(LiteralValue::Number(5.0));
572 let f = ctx.context.get_function("", "FACT").unwrap();
573 assert_eq!(
574 f.dispatch(
575 &[ArgumentHandle::new(&n, &ctx)],
576 &ctx.function_context(None)
577 )
578 .unwrap()
579 .into_literal(),
580 LiteralValue::Number(120.0)
581 );
582 }
583
584 #[test]
585 fn fact_zero() {
586 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(FactFn));
587 let ctx = interp(&wb);
588 let n = lit(LiteralValue::Number(0.0));
589 let f = ctx.context.get_function("", "FACT").unwrap();
590 assert_eq!(
591 f.dispatch(
592 &[ArgumentHandle::new(&n, &ctx)],
593 &ctx.function_context(None)
594 )
595 .unwrap()
596 .into_literal(),
597 LiteralValue::Number(1.0)
598 );
599 }
600
601 #[test]
602 fn gcd_basic() {
603 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(GcdFn));
604 let ctx = interp(&wb);
605 let a = lit(LiteralValue::Number(12.0));
606 let b = lit(LiteralValue::Number(18.0));
607 let f = ctx.context.get_function("", "GCD").unwrap();
608 assert_eq!(
609 f.dispatch(
610 &[ArgumentHandle::new(&a, &ctx), ArgumentHandle::new(&b, &ctx)],
611 &ctx.function_context(None)
612 )
613 .unwrap()
614 .into_literal(),
615 LiteralValue::Number(6.0)
616 );
617 }
618
619 #[test]
620 fn lcm_basic() {
621 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(LcmFn));
622 let ctx = interp(&wb);
623 let a = lit(LiteralValue::Number(4.0));
624 let b = lit(LiteralValue::Number(6.0));
625 let f = ctx.context.get_function("", "LCM").unwrap();
626 assert_eq!(
627 f.dispatch(
628 &[ArgumentHandle::new(&a, &ctx), ArgumentHandle::new(&b, &ctx)],
629 &ctx.function_context(None)
630 )
631 .unwrap()
632 .into_literal(),
633 LiteralValue::Number(12.0)
634 );
635 }
636
637 #[test]
638 fn combin_basic() {
639 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(CombinFn));
640 let ctx = interp(&wb);
641 let n = lit(LiteralValue::Number(5.0));
642 let k = lit(LiteralValue::Number(2.0));
643 let f = ctx.context.get_function("", "COMBIN").unwrap();
644 assert_eq!(
645 f.dispatch(
646 &[ArgumentHandle::new(&n, &ctx), ArgumentHandle::new(&k, &ctx)],
647 &ctx.function_context(None)
648 )
649 .unwrap()
650 .into_literal(),
651 LiteralValue::Number(10.0)
652 );
653 }
654
655 #[test]
656 fn permut_basic() {
657 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(PermutFn));
658 let ctx = interp(&wb);
659 let n = lit(LiteralValue::Number(5.0));
660 let k = lit(LiteralValue::Number(2.0));
661 let f = ctx.context.get_function("", "PERMUT").unwrap();
662 assert_eq!(
663 f.dispatch(
664 &[ArgumentHandle::new(&n, &ctx), ArgumentHandle::new(&k, &ctx)],
665 &ctx.function_context(None)
666 )
667 .unwrap()
668 .into_literal(),
669 LiteralValue::Number(20.0)
670 );
671 }
672}