Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions base/src/expressions/parser/static_analysis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,7 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
Function::Choose => 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),
Expand Down Expand Up @@ -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),
Expand Down
97 changes: 97 additions & 0 deletions base/src/functions/mathematical.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
7 changes: 6 additions & 1 deletion base/src/functions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ pub enum Function {
Choose,
Column,
Columns,
Combina,
Cos,
Cosh,
Log,
Expand Down Expand Up @@ -253,7 +254,7 @@ pub enum Function {
}

impl Function {
pub fn into_iter() -> IntoIter<Function, 198> {
pub fn into_iter() -> IntoIter<Function, 199> {
[
Function::And,
Function::False,
Expand Down Expand Up @@ -301,6 +302,7 @@ impl Function {
Function::Choose,
Function::Column,
Function::Columns,
Function::Combina,
Function::Index,
Function::Indirect,
Function::Hlookup,
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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"),
Expand Down Expand Up @@ -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),
Expand Down
39 changes: 39 additions & 0 deletions base/src/test/test_fn_combina.rs
Original file line number Diff line number Diff line change
@@ -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!");
}
2 changes: 1 addition & 1 deletion docs/src/functions/math-and-trigonometry.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
| CEILING.MATH | <Badge type="info" text="Not implemented yet" /> | – |
| CEILING.PRECISE | <Badge type="info" text="Not implemented yet" /> | – |
| COMBIN | <Badge type="info" text="Not implemented yet" /> | – |
| COMBINA | <Badge type="info" text="Not implemented yet" /> | – |
| COMBINA | <Badge type="tip" text="Available" /> | [COMBINA](math_and_trigonometry/combina) |
| COS | <Badge type="tip" text="Available" /> | [COS](math_and_trigonometry/cos) |
| COSH | <Badge type="tip" text="Available" /> | – |
| COT | <Badge type="info" text="Not implemented yet" /> | – |
Expand Down
39 changes: 34 additions & 5 deletions docs/src/functions/math_and_trigonometry/combina.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
:::
## 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(<span title="Number" style="color:#1E88E5">number</span>, <span title="Number" style="color:#1E88E5">number_chosen</span>) => <span title="Number" style="color:#1E88E5">combinations</span>**

### 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)
Loading