From ccae8610cdeb00f76a5dc9332b2cae32b18c2530 Mon Sep 17 00:00:00 2001 From: Brian Hung Date: Sat, 11 Oct 2025 18:43:00 -0700 Subject: [PATCH] Implement COMBINA function --- .../src/expressions/parser/static_analysis.rs | 2 + base/src/functions/mathematical.rs | 97 +++++++++++++++++++ base/src/functions/mod.rs | 7 +- base/src/test/test_fn_combina.rs | 39 ++++++++ docs/src/functions/math-and-trigonometry.md | 2 +- .../math_and_trigonometry/combina.md | 39 +++++++- 6 files changed, 179 insertions(+), 7 deletions(-) create mode 100644 base/src/test/test_fn_combina.rs diff --git a/base/src/expressions/parser/static_analysis.rs b/base/src/expressions/parser/static_analysis.rs index 80f194360..d4e2fb839 100644 --- a/base/src/expressions/parser/static_analysis.rs +++ b/base/src/expressions/parser/static_analysis.rs @@ -609,6 +609,7 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec vec![Signature::Scalar; arg_count], Function::Column => args_signature_row(arg_count), Function::Columns => args_signature_one_vector(arg_count), + Function::Combina => args_signature_scalars(arg_count, 2, 0), Function::Ln => args_signature_scalars(arg_count, 1, 0), Function::Log => args_signature_scalars(arg_count, 1, 1), Function::Log10 => args_signature_scalars(arg_count, 1, 0), @@ -820,6 +821,7 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult { Function::Pi => StaticResult::Scalar, Function::Power => scalar_arguments(args), Function::Product => not_implemented(args), + Function::Combina => scalar_arguments(args), Function::Round => scalar_arguments(args), Function::Rounddown => scalar_arguments(args), Function::Roundup => scalar_arguments(args), diff --git a/base/src/functions/mathematical.rs b/base/src/functions/mathematical.rs index 8931b58d2..a3af2e8fc 100644 --- a/base/src/functions/mathematical.rs +++ b/base/src/functions/mathematical.rs @@ -281,6 +281,103 @@ impl Model { CalcResult::Number(result) } + pub(crate) fn fn_combina(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + if args.len() != 2 { + return CalcResult::new_args_number_error(cell); + } + + let number = match self.get_number(&args[0], cell) { + Ok(f) => f.trunc(), + Err(e) => return e, + }; + let number_chosen = match self.get_number(&args[1], cell) { + Ok(f) => f.trunc(), + Err(e) => return e, + }; + + if !number.is_finite() || !number_chosen.is_finite() { + return CalcResult::new_error( + Error::NUM, + cell, + "Arguments must be finite numbers".to_string(), + ); + } + + if number < 0.0 || number_chosen < 0.0 { + return CalcResult::new_error( + Error::NUM, + cell, + "Arguments must be greater than or equal to zero".to_string(), + ); + } + + if number == 0.0 { + return if number_chosen == 0.0 { + CalcResult::Number(1.0) + } else { + CalcResult::Number(0.0) + }; + } + + if number > u64::MAX as f64 || number_chosen > u64::MAX as f64 { + return CalcResult::new_error(Error::NUM, cell, "Arguments are too large".to_string()); + } + + let n = number as u64; + let k = number_chosen as u64; + + if k == 0 { + return CalcResult::Number(1.0); + } + + let total = match n.checked_add(k) { + Some(sum) => match sum.checked_sub(1) { + Some(value) => value, + None => { + return CalcResult::new_error( + Error::NUM, + cell, + "Invalid combination".to_string(), + ) + } + }, + None => { + return CalcResult::new_error(Error::NUM, cell, "Invalid combination".to_string()) + } + }; + + let remaining = match total.checked_sub(k) { + Some(value) => value, + None => { + return CalcResult::new_error(Error::NUM, cell, "Invalid combination".to_string()) + } + }; + let r = k.min(remaining); + + let start = match total.checked_sub(r) { + Some(value) => value, + None => { + return CalcResult::new_error(Error::NUM, cell, "Invalid combination".to_string()) + } + }; + + let mut result = 1.0_f64; + for i in 0..r { + let numerator = (start + i + 1) as f64; + let denominator = (i + 1) as f64; + result *= numerator / denominator; + if !result.is_finite() { + return CalcResult::new_error( + Error::NUM, + cell, + "COMBINA result is out of range".to_string(), + ); + } + } + + CalcResult::Number(result) + } + /// SUMIF(criteria_range, criteria, [sum_range]) /// if sum_rage is missing then criteria_range will be used pub(crate) fn fn_sumif(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { diff --git a/base/src/functions/mod.rs b/base/src/functions/mod.rs index 21c8f72da..fb7ca3283 100644 --- a/base/src/functions/mod.rs +++ b/base/src/functions/mod.rs @@ -52,6 +52,7 @@ pub enum Function { Choose, Column, Columns, + Combina, Cos, Cosh, Log, @@ -253,7 +254,7 @@ pub enum Function { } impl Function { - pub fn into_iter() -> IntoIter { + pub fn into_iter() -> IntoIter { [ Function::And, Function::False, @@ -301,6 +302,7 @@ impl Function { Function::Choose, Function::Column, Function::Columns, + Function::Combina, Function::Index, Function::Indirect, Function::Hlookup, @@ -560,6 +562,7 @@ impl Function { "CHOOSE" => Some(Function::Choose), "COLUMN" => Some(Function::Column), "COLUMNS" => Some(Function::Columns), + "COMBINA" => Some(Function::Combina), "INDEX" => Some(Function::Index), "INDIRECT" => Some(Function::Indirect), "HLOOKUP" => Some(Function::Hlookup), @@ -779,6 +782,7 @@ impl fmt::Display for Function { Function::Choose => write!(f, "CHOOSE"), Function::Column => write!(f, "COLUMN"), Function::Columns => write!(f, "COLUMNS"), + Function::Combina => write!(f, "COMBINA"), Function::Index => write!(f, "INDEX"), Function::Indirect => write!(f, "INDIRECT"), Function::Hlookup => write!(f, "HLOOKUP"), @@ -1004,6 +1008,7 @@ impl Model { Function::Max => self.fn_max(args, cell), Function::Min => self.fn_min(args, cell), Function::Product => self.fn_product(args, cell), + Function::Combina => self.fn_combina(args, cell), Function::Rand => self.fn_rand(args, cell), Function::Randbetween => self.fn_randbetween(args, cell), Function::Round => self.fn_round(args, cell), diff --git a/base/src/test/test_fn_combina.rs b/base/src/test/test_fn_combina.rs new file mode 100644 index 000000000..cc00a1944 --- /dev/null +++ b/base/src/test/test_fn_combina.rs @@ -0,0 +1,39 @@ +#![allow(clippy::unwrap_used)] + +use crate::test::util::new_empty_model; + +#[test] +fn test_fn_combina_results() { + let mut model = new_empty_model(); + model._set("A1", "=COMBINA(10,3)"); + model._set("A2", "=COMBINA(3.7,2.2)"); + model._set("A3", "=COMBINA(4,0)"); + model._set("A4", "=COMBINA(0,3)"); + model._set("A5", "=COMBINA(0,0)"); + model._set("A6", "=COMBINA(4,3)"); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"220"); + assert_eq!(model._get_text("A2"), *"6"); + assert_eq!(model._get_text("A3"), *"1"); + assert_eq!(model._get_text("A4"), *"0"); + assert_eq!(model._get_text("A5"), *"1"); + assert_eq!(model._get_text("A6"), *"20"); +} + +#[test] +fn test_fn_combina_errors() { + let mut model = new_empty_model(); + model._set("B1", "=COMBINA(-1,2)"); + model._set("B2", "=COMBINA(3,-2)"); + model._set("B3", "=COMBINA(1)"); + model._set("B4", "=COMBINA(1,2,3)"); + + model.evaluate(); + + assert_eq!(model._get_text("B1"), *"#NUM!"); + assert_eq!(model._get_text("B2"), *"#NUM!"); + assert_eq!(model._get_text("B3"), *"#ERROR!"); + assert_eq!(model._get_text("B4"), *"#ERROR!"); +} diff --git a/docs/src/functions/math-and-trigonometry.md b/docs/src/functions/math-and-trigonometry.md index 03593584f..dd37d49b3 100644 --- a/docs/src/functions/math-and-trigonometry.md +++ b/docs/src/functions/math-and-trigonometry.md @@ -28,7 +28,7 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir | CEILING.MATH | | – | | CEILING.PRECISE | | – | | COMBIN | | – | -| COMBINA | | – | +| COMBINA | | [COMBINA](math_and_trigonometry/combina) | | COS | | [COS](math_and_trigonometry/cos) | | COSH | | – | | COT | | – | diff --git a/docs/src/functions/math_and_trigonometry/combina.md b/docs/src/functions/math_and_trigonometry/combina.md index 1cd06128a..3dcfae5dd 100644 --- a/docs/src/functions/math_and_trigonometry/combina.md +++ b/docs/src/functions/math_and_trigonometry/combina.md @@ -4,9 +4,38 @@ outline: deep lang: en-US --- -# COMBINA +# COMBINA function -::: warning -🚧 This function is not yet available in IronCalc. -[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions) -::: \ No newline at end of file +## Overview +The **COMBINA** function returns the number of combinations for a set of items when repetitions are allowed. It is useful when you need to determine how many unique groups of a specific size can be formed from a larger pool of items where each item can be selected more than once. + +## Usage +### Syntax +**COMBINA(number, number_chosen) => combinations** + +### Argument descriptions +* *number* ([number](/features/value-types#numbers), required). The total number of distinct items available. Values with decimals are truncated to integers. +* *number_chosen* ([number](/features/value-types#numbers), required). The number of items in each combination. Values with decimals are truncated to integers. + +### Additional guidance +If you need combinations without repetition, use the [COMBIN](https://support.microsoft.com/office/combin-function-89073f40-2f70-4fb9-8b82-4901034f0b34) function instead. When order matters, use [PERMUT](/functions/statistical/permut) or a related permutation function. + +### Returned value +COMBINA returns a [number](/features/value-types#numbers) representing the count of possible combinations with repetition. + +### Error conditions +* If either argument is omitted or more than two arguments are supplied, COMBINA returns the [`#ERROR!`](/features/error-types.md#error) error. +* If either argument cannot be interpreted as a [number](/features/value-types#numbers), COMBINA returns the [`#VALUE!`](/features/error-types.md#value) error. +* If either argument is negative, or the result exceeds the numeric range handled by IronCalc, COMBINA returns the [`#NUM!`](/features/error-types.md#num) error. + +## Details +COMBINA calculates the value of the binomial coefficient $\binom{n + k - 1}{k}$, where $n$ is *number* and $k$ is *number_chosen*. When *number* is zero and *number_chosen* is greater than zero, COMBINA returns zero, reflecting that no combinations are possible. When both arguments are zero, COMBINA returns one to represent the empty combination. + +## Examples +* `COMBINA(10, 3)` returns `220`, counting all 3-item codes that can be formed with digits 0–9 when digits can repeat. +* `COMBINA(4, 2)` returns `10`, showing the number of 2-item selections that can be made from four items when repetition is allowed. +* `COMBINA(0, 3)` returns `0`, because there are no items to choose from. + +## Links +* [Microsoft Excel documentation for COMBINA](https://support.microsoft.com/office/combina-function-205c1f8f-2323-4d1c-ac54-53398b3e0ad3) +* [Wikipedia: Multiset coefficient](https://en.wikipedia.org/wiki/Multiset#Counting_multisets)