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
544#[derive(Debug)]
545pub struct FactDoubleFn;
546/// Returns the double factorial of a number.
547///
548/// # Examples
549///
550/// ```excel
551/// =FACTDOUBLE(7)
552/// ```
553///
554/// ```yaml,sandbox
555/// title: "Odd double factorial"
556/// formula: '=FACTDOUBLE(7)'
557/// expected: 105
558/// ```
559///
560/// ```yaml,docs
561/// related:
562/// - FACT
563/// - COMBIN
564/// faq:
565/// - q: "What does FACTDOUBLE do with 0 or -1?"
566/// a: "It returns 1, matching spreadsheet double-factorial conventions."
567/// ```
568///
569/// [formualizer-docgen:schema:start]
570/// Name: FACTDOUBLE
571/// Type: FactDoubleFn
572/// Min args: 1
573/// Max args: 1
574/// Variadic: false
575/// Signature: FACTDOUBLE(arg1: number@scalar)
576/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
577/// Caps: PURE
578/// [formualizer-docgen:schema:end]
579impl Function for FactDoubleFn {
580 func_caps!(PURE);
581 fn name(&self) -> &'static str {
582 "FACTDOUBLE"
583 }
584 fn min_args(&self) -> usize {
585 1
586 }
587 fn arg_schema(&self) -> &'static [ArgSchema] {
588 &ARG_NUM_LENIENT_ONE[..]
589 }
590 fn eval<'a, 'b, 'c>(
591 &self,
592 args: &'c [ArgumentHandle<'a, 'b>],
593 _: &dyn FunctionContext<'b>,
594 ) -> Result<CalcValue<'b>, ExcelError> {
595 let v = args[0].value()?.into_literal();
596 let n = match v {
597 LiteralValue::Error(e) => return Ok(CalcValue::Scalar(LiteralValue::Error(e))),
598 other => coerce_num(&other)?,
599 };
600 let n = n.trunc() as i64;
601 if n < -1 {
602 return Ok(CalcValue::Scalar(
603 LiteralValue::Error(ExcelError::new_num()),
604 ));
605 }
606 if n <= 0 {
607 return Ok(CalcValue::Scalar(LiteralValue::Number(1.0)));
608 }
609 let mut result = 1.0_f64;
610 let mut i = n;
611 while i > 0 {
612 result *= i as f64;
613 i -= 2;
614 }
615 Ok(CalcValue::Scalar(LiteralValue::Number(result)))
616 }
617}
618
619#[derive(Debug)]
620pub struct CombinaFn;
621/// Returns the number of combinations with repetition.
622///
623/// # Examples
624///
625/// ```excel
626/// =COMBINA(4,2)
627/// ```
628///
629/// ```yaml,sandbox
630/// title: "Choose with repetition"
631/// formula: '=COMBINA(4,3)'
632/// expected: 20
633/// ```
634///
635/// ```yaml,docs
636/// related:
637/// - COMBIN
638/// faq:
639/// - q: "How is COMBINA different from COMBIN?"
640/// a: "COMBINA counts selections where the same item can be chosen more than once."
641/// ```
642///
643/// [formualizer-docgen:schema:start]
644/// Name: COMBINA
645/// Type: CombinaFn
646/// Min args: 2
647/// Max args: 2
648/// Variadic: false
649/// Signature: COMBINA(arg1: number@scalar, arg2: number@scalar)
650/// 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}
651/// Caps: PURE
652/// [formualizer-docgen:schema:end]
653impl Function for CombinaFn {
654 func_caps!(PURE);
655 fn name(&self) -> &'static str {
656 "COMBINA"
657 }
658 fn min_args(&self) -> usize {
659 2
660 }
661 fn arg_schema(&self) -> &'static [ArgSchema] {
662 &ARG_NUM_LENIENT_TWO[..]
663 }
664 fn eval<'a, 'b, 'c>(
665 &self,
666 args: &'c [ArgumentHandle<'a, 'b>],
667 _: &dyn FunctionContext<'b>,
668 ) -> Result<CalcValue<'b>, ExcelError> {
669 if args.len() < 2 {
670 return Ok(CalcValue::Scalar(LiteralValue::Error(
671 ExcelError::new_value(),
672 )));
673 }
674 let n_val = args[0].value()?.into_literal();
675 let k_val = args[1].value()?.into_literal();
676 let n = match n_val {
677 LiteralValue::Error(e) => return Ok(CalcValue::Scalar(LiteralValue::Error(e))),
678 other => coerce_num(&other)?,
679 };
680 let k = match k_val {
681 LiteralValue::Error(e) => return Ok(CalcValue::Scalar(LiteralValue::Error(e))),
682 other => coerce_num(&other)?,
683 };
684 let n = n.trunc() as i64;
685 let k = k.trunc() as i64;
686 if n < 0 || k < 0 {
687 return Ok(CalcValue::Scalar(
688 LiteralValue::Error(ExcelError::new_num()),
689 ));
690 }
691 if k == 0 {
692 return Ok(CalcValue::Scalar(LiteralValue::Number(1.0)));
693 }
694 // COMBINA(n,k) = C(n+k-1, k)
695 let nn = n + k - 1;
696 let kk = k.min(nn - k) as u64;
697 let nn = nn as u64;
698 let mut result = 1.0_f64;
699 for i in 0..kk {
700 result = result * (nn - i) as f64 / (i + 1) as f64;
701 }
702 Ok(CalcValue::Scalar(LiteralValue::Number(result.round())))
703 }
704}
705
706pub fn register_builtins() {
707 use std::sync::Arc;
708 crate::function_registry::register_function(Arc::new(FactFn));
709 crate::function_registry::register_function(Arc::new(FactDoubleFn));
710 crate::function_registry::register_function(Arc::new(GcdFn));
711 crate::function_registry::register_function(Arc::new(LcmFn));
712 crate::function_registry::register_function(Arc::new(CombinFn));
713 crate::function_registry::register_function(Arc::new(CombinaFn));
714 crate::function_registry::register_function(Arc::new(PermutFn));
715}
716
717#[cfg(test)]
718mod tests {
719 use super::*;
720 use crate::test_workbook::TestWorkbook;
721 use crate::traits::ArgumentHandle;
722 use formualizer_parse::parser::{ASTNode, ASTNodeType};
723
724 fn interp(wb: &TestWorkbook) -> crate::interpreter::Interpreter<'_> {
725 wb.interpreter()
726 }
727 fn lit(v: LiteralValue) -> ASTNode {
728 ASTNode::new(ASTNodeType::Literal(v), None)
729 }
730
731 #[test]
732 fn fact_basic() {
733 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(FactFn));
734 let ctx = interp(&wb);
735 let n = lit(LiteralValue::Number(5.0));
736 let f = ctx.context.get_function("", "FACT").unwrap();
737 assert_eq!(
738 f.dispatch(
739 &[ArgumentHandle::new(&n, &ctx)],
740 &ctx.function_context(None)
741 )
742 .unwrap()
743 .into_literal(),
744 LiteralValue::Number(120.0)
745 );
746 }
747
748 #[test]
749 fn fact_zero() {
750 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(FactFn));
751 let ctx = interp(&wb);
752 let n = lit(LiteralValue::Number(0.0));
753 let f = ctx.context.get_function("", "FACT").unwrap();
754 assert_eq!(
755 f.dispatch(
756 &[ArgumentHandle::new(&n, &ctx)],
757 &ctx.function_context(None)
758 )
759 .unwrap()
760 .into_literal(),
761 LiteralValue::Number(1.0)
762 );
763 }
764
765 #[test]
766 fn gcd_basic() {
767 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(GcdFn));
768 let ctx = interp(&wb);
769 let a = lit(LiteralValue::Number(12.0));
770 let b = lit(LiteralValue::Number(18.0));
771 let f = ctx.context.get_function("", "GCD").unwrap();
772 assert_eq!(
773 f.dispatch(
774 &[ArgumentHandle::new(&a, &ctx), ArgumentHandle::new(&b, &ctx)],
775 &ctx.function_context(None)
776 )
777 .unwrap()
778 .into_literal(),
779 LiteralValue::Number(6.0)
780 );
781 }
782
783 #[test]
784 fn lcm_basic() {
785 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(LcmFn));
786 let ctx = interp(&wb);
787 let a = lit(LiteralValue::Number(4.0));
788 let b = lit(LiteralValue::Number(6.0));
789 let f = ctx.context.get_function("", "LCM").unwrap();
790 assert_eq!(
791 f.dispatch(
792 &[ArgumentHandle::new(&a, &ctx), ArgumentHandle::new(&b, &ctx)],
793 &ctx.function_context(None)
794 )
795 .unwrap()
796 .into_literal(),
797 LiteralValue::Number(12.0)
798 );
799 }
800
801 #[test]
802 fn combin_basic() {
803 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(CombinFn));
804 let ctx = interp(&wb);
805 let n = lit(LiteralValue::Number(5.0));
806 let k = lit(LiteralValue::Number(2.0));
807 let f = ctx.context.get_function("", "COMBIN").unwrap();
808 assert_eq!(
809 f.dispatch(
810 &[ArgumentHandle::new(&n, &ctx), ArgumentHandle::new(&k, &ctx)],
811 &ctx.function_context(None)
812 )
813 .unwrap()
814 .into_literal(),
815 LiteralValue::Number(10.0)
816 );
817 }
818
819 #[test]
820 fn permut_basic() {
821 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(PermutFn));
822 let ctx = interp(&wb);
823 let n = lit(LiteralValue::Number(5.0));
824 let k = lit(LiteralValue::Number(2.0));
825 let f = ctx.context.get_function("", "PERMUT").unwrap();
826 assert_eq!(
827 f.dispatch(
828 &[ArgumentHandle::new(&n, &ctx), ArgumentHandle::new(&k, &ctx)],
829 &ctx.function_context(None)
830 )
831 .unwrap()
832 .into_literal(),
833 LiteralValue::Number(20.0)
834 );
835 }
836}