Compare commits
95 Commits
feature/br
...
dani/widge
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ce34045d6 | ||
|
|
bcd1f66c9c | ||
|
|
5a891483b6 | ||
|
|
0eafc9b599 | ||
|
|
e48e539bd6 | ||
|
|
9aac285964 | ||
|
|
ba40c3c673 | ||
|
|
cc01556387 | ||
|
|
35323df20e | ||
|
|
19c115b32f | ||
|
|
6b60b339d6 | ||
|
|
41c8d88b80 | ||
|
|
73e5c305cc | ||
|
|
774b447c84 | ||
|
|
23b7333572 | ||
|
|
ef47c26c50 | ||
|
|
5cc61b0de4 | ||
|
|
42e8d44454 | ||
|
|
f840806f94 | ||
|
|
4a21d4b03a | ||
|
|
4cf162eb82 | ||
|
|
2cab93be18 | ||
|
|
fd34e46689 | ||
|
|
3bb49d1e8f | ||
|
|
1391f196b5 | ||
|
|
3db094c956 | ||
|
|
50941cb6ef | ||
|
|
150b516863 | ||
|
|
dc49afa2c3 | ||
|
|
acb90fbb9d | ||
|
|
7676efca44 | ||
|
|
8e15c623dd | ||
|
|
eb76d8dd23 | ||
|
|
1053d00d22 | ||
|
|
5ff4774c5a | ||
|
|
7e966baa0d | ||
|
|
c52c05aa8e | ||
|
|
129959137d | ||
|
|
4d5af45711 | ||
|
|
471f32f92a | ||
|
|
7b5427196d | ||
|
|
66b7586730 | ||
|
|
630f0e1baf | ||
|
|
bc9fefcb70 | ||
|
|
3d970acc34 | ||
|
|
e0e566db76 | ||
|
|
e3fc1d229a | ||
|
|
78d1f6b4a4 | ||
|
|
45ee1c35fe | ||
|
|
671cfff619 | ||
|
|
7e2fcec4a3 | ||
|
|
12342da649 | ||
|
|
4e9d7611a8 | ||
|
|
e0339f641b | ||
|
|
aa953e1ece | ||
|
|
cbf75c059b | ||
|
|
b2744efeb5 | ||
|
|
ef6849e822 | ||
|
|
aa4dd598b1 | ||
|
|
8b3bd7943e | ||
|
|
a1d1b64b76 | ||
|
|
5094a7fe4d | ||
|
|
c283fd7b60 | ||
|
|
36beccd4ae | ||
|
|
a252f9c626 | ||
|
|
f8bd03d92c | ||
|
|
e44a2e8c3e | ||
|
|
4217c1455b | ||
|
|
d8b3ba0dae | ||
|
|
95a7782f22 | ||
|
|
087211ebc3 | ||
|
|
46d766c85c | ||
|
|
2a14ee73c4 | ||
|
|
401c7c4289 | ||
|
|
3246137545 | ||
|
|
b1f45511d0 | ||
|
|
4b93174261 | ||
|
|
3111a74530 | ||
|
|
ae3fcaf9e9 | ||
|
|
dd78db3d2b | ||
|
|
acf334074f | ||
|
|
e48810d91b | ||
|
|
18db1cf052 | ||
|
|
ed40f79324 | ||
|
|
10ee95c48f | ||
|
|
741a223f3d | ||
|
|
ba139d1b6c | ||
|
|
e0306cb161 | ||
|
|
cea1f67cd0 | ||
|
|
4a3eef5a81 | ||
|
|
91299e3c0b | ||
|
|
1b38d79b81 | ||
|
|
a2d11a42cc | ||
|
|
480a2d1769 | ||
|
|
f30f6864e2 |
20
Cargo.lock
generated
20
Cargo.lock
generated
@@ -43,6 +43,15 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "approx"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6"
|
||||||
|
dependencies = [
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arrayvec"
|
name = "arrayvec"
|
||||||
version = "0.7.6"
|
version = "0.7.6"
|
||||||
@@ -443,6 +452,7 @@ dependencies = [
|
|||||||
"ryu",
|
"ryu",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"statrs",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -965,6 +975,16 @@ version = "0.3.11"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
|
checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "statrs"
|
||||||
|
version = "0.18.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2a3fe7c28c6512e766b0874335db33c94ad7b8f9054228ae1c2abd47ce7d335e"
|
||||||
|
dependencies = [
|
||||||
|
"approx",
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "subtle"
|
name = "subtle"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ regex = { version = "1.0", optional = true}
|
|||||||
regex-lite = { version = "0.1.6", optional = true}
|
regex-lite = { version = "0.1.6", optional = true}
|
||||||
bitcode = "0.6.3"
|
bitcode = "0.6.3"
|
||||||
csv = "1.3.0"
|
csv = "1.3.0"
|
||||||
|
statrs = { version = "0.18.0", default-features = false, features = [] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["use_regex_full"]
|
default = ["use_regex_full"]
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use crate::{
|
|||||||
token::Error,
|
token::Error,
|
||||||
types::CellReferenceIndex,
|
types::CellReferenceIndex,
|
||||||
},
|
},
|
||||||
|
formatter::format::parse_formatted_number,
|
||||||
model::Model,
|
model::Model,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -14,6 +15,23 @@ pub(crate) enum NumberOrArray {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Model {
|
impl Model {
|
||||||
|
pub(crate) fn cast_number(&self, s: &str) -> Option<f64> {
|
||||||
|
match s.trim().parse::<f64>() {
|
||||||
|
Ok(f) => Some(f),
|
||||||
|
_ => {
|
||||||
|
let currency = &self.locale.currency.symbol;
|
||||||
|
let mut currencies = vec!["$", "€"];
|
||||||
|
if !currencies.iter().any(|e| *e == currency) {
|
||||||
|
currencies.push(currency);
|
||||||
|
}
|
||||||
|
// Try to parse as a formatted number (e.g., dates, currencies, percentages)
|
||||||
|
if let Ok((v, _number_format)) = parse_formatted_number(s, ¤cies) {
|
||||||
|
return Some(v);
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
pub(crate) fn get_number_or_array(
|
pub(crate) fn get_number_or_array(
|
||||||
&mut self,
|
&mut self,
|
||||||
node: &Node,
|
node: &Node,
|
||||||
@@ -21,9 +39,9 @@ impl Model {
|
|||||||
) -> Result<NumberOrArray, CalcResult> {
|
) -> Result<NumberOrArray, CalcResult> {
|
||||||
match self.evaluate_node_in_context(node, cell) {
|
match self.evaluate_node_in_context(node, cell) {
|
||||||
CalcResult::Number(f) => Ok(NumberOrArray::Number(f)),
|
CalcResult::Number(f) => Ok(NumberOrArray::Number(f)),
|
||||||
CalcResult::String(s) => match s.parse::<f64>() {
|
CalcResult::String(s) => match self.cast_number(&s) {
|
||||||
Ok(f) => Ok(NumberOrArray::Number(f)),
|
Some(f) => Ok(NumberOrArray::Number(f)),
|
||||||
_ => Err(CalcResult::new_error(
|
None => Err(CalcResult::new_error(
|
||||||
Error::VALUE,
|
Error::VALUE,
|
||||||
cell,
|
cell,
|
||||||
"Expecting number".to_string(),
|
"Expecting number".to_string(),
|
||||||
@@ -89,16 +107,16 @@ impl Model {
|
|||||||
self.cast_to_number(result, cell)
|
self.cast_to_number(result, cell)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cast_to_number(
|
pub(crate) fn cast_to_number(
|
||||||
&mut self,
|
&mut self,
|
||||||
result: CalcResult,
|
result: CalcResult,
|
||||||
cell: CellReferenceIndex,
|
cell: CellReferenceIndex,
|
||||||
) -> Result<f64, CalcResult> {
|
) -> Result<f64, CalcResult> {
|
||||||
match result {
|
match result {
|
||||||
CalcResult::Number(f) => Ok(f),
|
CalcResult::Number(f) => Ok(f),
|
||||||
CalcResult::String(s) => match s.parse::<f64>() {
|
CalcResult::String(s) => match self.cast_number(&s) {
|
||||||
Ok(f) => Ok(f),
|
Some(f) => Ok(f),
|
||||||
_ => Err(CalcResult::new_error(
|
None => Err(CalcResult::new_error(
|
||||||
Error::VALUE,
|
Error::VALUE,
|
||||||
cell,
|
cell,
|
||||||
"Expecting number".to_string(),
|
"Expecting number".to_string(),
|
||||||
|
|||||||
@@ -550,14 +550,6 @@ fn args_signature_irr(arg_count: usize) -> Vec<Signature> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn args_signature_fvschedule(arg_count: usize) -> Vec<Signature> {
|
|
||||||
if arg_count == 2 {
|
|
||||||
vec![Signature::Scalar, Signature::Vector]
|
|
||||||
} else {
|
|
||||||
vec![Signature::Error; arg_count]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn args_signature_xirr(arg_count: usize) -> Vec<Signature> {
|
fn args_signature_xirr(arg_count: usize) -> Vec<Signature> {
|
||||||
if arg_count == 2 {
|
if arg_count == 2 {
|
||||||
vec![Signature::Vector; arg_count]
|
vec![Signature::Vector; arg_count]
|
||||||
@@ -760,7 +752,6 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
|
|||||||
Function::Dollarfr => args_signature_scalars(arg_count, 2, 0),
|
Function::Dollarfr => args_signature_scalars(arg_count, 2, 0),
|
||||||
Function::Effect => args_signature_scalars(arg_count, 2, 0),
|
Function::Effect => args_signature_scalars(arg_count, 2, 0),
|
||||||
Function::Fv => args_signature_scalars(arg_count, 3, 2),
|
Function::Fv => args_signature_scalars(arg_count, 3, 2),
|
||||||
Function::Fvschedule => args_signature_fvschedule(arg_count),
|
|
||||||
Function::Ipmt => args_signature_scalars(arg_count, 4, 2),
|
Function::Ipmt => args_signature_scalars(arg_count, 4, 2),
|
||||||
Function::Irr => args_signature_irr(arg_count),
|
Function::Irr => args_signature_irr(arg_count),
|
||||||
Function::Ispmt => args_signature_scalars(arg_count, 4, 0),
|
Function::Ispmt => args_signature_scalars(arg_count, 4, 0),
|
||||||
@@ -768,20 +759,9 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
|
|||||||
Function::Nominal => args_signature_scalars(arg_count, 2, 0),
|
Function::Nominal => args_signature_scalars(arg_count, 2, 0),
|
||||||
Function::Nper => args_signature_scalars(arg_count, 3, 2),
|
Function::Nper => args_signature_scalars(arg_count, 3, 2),
|
||||||
Function::Npv => args_signature_npv(arg_count),
|
Function::Npv => args_signature_npv(arg_count),
|
||||||
Function::Duration => args_signature_scalars(arg_count, 5, 1),
|
|
||||||
Function::Mduration => args_signature_scalars(arg_count, 5, 1),
|
|
||||||
Function::Pduration => args_signature_scalars(arg_count, 3, 0),
|
Function::Pduration => args_signature_scalars(arg_count, 3, 0),
|
||||||
Function::Accrint => args_signature_scalars(arg_count, 6, 2),
|
|
||||||
Function::Accrintm => args_signature_scalars(arg_count, 4, 1),
|
|
||||||
Function::Coupdaybs => args_signature_scalars(arg_count, 3, 1),
|
|
||||||
Function::Coupdays => args_signature_scalars(arg_count, 3, 1),
|
|
||||||
Function::Coupdaysnc => args_signature_scalars(arg_count, 3, 1),
|
|
||||||
Function::Coupncd => args_signature_scalars(arg_count, 3, 1),
|
|
||||||
Function::Coupnum => args_signature_scalars(arg_count, 3, 1),
|
|
||||||
Function::Couppcd => args_signature_scalars(arg_count, 3, 1),
|
|
||||||
Function::Pmt => args_signature_scalars(arg_count, 3, 2),
|
Function::Pmt => args_signature_scalars(arg_count, 3, 2),
|
||||||
Function::Ppmt => args_signature_scalars(arg_count, 4, 2),
|
Function::Ppmt => args_signature_scalars(arg_count, 4, 2),
|
||||||
Function::Price => args_signature_scalars(arg_count, 6, 1),
|
|
||||||
Function::Pv => args_signature_scalars(arg_count, 3, 2),
|
Function::Pv => args_signature_scalars(arg_count, 3, 2),
|
||||||
Function::Rate => args_signature_scalars(arg_count, 3, 3),
|
Function::Rate => args_signature_scalars(arg_count, 3, 3),
|
||||||
Function::Rri => args_signature_scalars(arg_count, 3, 0),
|
Function::Rri => args_signature_scalars(arg_count, 3, 0),
|
||||||
@@ -790,14 +770,6 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
|
|||||||
Function::Tbilleq => args_signature_scalars(arg_count, 3, 0),
|
Function::Tbilleq => args_signature_scalars(arg_count, 3, 0),
|
||||||
Function::Tbillprice => args_signature_scalars(arg_count, 3, 0),
|
Function::Tbillprice => args_signature_scalars(arg_count, 3, 0),
|
||||||
Function::Tbillyield => args_signature_scalars(arg_count, 3, 0),
|
Function::Tbillyield => args_signature_scalars(arg_count, 3, 0),
|
||||||
Function::Yield => args_signature_scalars(arg_count, 6, 1),
|
|
||||||
Function::Pricedisc => args_signature_scalars(arg_count, 4, 1),
|
|
||||||
Function::Pricemat => args_signature_scalars(arg_count, 5, 1),
|
|
||||||
Function::Yielddisc => args_signature_scalars(arg_count, 4, 1),
|
|
||||||
Function::Yieldmat => args_signature_scalars(arg_count, 5, 1),
|
|
||||||
Function::Disc => args_signature_scalars(arg_count, 4, 1),
|
|
||||||
Function::Received => args_signature_scalars(arg_count, 4, 1),
|
|
||||||
Function::Intrate => args_signature_scalars(arg_count, 4, 1),
|
|
||||||
Function::Xirr => args_signature_xirr(arg_count),
|
Function::Xirr => args_signature_xirr(arg_count),
|
||||||
Function::Xnpv => args_signature_xnpv(arg_count),
|
Function::Xnpv => args_signature_xnpv(arg_count),
|
||||||
Function::Besseli => args_signature_scalars(arg_count, 2, 0),
|
Function::Besseli => args_signature_scalars(arg_count, 2, 0),
|
||||||
@@ -904,6 +876,19 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
|
|||||||
Function::Sheets => args_signature_scalars(arg_count, 0, 1),
|
Function::Sheets => args_signature_scalars(arg_count, 0, 1),
|
||||||
Function::Cell => args_signature_scalars(arg_count, 1, 1),
|
Function::Cell => args_signature_scalars(arg_count, 1, 1),
|
||||||
Function::Info => args_signature_scalars(arg_count, 1, 1),
|
Function::Info => args_signature_scalars(arg_count, 1, 1),
|
||||||
|
|
||||||
|
Function::Daverage => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
|
||||||
|
Function::Dcount => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
|
||||||
|
Function::Dget => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
|
||||||
|
Function::Dmax => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
|
||||||
|
Function::Dmin => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
|
||||||
|
Function::Dsum => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
|
||||||
|
Function::Dcounta => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
|
||||||
|
Function::Dproduct => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
|
||||||
|
Function::Dstdev => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
|
||||||
|
Function::Dvar => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
|
||||||
|
Function::Dvarp => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
|
||||||
|
Function::Dstdevp => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1044,7 +1029,6 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
|
|||||||
Function::Dollarfr => not_implemented(args),
|
Function::Dollarfr => not_implemented(args),
|
||||||
Function::Effect => not_implemented(args),
|
Function::Effect => not_implemented(args),
|
||||||
Function::Fv => not_implemented(args),
|
Function::Fv => not_implemented(args),
|
||||||
Function::Fvschedule => not_implemented(args),
|
|
||||||
Function::Ipmt => not_implemented(args),
|
Function::Ipmt => not_implemented(args),
|
||||||
Function::Irr => not_implemented(args),
|
Function::Irr => not_implemented(args),
|
||||||
Function::Ispmt => not_implemented(args),
|
Function::Ispmt => not_implemented(args),
|
||||||
@@ -1052,20 +1036,9 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
|
|||||||
Function::Nominal => not_implemented(args),
|
Function::Nominal => not_implemented(args),
|
||||||
Function::Nper => not_implemented(args),
|
Function::Nper => not_implemented(args),
|
||||||
Function::Npv => not_implemented(args),
|
Function::Npv => not_implemented(args),
|
||||||
Function::Duration => not_implemented(args),
|
|
||||||
Function::Mduration => not_implemented(args),
|
|
||||||
Function::Pduration => not_implemented(args),
|
Function::Pduration => not_implemented(args),
|
||||||
Function::Accrint => not_implemented(args),
|
|
||||||
Function::Accrintm => not_implemented(args),
|
|
||||||
Function::Coupdaybs => not_implemented(args),
|
|
||||||
Function::Coupdays => not_implemented(args),
|
|
||||||
Function::Coupdaysnc => not_implemented(args),
|
|
||||||
Function::Coupncd => not_implemented(args),
|
|
||||||
Function::Coupnum => not_implemented(args),
|
|
||||||
Function::Couppcd => not_implemented(args),
|
|
||||||
Function::Pmt => not_implemented(args),
|
Function::Pmt => not_implemented(args),
|
||||||
Function::Ppmt => not_implemented(args),
|
Function::Ppmt => not_implemented(args),
|
||||||
Function::Price => not_implemented(args),
|
|
||||||
Function::Pv => not_implemented(args),
|
Function::Pv => not_implemented(args),
|
||||||
Function::Rate => not_implemented(args),
|
Function::Rate => not_implemented(args),
|
||||||
Function::Rri => not_implemented(args),
|
Function::Rri => not_implemented(args),
|
||||||
@@ -1074,14 +1047,6 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
|
|||||||
Function::Tbilleq => not_implemented(args),
|
Function::Tbilleq => not_implemented(args),
|
||||||
Function::Tbillprice => not_implemented(args),
|
Function::Tbillprice => not_implemented(args),
|
||||||
Function::Tbillyield => not_implemented(args),
|
Function::Tbillyield => not_implemented(args),
|
||||||
Function::Yield => not_implemented(args),
|
|
||||||
Function::Pricedisc => not_implemented(args),
|
|
||||||
Function::Pricemat => not_implemented(args),
|
|
||||||
Function::Yielddisc => not_implemented(args),
|
|
||||||
Function::Yieldmat => not_implemented(args),
|
|
||||||
Function::Disc => not_implemented(args),
|
|
||||||
Function::Received => not_implemented(args),
|
|
||||||
Function::Intrate => not_implemented(args),
|
|
||||||
Function::Xirr => not_implemented(args),
|
Function::Xirr => not_implemented(args),
|
||||||
Function::Xnpv => not_implemented(args),
|
Function::Xnpv => not_implemented(args),
|
||||||
Function::Besseli => scalar_arguments(args),
|
Function::Besseli => scalar_arguments(args),
|
||||||
@@ -1187,5 +1152,18 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
|
|||||||
Function::Sheets => scalar_arguments(args),
|
Function::Sheets => scalar_arguments(args),
|
||||||
Function::Cell => scalar_arguments(args),
|
Function::Cell => scalar_arguments(args),
|
||||||
Function::Info => scalar_arguments(args),
|
Function::Info => scalar_arguments(args),
|
||||||
|
|
||||||
|
Function::Dget => not_implemented(args),
|
||||||
|
Function::Dmax => not_implemented(args),
|
||||||
|
Function::Dmin => not_implemented(args),
|
||||||
|
Function::Dcount => not_implemented(args),
|
||||||
|
Function::Daverage => not_implemented(args),
|
||||||
|
Function::Dsum => not_implemented(args),
|
||||||
|
Function::Dcounta => not_implemented(args),
|
||||||
|
Function::Dproduct => not_implemented(args),
|
||||||
|
Function::Dstdev => not_implemented(args),
|
||||||
|
Function::Dvar => not_implemented(args),
|
||||||
|
Function::Dvarp => not_implemented(args),
|
||||||
|
Function::Dstdevp => not_implemented(args),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -211,15 +211,19 @@ pub fn parse_reference_a1(r: &str) -> Option<ParsedReference> {
|
|||||||
pub fn is_valid_identifier(name: &str) -> bool {
|
pub fn is_valid_identifier(name: &str) -> bool {
|
||||||
// https://support.microsoft.com/en-us/office/names-in-formulas-fc2935f9-115d-4bef-a370-3aa8bb4c91f1
|
// https://support.microsoft.com/en-us/office/names-in-formulas-fc2935f9-115d-4bef-a370-3aa8bb4c91f1
|
||||||
// https://github.com/MartinTrummer/excel-names/
|
// https://github.com/MartinTrummer/excel-names/
|
||||||
// NOTE: We are being much more restrictive than Excel.
|
|
||||||
// In particular we do not support non ascii characters.
|
|
||||||
let upper = name.to_ascii_uppercase();
|
let upper = name.to_ascii_uppercase();
|
||||||
let bytes = upper.as_bytes();
|
// length of chars
|
||||||
let len = bytes.len();
|
let len = upper.chars().count();
|
||||||
|
|
||||||
|
let mut chars = upper.chars();
|
||||||
|
|
||||||
if len > 255 || len == 0 {
|
if len > 255 || len == 0 {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
let first = bytes[0] as char;
|
let first = match chars.next() {
|
||||||
|
Some(ch) => ch,
|
||||||
|
None => return false,
|
||||||
|
};
|
||||||
// The first character of a name must be a letter, an underscore character (_), or a backslash (\).
|
// The first character of a name must be a letter, an underscore character (_), or a backslash (\).
|
||||||
if !(first.is_ascii_alphabetic() || first == '_' || first == '\\') {
|
if !(first.is_ascii_alphabetic() || first == '_' || first == '\\') {
|
||||||
return false;
|
return false;
|
||||||
@@ -237,20 +241,10 @@ pub fn is_valid_identifier(name: &str) -> bool {
|
|||||||
if parse_reference_r1c1(name).is_some() {
|
if parse_reference_r1c1(name).is_some() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
let mut i = 1;
|
for ch in chars {
|
||||||
while i < len {
|
if !(ch.is_alphanumeric() || ch == '_' || ch == '.') {
|
||||||
let ch = bytes[i] as char;
|
return false;
|
||||||
match ch {
|
|
||||||
'a'..='z' => {}
|
|
||||||
'A'..='Z' => {}
|
|
||||||
'0'..='9' => {}
|
|
||||||
'_' => {}
|
|
||||||
'.' => {}
|
|
||||||
_ => {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
i += 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
true
|
true
|
||||||
|
|||||||
@@ -196,6 +196,7 @@ fn test_names() {
|
|||||||
assert!(is_valid_identifier("_."));
|
assert!(is_valid_identifier("_."));
|
||||||
assert!(is_valid_identifier("_1"));
|
assert!(is_valid_identifier("_1"));
|
||||||
assert!(is_valid_identifier("\\."));
|
assert!(is_valid_identifier("\\."));
|
||||||
|
assert!(is_valid_identifier("truñe"));
|
||||||
|
|
||||||
// invalid
|
// invalid
|
||||||
assert!(!is_valid_identifier("true"));
|
assert!(!is_valid_identifier("true"));
|
||||||
@@ -209,7 +210,6 @@ fn test_names() {
|
|||||||
assert!(!is_valid_identifier("1true"));
|
assert!(!is_valid_identifier("1true"));
|
||||||
|
|
||||||
assert!(!is_valid_identifier("test€"));
|
assert!(!is_valid_identifier("test€"));
|
||||||
assert!(!is_valid_identifier("truñe"));
|
|
||||||
assert!(!is_valid_identifier("tr&ue"));
|
assert!(!is_valid_identifier("tr&ue"));
|
||||||
|
|
||||||
assert!(!is_valid_identifier("LOG10"));
|
assert!(!is_valid_identifier("LOG10"));
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ pub struct Formatted {
|
|||||||
|
|
||||||
/// Returns the vector of chars of the fractional part of a *positive* number:
|
/// Returns the vector of chars of the fractional part of a *positive* number:
|
||||||
/// 3.1415926 ==> ['1', '4', '1', '5', '9', '2', '6']
|
/// 3.1415926 ==> ['1', '4', '1', '5', '9', '2', '6']
|
||||||
fn get_fract_part(value: f64, precision: i32) -> Vec<char> {
|
fn get_fract_part(value: f64, precision: i32, int_len: usize) -> Vec<char> {
|
||||||
let b = format!("{:.1$}", value.fract(), precision as usize)
|
let b = format!("{:.1$}", value.fract(), precision as usize)
|
||||||
.chars()
|
.chars()
|
||||||
.collect::<Vec<char>>();
|
.collect::<Vec<char>>();
|
||||||
@@ -30,6 +30,12 @@ fn get_fract_part(value: f64, precision: i32) -> Vec<char> {
|
|||||||
if last_non_zero < 2 {
|
if last_non_zero < 2 {
|
||||||
return vec![];
|
return vec![];
|
||||||
}
|
}
|
||||||
|
let max_len = if int_len > 15 {
|
||||||
|
2_usize
|
||||||
|
} else {
|
||||||
|
15_usize - int_len + 1
|
||||||
|
};
|
||||||
|
let last_non_zero = usize::min(last_non_zero, max_len + 1);
|
||||||
b[2..last_non_zero].to_vec()
|
b[2..last_non_zero].to_vec()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -423,7 +429,7 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
|
|||||||
if value_abs as i64 == 0 {
|
if value_abs as i64 == 0 {
|
||||||
int_part = vec![];
|
int_part = vec![];
|
||||||
}
|
}
|
||||||
let fract_part = get_fract_part(value_abs, p.precision);
|
let fract_part = get_fract_part(value_abs, p.precision, int_part.len());
|
||||||
// ln is the number of digits of the integer part of the value
|
// ln is the number of digits of the integer part of the value
|
||||||
let ln = int_part.len() as i32;
|
let ln = int_part.len() as i32;
|
||||||
// digit count is the number of digit tokens ('0', '?' and '#') to the left of the decimal point
|
// digit count is the number of digit tokens ('0', '?' and '#') to the left of the decimal point
|
||||||
@@ -744,10 +750,10 @@ fn parse_date(value: &str) -> Result<(i32, String), String> {
|
|||||||
/// "30.34%" => (0.3034, "0.00%")
|
/// "30.34%" => (0.3034, "0.00%")
|
||||||
/// 100€ => (100, "100€")
|
/// 100€ => (100, "100€")
|
||||||
pub(crate) fn parse_formatted_number(
|
pub(crate) fn parse_formatted_number(
|
||||||
value: &str,
|
original: &str,
|
||||||
currencies: &[&str],
|
currencies: &[&str],
|
||||||
) -> Result<(f64, Option<String>), String> {
|
) -> Result<(f64, Option<String>), String> {
|
||||||
let value = value.trim();
|
let value = original.trim();
|
||||||
let scientific_format = "0.00E+00";
|
let scientific_format = "0.00E+00";
|
||||||
|
|
||||||
// Check if it is a percentage
|
// Check if it is a percentage
|
||||||
@@ -799,7 +805,8 @@ pub(crate) fn parse_formatted_number(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok((serial_number, format)) = parse_date(value) {
|
// check if it is a date. NOTE: we don't trim the original here
|
||||||
|
if let Ok((serial_number, format)) = parse_date(original) {
|
||||||
return Ok((serial_number as f64, Some(format)));
|
return Ok((serial_number as f64, Some(format)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
946
base/src/functions/database.rs
Normal file
946
base/src/functions/database.rs
Normal file
@@ -0,0 +1,946 @@
|
|||||||
|
use chrono::Datelike;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
calc_result::CalcResult,
|
||||||
|
expressions::{parser::Node, token::Error, types::CellReferenceIndex},
|
||||||
|
formatter::dates::date_to_serial_number,
|
||||||
|
Model,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::util::{compare_values, from_wildcard_to_regex, result_matches_regex};
|
||||||
|
|
||||||
|
impl Model {
|
||||||
|
// =DAVERAGE(database, field, criteria)
|
||||||
|
pub(crate) fn fn_daverage(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
if args.len() != 3 {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let (db_left, db_right) = match self.get_reference(&args[0], cell) {
|
||||||
|
Ok(r) => (r.left, r.right),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let field_col = match self.resolve_db_field_column(db_left, db_right, &args[1], cell) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let criteria = match self.get_reference(&args[2], cell) {
|
||||||
|
Ok(r) => (r.left, r.right),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
if db_right.row <= db_left.row {
|
||||||
|
// no data rows
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::VALUE,
|
||||||
|
origin: cell,
|
||||||
|
message: "No data rows in database".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut sum = 0.0f64;
|
||||||
|
let mut count = 0usize;
|
||||||
|
|
||||||
|
let mut row = db_left.row + 1; // skip header
|
||||||
|
while row <= db_right.row {
|
||||||
|
if self.db_row_matches_criteria(db_left, db_right, row, criteria) {
|
||||||
|
let v = self.evaluate_cell(CellReferenceIndex {
|
||||||
|
sheet: db_left.sheet,
|
||||||
|
row,
|
||||||
|
column: field_col,
|
||||||
|
});
|
||||||
|
if let CalcResult::Number(n) = v {
|
||||||
|
if n.is_finite() {
|
||||||
|
sum += n;
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
row += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if count == 0 {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::DIV,
|
||||||
|
origin: cell,
|
||||||
|
message: "No numeric values matched criteria".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(sum / count as f64)
|
||||||
|
}
|
||||||
|
|
||||||
|
// =DCOUNT(database, field, criteria)
|
||||||
|
// Counts numeric entries in the field for rows that match criteria
|
||||||
|
pub(crate) fn fn_dcount(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
if args.len() != 3 {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let (db_left, db_right) = match self.get_reference(&args[0], cell) {
|
||||||
|
Ok(r) => (r.left, r.right),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let field_col = match self.resolve_db_field_column(db_left, db_right, &args[1], cell) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let criteria = match self.get_reference(&args[2], cell) {
|
||||||
|
Ok(r) => (r.left, r.right),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
if db_right.row <= db_left.row {
|
||||||
|
// no data rows
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::VALUE,
|
||||||
|
origin: cell,
|
||||||
|
message: "No data rows in database".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut count = 0usize;
|
||||||
|
let mut row = db_left.row + 1; // skip header
|
||||||
|
while row <= db_right.row {
|
||||||
|
if self.db_row_matches_criteria(db_left, db_right, row, criteria) {
|
||||||
|
let v = self.evaluate_cell(CellReferenceIndex {
|
||||||
|
sheet: db_left.sheet,
|
||||||
|
row,
|
||||||
|
column: field_col,
|
||||||
|
});
|
||||||
|
if matches!(v, CalcResult::Number(_)) {
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
row += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(count as f64)
|
||||||
|
}
|
||||||
|
|
||||||
|
// =DGET(database, field, criteria)
|
||||||
|
// Returns the (single) field value for the unique matching row
|
||||||
|
pub(crate) fn fn_dget(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
if args.len() != 3 {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let (db_left, db_right) = match self.get_reference(&args[0], cell) {
|
||||||
|
Ok(r) => (r.left, r.right),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let field_col = match self.resolve_db_field_column(db_left, db_right, &args[1], cell) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let criteria = match self.get_reference(&args[2], cell) {
|
||||||
|
Ok(r) => (r.left, r.right),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
if db_right.row <= db_left.row {
|
||||||
|
// no data rows
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::VALUE,
|
||||||
|
origin: cell,
|
||||||
|
message: "No data rows in database".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut result: Option<CalcResult> = None;
|
||||||
|
let mut matches = 0usize;
|
||||||
|
|
||||||
|
let mut row = db_left.row + 1;
|
||||||
|
while row <= db_right.row {
|
||||||
|
if self.db_row_matches_criteria(db_left, db_right, row, criteria) {
|
||||||
|
matches += 1;
|
||||||
|
if matches > 1 {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "More than one matching record".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
result = Some(self.evaluate_cell(CellReferenceIndex {
|
||||||
|
sheet: db_left.sheet,
|
||||||
|
row,
|
||||||
|
column: field_col,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
row += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
match (matches, result) {
|
||||||
|
(0, _) | (_, None) => CalcResult::Error {
|
||||||
|
error: Error::VALUE,
|
||||||
|
origin: cell,
|
||||||
|
message: "No matching record".to_string(),
|
||||||
|
},
|
||||||
|
(_, Some(v)) => v,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =DMAX(database, field, criteria)
|
||||||
|
pub(crate) fn fn_dmax(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
self.db_extreme(args, cell, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// =DMIN(database, field, criteria)
|
||||||
|
pub(crate) fn fn_dmin(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
self.db_extreme(args, cell, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// =DSUM(database, field, criteria)
|
||||||
|
pub(crate) fn fn_dsum(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
if args.len() != 3 {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let (db_left, db_right) = match self.get_reference(&args[0], cell) {
|
||||||
|
Ok(r) => (r.left, r.right),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let field_col = match self.resolve_db_field_column(db_left, db_right, &args[1], cell) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let criteria = match self.get_reference(&args[2], cell) {
|
||||||
|
Ok(r) => (r.left, r.right),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
if db_right.row <= db_left.row {
|
||||||
|
// no data rows
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::VALUE,
|
||||||
|
origin: cell,
|
||||||
|
message: "No data rows in database".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut sum = 0.0;
|
||||||
|
|
||||||
|
// skip header
|
||||||
|
let mut row = db_left.row + 1;
|
||||||
|
while row <= db_right.row {
|
||||||
|
if self.db_row_matches_criteria(db_left, db_right, row, criteria) {
|
||||||
|
let v = self.evaluate_cell(CellReferenceIndex {
|
||||||
|
sheet: db_left.sheet,
|
||||||
|
row,
|
||||||
|
column: field_col,
|
||||||
|
});
|
||||||
|
if let CalcResult::Number(n) = v {
|
||||||
|
if n.is_finite() {
|
||||||
|
sum += n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
row += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(sum)
|
||||||
|
}
|
||||||
|
|
||||||
|
// =DCOUNTA(database, field, criteria)
|
||||||
|
// Counts non-empty entries (any type) in the field for rows that match criteria
|
||||||
|
pub(crate) fn fn_dcounta(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
if args.len() != 3 {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let (db_left, db_right) = match self.get_reference(&args[0], cell) {
|
||||||
|
Ok(r) => (r.left, r.right),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let field_col = match self.resolve_db_field_column(db_left, db_right, &args[1], cell) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let criteria = match self.get_reference(&args[2], cell) {
|
||||||
|
Ok(r) => (r.left, r.right),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
if db_right.row <= db_left.row {
|
||||||
|
// no data rows
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::VALUE,
|
||||||
|
origin: cell,
|
||||||
|
message: "No data rows in database".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut count = 0;
|
||||||
|
for row in (db_left.row + 1)..=db_right.row {
|
||||||
|
if self.db_row_matches_criteria(db_left, db_right, row, criteria) {
|
||||||
|
let v = self.evaluate_cell(CellReferenceIndex {
|
||||||
|
sheet: db_left.sheet,
|
||||||
|
row,
|
||||||
|
column: field_col,
|
||||||
|
});
|
||||||
|
if !matches!(v, CalcResult::EmptyCell | CalcResult::EmptyArg) {
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(count as f64)
|
||||||
|
}
|
||||||
|
|
||||||
|
// =DPRODUCT(database, field, criteria)
|
||||||
|
pub(crate) fn fn_dproduct(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
if args.len() != 3 {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let (db_left, db_right) = match self.get_reference(&args[0], cell) {
|
||||||
|
Ok(r) => (r.left, r.right),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let field_col = match self.resolve_db_field_column(db_left, db_right, &args[1], cell) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let criteria = match self.get_reference(&args[2], cell) {
|
||||||
|
Ok(r) => (r.left, r.right),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
if db_right.row <= db_left.row {
|
||||||
|
// no data rows
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::VALUE,
|
||||||
|
origin: cell,
|
||||||
|
message: "No data rows in database".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut product = 1.0f64;
|
||||||
|
let mut has_numeric = false;
|
||||||
|
|
||||||
|
let mut row = db_left.row + 1; // skip header
|
||||||
|
while row <= db_right.row {
|
||||||
|
if self.db_row_matches_criteria(db_left, db_right, row, criteria) {
|
||||||
|
let v = self.evaluate_cell(CellReferenceIndex {
|
||||||
|
sheet: db_left.sheet,
|
||||||
|
row,
|
||||||
|
column: field_col,
|
||||||
|
});
|
||||||
|
if let CalcResult::Number(n) = v {
|
||||||
|
if n.is_finite() {
|
||||||
|
product *= n;
|
||||||
|
has_numeric = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
row += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Excel returns 0 when no rows / no numeric values match for DPRODUCT
|
||||||
|
if has_numeric {
|
||||||
|
CalcResult::Number(product)
|
||||||
|
} else {
|
||||||
|
CalcResult::Number(0.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Small internal helper for DSTDEV / DVAR
|
||||||
|
// Collects sum, sum of squares, and count of numeric values in the field
|
||||||
|
// for rows that match the criteria.
|
||||||
|
fn db_numeric_stats(
|
||||||
|
&mut self,
|
||||||
|
args: &[Node],
|
||||||
|
cell: CellReferenceIndex,
|
||||||
|
) -> Result<(f64, f64, usize), CalcResult> {
|
||||||
|
if args.len() != 3 {
|
||||||
|
return Err(CalcResult::new_args_number_error(cell));
|
||||||
|
}
|
||||||
|
|
||||||
|
let (db_left, db_right) = match self.get_reference(&args[0], cell) {
|
||||||
|
Ok(r) => (r.left, r.right),
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
};
|
||||||
|
|
||||||
|
let field_col = self.resolve_db_field_column(db_left, db_right, &args[1], cell)?;
|
||||||
|
|
||||||
|
let criteria = match self.get_reference(&args[2], cell) {
|
||||||
|
Ok(r) => (r.left, r.right),
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
};
|
||||||
|
|
||||||
|
if db_right.row <= db_left.row {
|
||||||
|
// no data rows
|
||||||
|
return Err(CalcResult::Error {
|
||||||
|
error: Error::VALUE,
|
||||||
|
origin: cell,
|
||||||
|
message: "No data rows in database".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut sum = 0.0f64;
|
||||||
|
let mut sumsq = 0.0f64;
|
||||||
|
let mut count = 0usize;
|
||||||
|
|
||||||
|
let mut row = db_left.row + 1; // skip header
|
||||||
|
while row <= db_right.row {
|
||||||
|
if self.db_row_matches_criteria(db_left, db_right, row, criteria) {
|
||||||
|
let v = self.evaluate_cell(CellReferenceIndex {
|
||||||
|
sheet: db_left.sheet,
|
||||||
|
row,
|
||||||
|
column: field_col,
|
||||||
|
});
|
||||||
|
if let CalcResult::Number(n) = v {
|
||||||
|
if n.is_finite() {
|
||||||
|
sum += n;
|
||||||
|
sumsq += n * n;
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
row += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((sum, sumsq, count))
|
||||||
|
}
|
||||||
|
|
||||||
|
// =DSTDEV(database, field, criteria)
|
||||||
|
// Sample standard deviation of matching numeric values
|
||||||
|
pub(crate) fn fn_dstdev(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
let (sum, sumsq, count) = match self.db_numeric_stats(args, cell) {
|
||||||
|
Ok(stats) => stats,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Excel behaviour: #DIV/0! if 0 or 1 numeric values match
|
||||||
|
if count < 2 {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::DIV,
|
||||||
|
origin: cell,
|
||||||
|
message: "Not enough numeric values matched criteria".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let n = count as f64;
|
||||||
|
let var = (sumsq - (sum * sum) / n) / (n - 1.0);
|
||||||
|
let var = if var < 0.0 { 0.0 } else { var };
|
||||||
|
CalcResult::Number(var.sqrt())
|
||||||
|
}
|
||||||
|
|
||||||
|
// =DVAR(database, field, criteria)
|
||||||
|
// Sample variance of matching numeric values
|
||||||
|
pub(crate) fn fn_dvar(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
let (sum, sumsq, count) = match self.db_numeric_stats(args, cell) {
|
||||||
|
Ok(stats) => stats,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Excel behaviour: #DIV/0! if 0 or 1 numeric values match
|
||||||
|
if count < 2 {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::DIV,
|
||||||
|
origin: cell,
|
||||||
|
message: "Not enough numeric values matched criteria".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let n = count as f64;
|
||||||
|
let var = (sumsq - (sum * sum) / n) / (n - 1.0);
|
||||||
|
let var = if var < 0.0 { 0.0 } else { var };
|
||||||
|
CalcResult::Number(var)
|
||||||
|
}
|
||||||
|
|
||||||
|
// =DSTDEVP(database, field, criteria)
|
||||||
|
// Population standard deviation of matching numeric values
|
||||||
|
pub(crate) fn fn_dstdevp(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
let (sum, sumsq, count) = match self.db_numeric_stats(args, cell) {
|
||||||
|
Ok(stats) => stats,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Excel behaviour: #DIV/0! if no numeric values match
|
||||||
|
if count == 0 {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::DIV,
|
||||||
|
origin: cell,
|
||||||
|
message: "No numeric values matched criteria".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let n = count as f64;
|
||||||
|
let var = (sumsq - (sum * sum) / n) / n;
|
||||||
|
let var = if var < 0.0 { 0.0 } else { var };
|
||||||
|
CalcResult::Number(var.sqrt())
|
||||||
|
}
|
||||||
|
|
||||||
|
// =DVARP(database, field, criteria)
|
||||||
|
// Population variance of matching numeric values
|
||||||
|
pub(crate) fn fn_dvarp(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
let (sum, sumsq, count) = match self.db_numeric_stats(args, cell) {
|
||||||
|
Ok(stats) => stats,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Excel behaviour: #DIV/0! if no numeric values match
|
||||||
|
if count == 0 {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::DIV,
|
||||||
|
origin: cell,
|
||||||
|
message: "No numeric values matched criteria".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let n = count as f64;
|
||||||
|
let var = (sumsq - (sum * sum) / n) / n;
|
||||||
|
let var = if var < 0.0 { 0.0 } else { var };
|
||||||
|
CalcResult::Number(var)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve the "field" (2nd arg) to an absolute column index (i32) within the sheet.
|
||||||
|
/// Field can be a number (1-based index) or a header name (case-insensitive).
|
||||||
|
/// Returns the absolute column index, not a 1-based offset within the database range.
|
||||||
|
fn resolve_db_field_column(
|
||||||
|
&mut self,
|
||||||
|
db_left: CellReferenceIndex,
|
||||||
|
db_right: CellReferenceIndex,
|
||||||
|
field_arg: &Node,
|
||||||
|
cell: CellReferenceIndex,
|
||||||
|
) -> Result<i32, CalcResult> {
|
||||||
|
let field_column_name = match self.evaluate_node_in_context(field_arg, cell) {
|
||||||
|
CalcResult::String(s) => s.to_lowercase(),
|
||||||
|
CalcResult::Number(index) => {
|
||||||
|
let index = index.floor() as i32;
|
||||||
|
if index < 1 || db_left.column + index - 1 > db_right.column {
|
||||||
|
return Err(CalcResult::Error {
|
||||||
|
error: Error::VALUE,
|
||||||
|
origin: cell,
|
||||||
|
message: "Field index out of range".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Ok(db_left.column + index - 1);
|
||||||
|
}
|
||||||
|
CalcResult::Boolean(b) => {
|
||||||
|
return if b {
|
||||||
|
Ok(db_left.column)
|
||||||
|
} else {
|
||||||
|
// Index 0 is out of range
|
||||||
|
Err(CalcResult::Error {
|
||||||
|
error: Error::VALUE,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid field specifier".to_string(),
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
error @ CalcResult::Error { .. } => {
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
CalcResult::Range { .. } => {
|
||||||
|
return Err(CalcResult::Error {
|
||||||
|
error: Error::NIMPL,
|
||||||
|
origin: cell,
|
||||||
|
message: "Arrays not supported yet".to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(),
|
||||||
|
CalcResult::Array(_) => {
|
||||||
|
return Err(CalcResult::Error {
|
||||||
|
error: Error::NIMPL,
|
||||||
|
origin: cell,
|
||||||
|
message: "Arrays not supported yet".to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// We search in the database a column whose header matches field_column_name
|
||||||
|
for column in db_left.column..=db_right.column {
|
||||||
|
let v = self.evaluate_cell(CellReferenceIndex {
|
||||||
|
sheet: db_left.sheet,
|
||||||
|
row: db_left.row,
|
||||||
|
column,
|
||||||
|
});
|
||||||
|
match &v {
|
||||||
|
CalcResult::String(s) => {
|
||||||
|
if s.to_lowercase() == field_column_name {
|
||||||
|
return Ok(column);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CalcResult::Number(n) => {
|
||||||
|
if field_column_name == n.to_string() {
|
||||||
|
return Ok(column);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CalcResult::Boolean(b) => {
|
||||||
|
if field_column_name == b.to_string() {
|
||||||
|
return Ok(column);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CalcResult::Error { .. }
|
||||||
|
| CalcResult::Range { .. }
|
||||||
|
| CalcResult::EmptyCell
|
||||||
|
| CalcResult::EmptyArg
|
||||||
|
| CalcResult::Array(_) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(CalcResult::Error {
|
||||||
|
error: Error::VALUE,
|
||||||
|
origin: cell,
|
||||||
|
message: "Field header not found".to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check whether a database row matches the criteria range.
|
||||||
|
/// Criteria logic: OR across criteria rows; AND across columns within a row.
|
||||||
|
fn db_row_matches_criteria(
|
||||||
|
&mut self,
|
||||||
|
db_left: CellReferenceIndex,
|
||||||
|
db_right: CellReferenceIndex,
|
||||||
|
row: i32,
|
||||||
|
criteria: (CellReferenceIndex, CellReferenceIndex),
|
||||||
|
) -> bool {
|
||||||
|
let (c_left, c_right) = criteria;
|
||||||
|
|
||||||
|
// Read criteria headers (first row of criteria range)
|
||||||
|
// Map header name (lowercased) -> db column (if exists)
|
||||||
|
let mut crit_cols: Vec<i32> = Vec::new();
|
||||||
|
let mut header_count = 0;
|
||||||
|
// We cover the criteria table:
|
||||||
|
// headerA | headerB | ...
|
||||||
|
// critA1 | critA2 | ...
|
||||||
|
// critB1 | critB2 | ...
|
||||||
|
// ...
|
||||||
|
for column in c_left.column..=c_right.column {
|
||||||
|
let cell = CellReferenceIndex {
|
||||||
|
sheet: c_left.sheet,
|
||||||
|
row: c_left.row,
|
||||||
|
column,
|
||||||
|
};
|
||||||
|
let criteria_header = self.evaluate_cell(cell);
|
||||||
|
if let Ok(s) = self.cast_to_string(criteria_header, cell) {
|
||||||
|
// Non-empty string header. If the header is non string we skip it
|
||||||
|
header_count += 1;
|
||||||
|
let wanted = s.to_lowercase();
|
||||||
|
|
||||||
|
// Find corresponding Database column
|
||||||
|
let mut found = false;
|
||||||
|
for db_column in db_left.column..=db_right.column {
|
||||||
|
let db_header = self.evaluate_cell(CellReferenceIndex {
|
||||||
|
sheet: db_left.sheet,
|
||||||
|
row: db_left.row,
|
||||||
|
column: db_column,
|
||||||
|
});
|
||||||
|
if let Ok(hs) = self.cast_to_string(db_header, cell) {
|
||||||
|
if hs.to_lowercase() == wanted {
|
||||||
|
crit_cols.push(db_column);
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
// that means the criteria column has no matching DB column
|
||||||
|
// If the criteria condition is empty then we remove this condition
|
||||||
|
// otherwise this condition can never be satisfied
|
||||||
|
// We evaluate all criteria rows to see if any is non-empty
|
||||||
|
let mut has_non_empty = false;
|
||||||
|
for r in (c_left.row + 1)..=c_right.row {
|
||||||
|
let ccell = self.evaluate_cell(CellReferenceIndex {
|
||||||
|
sheet: c_left.sheet,
|
||||||
|
row: r,
|
||||||
|
column,
|
||||||
|
});
|
||||||
|
if !matches!(ccell, CalcResult::EmptyCell | CalcResult::EmptyArg) {
|
||||||
|
has_non_empty = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if has_non_empty {
|
||||||
|
// This criteria column can never be satisfied
|
||||||
|
header_count -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if c_right.row <= c_left.row {
|
||||||
|
// If no criteria rows (only headers), everything matches
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if header_count == 0 {
|
||||||
|
// If there are not "String" headers, nothing matches
|
||||||
|
// NB: There might be String headers that do not match any DB columns,
|
||||||
|
// in that case everything matches.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evaluate each criteria row (OR)
|
||||||
|
for r in (c_left.row + 1)..=c_right.row {
|
||||||
|
// AND across columns for this criteria row
|
||||||
|
let mut and_ok = true;
|
||||||
|
|
||||||
|
for (offset, db_col) in crit_cols.iter().enumerate() {
|
||||||
|
// Criteria cell
|
||||||
|
let ccell = self.evaluate_cell(CellReferenceIndex {
|
||||||
|
sheet: c_left.sheet,
|
||||||
|
row: r,
|
||||||
|
column: c_left.column + offset as i32,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Empty criteria cell -> ignored
|
||||||
|
if matches!(ccell, CalcResult::EmptyCell | CalcResult::EmptyArg) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Database value for this row/column
|
||||||
|
let db_val = self.evaluate_cell(CellReferenceIndex {
|
||||||
|
sheet: db_left.sheet,
|
||||||
|
row,
|
||||||
|
column: *db_col,
|
||||||
|
});
|
||||||
|
|
||||||
|
if !self.criteria_cell_matches(&db_val, &ccell) {
|
||||||
|
and_ok = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if and_ok {
|
||||||
|
// This criteria row satisfied (OR)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// none matched
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implements Excel-like criteria matching for a single value.
|
||||||
|
/// Supports prefixes: <>, >=, <=, >, <, = ; wildcards * and ? for string equals.
|
||||||
|
fn criteria_cell_matches(&self, db_val: &CalcResult, crit_cell: &CalcResult) -> bool {
|
||||||
|
// Convert the criteria cell to a string for operator parsing if possible,
|
||||||
|
// otherwise fall back to equality via compare_values.
|
||||||
|
|
||||||
|
let mut criteria = match crit_cell {
|
||||||
|
CalcResult::String(s) => s.trim().to_string(),
|
||||||
|
CalcResult::Number(n) => {
|
||||||
|
// treat as equality with number
|
||||||
|
return match db_val {
|
||||||
|
CalcResult::Number(v) => (*v - *n).abs() <= f64::EPSILON,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
CalcResult::Boolean(b) => {
|
||||||
|
// check equality with boolean
|
||||||
|
return match db_val {
|
||||||
|
CalcResult::Boolean(v) => *v == *b,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(),
|
||||||
|
CalcResult::Error { .. } => return false,
|
||||||
|
CalcResult::Range { .. } | CalcResult::Array(_) => return false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Detect operator prefix
|
||||||
|
let mut op = "="; // default equality (with wildcard semantics for strings)
|
||||||
|
let prefixes = ["<>", ">=", "<=", ">", "<", "="];
|
||||||
|
for p in prefixes.iter() {
|
||||||
|
if criteria.starts_with(p) {
|
||||||
|
op = p;
|
||||||
|
criteria = criteria[p.len()..].trim().to_string();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is it a number?
|
||||||
|
let rhs_num = criteria.parse::<f64>().ok();
|
||||||
|
|
||||||
|
// Is it a date?
|
||||||
|
// FIXME: We should parse dates according to locale settings
|
||||||
|
let rhs_date = criteria.parse::<chrono::NaiveDate>().ok();
|
||||||
|
|
||||||
|
match op {
|
||||||
|
">" | ">=" | "<" | "<=" => {
|
||||||
|
if let Some(d) = rhs_date {
|
||||||
|
// date comparison
|
||||||
|
let serial = match date_to_serial_number(d.day(), d.month(), d.year()) {
|
||||||
|
Ok(sn) => sn as f64,
|
||||||
|
Err(_) => return false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let CalcResult::Number(n) = db_val {
|
||||||
|
match op {
|
||||||
|
">" => *n > serial,
|
||||||
|
">=" => *n >= serial,
|
||||||
|
"<" => *n < serial,
|
||||||
|
"<=" => *n <= serial,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
} else if let Some(t) = rhs_num {
|
||||||
|
// numeric comparison
|
||||||
|
if let CalcResult::Number(n) = db_val {
|
||||||
|
match op {
|
||||||
|
">" => *n > t,
|
||||||
|
">=" => *n >= t,
|
||||||
|
"<" => *n < t,
|
||||||
|
"<=" => *n <= t,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// string comparison (case-insensitive) using compare_values semantics
|
||||||
|
let rhs = CalcResult::String(criteria.to_lowercase());
|
||||||
|
let lhs = match db_val {
|
||||||
|
CalcResult::String(s) => CalcResult::String(s.to_lowercase()),
|
||||||
|
x => x.clone(),
|
||||||
|
};
|
||||||
|
let c = compare_values(&lhs, &rhs);
|
||||||
|
match op {
|
||||||
|
">" => c > 0,
|
||||||
|
">=" => c >= 0,
|
||||||
|
"<" => c < 0,
|
||||||
|
"<=" => c <= 0,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"<>" => {
|
||||||
|
// not equal (with wildcard semantics for strings)
|
||||||
|
// If rhs has wildcards and db_val is string, do regex; else use compare_values != 0
|
||||||
|
if let CalcResult::String(s) = db_val {
|
||||||
|
if criteria.contains('*') || criteria.contains('?') {
|
||||||
|
if let Ok(re) = from_wildcard_to_regex(&criteria.to_lowercase(), true) {
|
||||||
|
return !result_matches_regex(
|
||||||
|
&CalcResult::String(s.to_lowercase()),
|
||||||
|
&re,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let rhs = if let Some(n) = rhs_num {
|
||||||
|
CalcResult::Number(n)
|
||||||
|
} else {
|
||||||
|
CalcResult::String(criteria.to_lowercase())
|
||||||
|
};
|
||||||
|
let lhs = match db_val {
|
||||||
|
CalcResult::String(s) => CalcResult::String(s.to_lowercase()),
|
||||||
|
x => x.clone(),
|
||||||
|
};
|
||||||
|
compare_values(&lhs, &rhs) != 0
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// equality. For strings, support wildcards (*, ?)
|
||||||
|
if let Some(n) = rhs_num {
|
||||||
|
// numeric equals
|
||||||
|
if let CalcResult::Number(m) = db_val {
|
||||||
|
(*m - n).abs() <= f64::EPSILON
|
||||||
|
} else {
|
||||||
|
compare_values(db_val, &CalcResult::Number(n)) == 0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// textual/boolean equals (case-insensitive), wildcard-enabled for strings
|
||||||
|
if let CalcResult::String(s) = db_val {
|
||||||
|
if criteria.contains('*') || criteria.contains('?') {
|
||||||
|
if let Ok(re) = from_wildcard_to_regex(&criteria.to_lowercase(), true) {
|
||||||
|
return result_matches_regex(
|
||||||
|
&CalcResult::String(s.to_lowercase()),
|
||||||
|
&re,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// This is weird but we only need to check if "starts with" for equality
|
||||||
|
return s.to_lowercase().starts_with(&criteria.to_lowercase());
|
||||||
|
}
|
||||||
|
// Fallback: compare_values equality
|
||||||
|
compare_values(db_val, &CalcResult::String(criteria.to_lowercase())) == 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shared implementation for DMAX/DMIN
|
||||||
|
fn db_extreme(
|
||||||
|
&mut self,
|
||||||
|
args: &[Node],
|
||||||
|
cell: CellReferenceIndex,
|
||||||
|
want_max: bool,
|
||||||
|
) -> CalcResult {
|
||||||
|
if args.len() != 3 {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let (db_left, db_right) = match self.get_reference(&args[0], cell) {
|
||||||
|
Ok(r) => (r.left, r.right),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let field_col = match self.resolve_db_field_column(db_left, db_right, &args[1], cell) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let criteria = match self.get_reference(&args[2], cell) {
|
||||||
|
Ok(r) => (r.left, r.right),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
if db_right.row <= db_left.row {
|
||||||
|
// no data rows
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::VALUE,
|
||||||
|
origin: cell,
|
||||||
|
message: "No data rows in database".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut best: Option<f64> = None;
|
||||||
|
|
||||||
|
let mut row = db_left.row + 1;
|
||||||
|
while row <= db_right.row {
|
||||||
|
if self.db_row_matches_criteria(db_left, db_right, row, criteria) {
|
||||||
|
let v = self.evaluate_cell(CellReferenceIndex {
|
||||||
|
sheet: db_left.sheet,
|
||||||
|
row,
|
||||||
|
column: field_col,
|
||||||
|
});
|
||||||
|
if let CalcResult::Number(value) = v {
|
||||||
|
if value.is_finite() {
|
||||||
|
best = Some(match best {
|
||||||
|
None => value,
|
||||||
|
Some(cur) => {
|
||||||
|
if want_max {
|
||||||
|
value.max(cur)
|
||||||
|
} else {
|
||||||
|
value.min(cur)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
row += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
match best {
|
||||||
|
Some(v) => CalcResult::Number(v),
|
||||||
|
None => CalcResult::Number(0.0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,26 @@ use chrono::Timelike;
|
|||||||
const SECONDS_PER_DAY: i32 = 86_400;
|
const SECONDS_PER_DAY: i32 = 86_400;
|
||||||
const SECONDS_PER_DAY_F64: f64 = SECONDS_PER_DAY as f64;
|
const SECONDS_PER_DAY_F64: f64 = SECONDS_PER_DAY as f64;
|
||||||
|
|
||||||
|
fn is_leap_year(year: i32) -> bool {
|
||||||
|
(year % 4 == 0) && (year % 100 != 0 || year % 400 == 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_feb_29_between_dates(start: chrono::NaiveDate, end: chrono::NaiveDate) -> bool {
|
||||||
|
let start_year = start.year();
|
||||||
|
let end_year = end.year();
|
||||||
|
|
||||||
|
for year in start_year..=end_year {
|
||||||
|
if is_leap_year(year)
|
||||||
|
&& (year < end_year
|
||||||
|
|| (year == end_year && end.month() > 2)
|
||||||
|
&& (year > start_year || (year == start_year && start.month() <= 2)))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Helper macros to eliminate boilerplate in date/time component extraction
|
// Helper macros to eliminate boilerplate in date/time component extraction
|
||||||
// functions (DAY, MONTH, YEAR, HOUR, MINUTE, SECOND).
|
// functions (DAY, MONTH, YEAR, HOUR, MINUTE, SECOND).
|
||||||
@@ -1567,18 +1587,44 @@ impl Model {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
1 => {
|
1 => {
|
||||||
let year_days = if start_date.year() == end_date.year() {
|
// Procedure E
|
||||||
if (start_date.year() % 4 == 0 && start_date.year() % 100 != 0)
|
|
||||||
|| start_date.year() % 400 == 0
|
let start_year = start_date.year();
|
||||||
{
|
let end_year = end_date.year();
|
||||||
366.0
|
|
||||||
} else {
|
let step_a = start_year != end_year;
|
||||||
365.0
|
let step_b = start_year + 1 != end_year;
|
||||||
|
let step_c = start_date.month() < end_date.month();
|
||||||
|
let step_d = start_date.month() == end_date.month();
|
||||||
|
let step_e = start_date.day() <= end_date.day();
|
||||||
|
let step_f = step_a && (step_b || step_c || (step_d && step_e));
|
||||||
|
if step_f {
|
||||||
|
// 7.
|
||||||
|
// return average of days in year between start_year and end_year, inclusive
|
||||||
|
let mut total_days = 0;
|
||||||
|
for year in start_year..=end_year {
|
||||||
|
if is_leap_year(year) {
|
||||||
|
total_days += 366;
|
||||||
|
} else {
|
||||||
|
total_days += 365;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
days / (total_days as f64 / (end_year - start_year + 1) as f64)
|
||||||
|
} else if step_a && is_leap_year(start_year) {
|
||||||
|
// 8.
|
||||||
|
days / 366.0
|
||||||
|
} else if is_feb_29_between_dates(start_date, end_date) {
|
||||||
|
// 9. If a February 29 occurs between date1 and date2 then return 366
|
||||||
|
days / 366.0
|
||||||
|
} else if end_date.month() == 2 && end_date.day() == 29 {
|
||||||
|
// 10. If date2 is February 29 then return 366
|
||||||
|
days / 366.0
|
||||||
|
} else if !step_a && is_leap_year(start_year) {
|
||||||
|
days / 366.0
|
||||||
} else {
|
} else {
|
||||||
365.0
|
// 11.
|
||||||
};
|
days / 365.0
|
||||||
days / year_days
|
}
|
||||||
}
|
}
|
||||||
2 => days / 360.0,
|
2 => days / 360.0,
|
||||||
3 => days / 365.0,
|
3 => days / 365.0,
|
||||||
@@ -1595,6 +1641,34 @@ impl Model {
|
|||||||
}
|
}
|
||||||
_ => return CalcResult::new_error(Error::NUM, cell, "Invalid basis".to_string()),
|
_ => return CalcResult::new_error(Error::NUM, cell, "Invalid basis".to_string()),
|
||||||
};
|
};
|
||||||
CalcResult::Number(result)
|
CalcResult::Number(result.abs())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_is_leap_year() {
|
||||||
|
assert!(is_leap_year(2000));
|
||||||
|
assert!(!is_leap_year(1900));
|
||||||
|
assert!(is_leap_year(2004));
|
||||||
|
assert!(!is_leap_year(2001));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_is_feb_29_between_dates() {
|
||||||
|
let d1 = chrono::NaiveDate::from_ymd_opt(2020, 2, 28).unwrap();
|
||||||
|
let d2 = chrono::NaiveDate::from_ymd_opt(2020, 3, 1).unwrap();
|
||||||
|
assert!(is_feb_29_between_dates(d1, d2));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_is_feb_29_between_dates_false() {
|
||||||
|
let d1 = chrono::NaiveDate::from_ymd_opt(2021, 2, 28).unwrap();
|
||||||
|
let d2 = chrono::NaiveDate::from_ymd_opt(2021, 3, 1).unwrap();
|
||||||
|
assert!(!is_feb_29_between_dates(d1, d2));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
|
use statrs::function::erf::{erf, erfc};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
calc_result::CalcResult,
|
calc_result::CalcResult,
|
||||||
expressions::{parser::Node, token::Error, types::CellReferenceIndex},
|
expressions::{parser::Node, token::Error, types::CellReferenceIndex},
|
||||||
model::Model,
|
model::Model,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::transcendental::{bessel_i, bessel_j, bessel_k, bessel_y, erf};
|
use super::transcendental::{bessel_i, bessel_j, bessel_k, bessel_y};
|
||||||
// https://root.cern/doc/v610/TMath_8cxx_source.html
|
// https://root.cern/doc/v610/TMath_8cxx_source.html
|
||||||
|
|
||||||
// Notice that the parameters for Bessel functions in Excel and here have inverted order
|
// Notice that the parameters for Bessel functions in Excel and here have inverted order
|
||||||
@@ -160,7 +162,7 @@ impl Model {
|
|||||||
Ok(f) => f,
|
Ok(f) => f,
|
||||||
Err(s) => return s,
|
Err(s) => return s,
|
||||||
};
|
};
|
||||||
CalcResult::Number(1.0 - erf(x))
|
CalcResult::Number(erfc(x))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn fn_erfcprecise(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
pub(crate) fn fn_erfcprecise(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
@@ -171,6 +173,6 @@ impl Model {
|
|||||||
Ok(f) => f,
|
Ok(f) => f,
|
||||||
Err(s) => return s,
|
Err(s) => return s,
|
||||||
};
|
};
|
||||||
CalcResult::Number(1.0 - erf(x))
|
CalcResult::Number(erfc(x))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
pub(crate) fn erf(x: f64) -> f64 {
|
|
||||||
let cof = vec![
|
|
||||||
-1.3026537197817094,
|
|
||||||
6.419_697_923_564_902e-1,
|
|
||||||
1.9476473204185836e-2,
|
|
||||||
-9.561_514_786_808_63e-3,
|
|
||||||
-9.46595344482036e-4,
|
|
||||||
3.66839497852761e-4,
|
|
||||||
4.2523324806907e-5,
|
|
||||||
-2.0278578112534e-5,
|
|
||||||
-1.624290004647e-6,
|
|
||||||
1.303655835580e-6,
|
|
||||||
1.5626441722e-8,
|
|
||||||
-8.5238095915e-8,
|
|
||||||
6.529054439e-9,
|
|
||||||
5.059343495e-9,
|
|
||||||
-9.91364156e-10,
|
|
||||||
-2.27365122e-10,
|
|
||||||
9.6467911e-11,
|
|
||||||
2.394038e-12,
|
|
||||||
-6.886027e-12,
|
|
||||||
8.94487e-13,
|
|
||||||
3.13092e-13,
|
|
||||||
-1.12708e-13,
|
|
||||||
3.81e-16,
|
|
||||||
7.106e-15,
|
|
||||||
-1.523e-15,
|
|
||||||
-9.4e-17,
|
|
||||||
1.21e-16,
|
|
||||||
-2.8e-17,
|
|
||||||
];
|
|
||||||
|
|
||||||
let mut d = 0.0;
|
|
||||||
let mut dd = 0.0;
|
|
||||||
|
|
||||||
let x_abs = x.abs();
|
|
||||||
|
|
||||||
let t = 2.0 / (2.0 + x_abs);
|
|
||||||
let ty = 4.0 * t - 2.0;
|
|
||||||
|
|
||||||
for j in (1..=cof.len() - 1).rev() {
|
|
||||||
let tmp = d;
|
|
||||||
d = ty * d - dd + cof[j];
|
|
||||||
dd = tmp;
|
|
||||||
}
|
|
||||||
|
|
||||||
let res = t * f64::exp(-x_abs * x_abs + 0.5 * (cof[0] + ty * d) - dd);
|
|
||||||
if x < 0.0 {
|
|
||||||
res - 1.0
|
|
||||||
} else {
|
|
||||||
1.0 - res
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,6 @@ mod bessel_j1_y1;
|
|||||||
mod bessel_jn_yn;
|
mod bessel_jn_yn;
|
||||||
mod bessel_k;
|
mod bessel_k;
|
||||||
mod bessel_util;
|
mod bessel_util;
|
||||||
mod erf;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test_bessel;
|
mod test_bessel;
|
||||||
@@ -13,4 +12,3 @@ pub(crate) use bessel_i::bessel_i;
|
|||||||
pub(crate) use bessel_jn_yn::jn as bessel_j;
|
pub(crate) use bessel_jn_yn::jn as bessel_j;
|
||||||
pub(crate) use bessel_jn_yn::yn as bessel_y;
|
pub(crate) use bessel_jn_yn::yn as bessel_y;
|
||||||
pub(crate) use bessel_k::bessel_k;
|
pub(crate) use bessel_k::bessel_k;
|
||||||
pub(crate) use erf::erf;
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -68,14 +68,14 @@ macro_rules! single_number_fn {
|
|||||||
},
|
},
|
||||||
// If String, parse to f64 then apply or #VALUE! error
|
// If String, parse to f64 then apply or #VALUE! error
|
||||||
ArrayNode::String(s) => {
|
ArrayNode::String(s) => {
|
||||||
let node = match s.parse::<f64>() {
|
let node = match self.cast_number(&s) {
|
||||||
Ok(f) => match $op(f) {
|
Some(f) => match $op(f) {
|
||||||
Ok(x) => ArrayNode::Number(x),
|
Ok(x) => ArrayNode::Number(x),
|
||||||
Err(Error::DIV) => ArrayNode::Error(Error::DIV),
|
Err(Error::DIV) => ArrayNode::Error(Error::DIV),
|
||||||
Err(Error::VALUE) => ArrayNode::Error(Error::VALUE),
|
Err(Error::VALUE) => ArrayNode::Error(Error::VALUE),
|
||||||
Err(e) => ArrayNode::Error(e),
|
Err(e) => ArrayNode::Error(e),
|
||||||
},
|
},
|
||||||
Err(_) => ArrayNode::Error(Error::VALUE),
|
None => ArrayNode::Error(Error::VALUE),
|
||||||
};
|
};
|
||||||
data_row.push(node);
|
data_row.push(node);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -934,11 +934,11 @@ impl Model {
|
|||||||
if args.len() != 2 {
|
if args.len() != 2 {
|
||||||
return CalcResult::new_args_number_error(cell);
|
return CalcResult::new_args_number_error(cell);
|
||||||
}
|
}
|
||||||
let value = match self.get_number(&args[0], cell) {
|
let value = match self.get_number_no_bools(&args[0], cell) {
|
||||||
Ok(f) => f,
|
Ok(f) => f,
|
||||||
Err(s) => return s,
|
Err(s) => return s,
|
||||||
};
|
};
|
||||||
let divisor = match self.get_number(&args[1], cell) {
|
let divisor = match self.get_number_no_bools(&args[1], cell) {
|
||||||
Ok(f) => f,
|
Ok(f) => f,
|
||||||
Err(s) => return s,
|
Err(s) => return s,
|
||||||
};
|
};
|
||||||
@@ -1163,11 +1163,34 @@ impl Model {
|
|||||||
if args.len() != 2 {
|
if args.len() != 2 {
|
||||||
return CalcResult::new_args_number_error(cell);
|
return CalcResult::new_args_number_error(cell);
|
||||||
}
|
}
|
||||||
let value = match self.get_number(&args[0], cell) {
|
|
||||||
|
let value = self.evaluate_node_in_context(&args[0], cell);
|
||||||
|
|
||||||
|
let multiple = self.evaluate_node_in_context(&args[1], cell);
|
||||||
|
|
||||||
|
// if either is empty => #N/A
|
||||||
|
if matches!(value, CalcResult::EmptyArg) || matches!(multiple, CalcResult::EmptyArg) {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NA,
|
||||||
|
origin: cell,
|
||||||
|
message: "Bad argument for MROUND".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Booleans are not cast
|
||||||
|
if matches!(value, CalcResult::Boolean(_)) {
|
||||||
|
return CalcResult::new_error(Error::VALUE, cell, "Expecting number".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if matches!(multiple, CalcResult::Boolean(_)) {
|
||||||
|
return CalcResult::new_error(Error::VALUE, cell, "Expecting number".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let value = match self.cast_to_number(value, cell) {
|
||||||
Ok(f) => f,
|
Ok(f) => f,
|
||||||
Err(s) => return s,
|
Err(s) => return s,
|
||||||
};
|
};
|
||||||
let multiple = match self.get_number(&args[1], cell) {
|
let multiple = match self.cast_to_number(multiple, cell) {
|
||||||
Ok(f) => f,
|
Ok(f) => f,
|
||||||
Err(s) => return s,
|
Err(s) => return s,
|
||||||
};
|
};
|
||||||
@@ -1210,11 +1233,15 @@ impl Model {
|
|||||||
if !(-15.0..=15.0).contains(&num_digits) {
|
if !(-15.0..=15.0).contains(&num_digits) {
|
||||||
return CalcResult::Number(value);
|
return CalcResult::Number(value);
|
||||||
}
|
}
|
||||||
CalcResult::Number(if value >= 0.0 {
|
let v = if value >= 0.0 {
|
||||||
f64::floor(value * 10f64.powf(num_digits)) / 10f64.powf(num_digits)
|
f64::floor(value * 10f64.powf(num_digits)) / 10f64.powf(num_digits)
|
||||||
} else {
|
} else {
|
||||||
f64::ceil(value * 10f64.powf(num_digits)) / 10f64.powf(num_digits)
|
f64::ceil(value * 10f64.powf(num_digits)) / 10f64.powf(num_digits)
|
||||||
})
|
};
|
||||||
|
if value.is_finite() && v.is_infinite() {
|
||||||
|
return CalcResult::Number(value);
|
||||||
|
}
|
||||||
|
CalcResult::Number(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
single_number_fn!(fn_log10, |f| if f <= 0.0 {
|
single_number_fn!(fn_log10, |f| if f <= 0.0 {
|
||||||
@@ -1250,13 +1277,19 @@ impl Model {
|
|||||||
} else {
|
} else {
|
||||||
Ok((f * PI).sqrt())
|
Ok((f * PI).sqrt())
|
||||||
});
|
});
|
||||||
single_number_fn!(fn_acot, |f| if f == 0.0 {
|
|
||||||
Err(Error::DIV)
|
single_number_fn!(fn_acot, |f| {
|
||||||
} else {
|
let v = f64::atan(1.0 / f);
|
||||||
Ok(f64::atan(1.0 / f))
|
if f >= 0.0 {
|
||||||
|
Ok(v)
|
||||||
|
} else {
|
||||||
|
// To be compatible with Excel we need a different branch
|
||||||
|
// when f < 0
|
||||||
|
Ok(v + PI)
|
||||||
|
}
|
||||||
});
|
});
|
||||||
single_number_fn!(fn_acoth, |f: f64| if f.abs() == 1.0 {
|
single_number_fn!(fn_acoth, |f: f64| if f.abs() == 1.0 {
|
||||||
Err(Error::DIV)
|
Err(Error::NUM)
|
||||||
} else {
|
} else {
|
||||||
Ok(0.5 * (f64::ln((f + 1.0) / (f - 1.0))))
|
Ok(0.5 * (f64::ln((f + 1.0) / (f - 1.0))))
|
||||||
});
|
});
|
||||||
@@ -1265,8 +1298,11 @@ impl Model {
|
|||||||
} else {
|
} else {
|
||||||
Ok(f64::cos(f) / f64::sin(f))
|
Ok(f64::cos(f) / f64::sin(f))
|
||||||
});
|
});
|
||||||
single_number_fn!(fn_coth, |f| if f == 0.0 {
|
single_number_fn!(fn_coth, |f: f64| if f == 0.0 {
|
||||||
Err(Error::DIV)
|
Err(Error::DIV)
|
||||||
|
} else if f.abs() > 20.0 {
|
||||||
|
// for values > 20.0 this is exact in f64
|
||||||
|
Ok(f.signum())
|
||||||
} else {
|
} else {
|
||||||
Ok(f64::cosh(f) / f64::sinh(f))
|
Ok(f64::cosh(f) / f64::sinh(f))
|
||||||
});
|
});
|
||||||
@@ -1280,16 +1316,8 @@ impl Model {
|
|||||||
} else {
|
} else {
|
||||||
Ok(1.0 / f64::sinh(f))
|
Ok(1.0 / f64::sinh(f))
|
||||||
});
|
});
|
||||||
single_number_fn!(fn_sec, |f| if f == 0.0 {
|
single_number_fn!(fn_sec, |f| Ok(1.0 / f64::cos(f)));
|
||||||
Err(Error::DIV)
|
single_number_fn!(fn_sech, |f| Ok(1.0 / f64::cosh(f)));
|
||||||
} else {
|
|
||||||
Ok(1.0 / f64::cos(f))
|
|
||||||
});
|
|
||||||
single_number_fn!(fn_sech, |f| if f == 0.0 {
|
|
||||||
Err(Error::DIV)
|
|
||||||
} else {
|
|
||||||
Ok(1.0 / f64::cosh(f))
|
|
||||||
});
|
|
||||||
single_number_fn!(fn_exp, |f: f64| Ok(f64::exp(f)));
|
single_number_fn!(fn_exp, |f: f64| Ok(f64::exp(f)));
|
||||||
single_number_fn!(fn_fact, |x: f64| {
|
single_number_fn!(fn_fact, |x: f64| {
|
||||||
let x = x.floor();
|
let x = x.floor();
|
||||||
@@ -1320,7 +1348,13 @@ impl Model {
|
|||||||
}
|
}
|
||||||
Ok(acc)
|
Ok(acc)
|
||||||
});
|
});
|
||||||
single_number_fn!(fn_sign, |f| Ok(f64::signum(f)));
|
single_number_fn!(fn_sign, |f| {
|
||||||
|
if f == 0.0 {
|
||||||
|
Ok(0.0)
|
||||||
|
} else {
|
||||||
|
Ok(f64::signum(f))
|
||||||
|
}
|
||||||
|
});
|
||||||
single_number_fn!(fn_degrees, |f| Ok(f * (180.0 / PI)));
|
single_number_fn!(fn_degrees, |f| Ok(f * (180.0 / PI)));
|
||||||
single_number_fn!(fn_radians, |f| Ok(f * (PI / 180.0)));
|
single_number_fn!(fn_radians, |f| Ok(f * (PI / 180.0)));
|
||||||
single_number_fn!(fn_odd, |f| {
|
single_number_fn!(fn_odd, |f| {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) mod binary_search;
|
pub(crate) mod binary_search;
|
||||||
|
mod database;
|
||||||
mod date_and_time;
|
mod date_and_time;
|
||||||
mod engineering;
|
mod engineering;
|
||||||
mod financial;
|
mod financial;
|
||||||
@@ -217,14 +218,6 @@ pub enum Function {
|
|||||||
Isoweeknum,
|
Isoweeknum,
|
||||||
|
|
||||||
// Financial
|
// Financial
|
||||||
Accrint,
|
|
||||||
Accrintm,
|
|
||||||
Coupdaybs,
|
|
||||||
Coupdays,
|
|
||||||
Coupdaysnc,
|
|
||||||
Coupncd,
|
|
||||||
Coupnum,
|
|
||||||
Couppcd,
|
|
||||||
Cumipmt,
|
Cumipmt,
|
||||||
Cumprinc,
|
Cumprinc,
|
||||||
Db,
|
Db,
|
||||||
@@ -233,7 +226,6 @@ pub enum Function {
|
|||||||
Dollarfr,
|
Dollarfr,
|
||||||
Effect,
|
Effect,
|
||||||
Fv,
|
Fv,
|
||||||
Fvschedule,
|
|
||||||
Ipmt,
|
Ipmt,
|
||||||
Irr,
|
Irr,
|
||||||
Ispmt,
|
Ispmt,
|
||||||
@@ -241,12 +233,9 @@ pub enum Function {
|
|||||||
Nominal,
|
Nominal,
|
||||||
Nper,
|
Nper,
|
||||||
Npv,
|
Npv,
|
||||||
Duration,
|
|
||||||
Mduration,
|
|
||||||
Pduration,
|
Pduration,
|
||||||
Pmt,
|
Pmt,
|
||||||
Ppmt,
|
Ppmt,
|
||||||
Price,
|
|
||||||
Pv,
|
Pv,
|
||||||
Rate,
|
Rate,
|
||||||
Rri,
|
Rri,
|
||||||
@@ -255,16 +244,8 @@ pub enum Function {
|
|||||||
Tbilleq,
|
Tbilleq,
|
||||||
Tbillprice,
|
Tbillprice,
|
||||||
Tbillyield,
|
Tbillyield,
|
||||||
Pricedisc,
|
|
||||||
Pricemat,
|
|
||||||
Yielddisc,
|
|
||||||
Yieldmat,
|
|
||||||
Disc,
|
|
||||||
Received,
|
|
||||||
Intrate,
|
|
||||||
Xirr,
|
Xirr,
|
||||||
Xnpv,
|
Xnpv,
|
||||||
Yield,
|
|
||||||
|
|
||||||
// Engineering: Bessel and transcendental functions
|
// Engineering: Bessel and transcendental functions
|
||||||
Besseli,
|
Besseli,
|
||||||
@@ -330,10 +311,24 @@ pub enum Function {
|
|||||||
Delta,
|
Delta,
|
||||||
Gestep,
|
Gestep,
|
||||||
Subtotal,
|
Subtotal,
|
||||||
|
|
||||||
|
// Database
|
||||||
|
Daverage,
|
||||||
|
Dcount,
|
||||||
|
Dget,
|
||||||
|
Dmax,
|
||||||
|
Dmin,
|
||||||
|
Dsum,
|
||||||
|
Dcounta,
|
||||||
|
Dproduct,
|
||||||
|
Dstdev,
|
||||||
|
Dvar,
|
||||||
|
Dvarp,
|
||||||
|
Dstdevp,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Function {
|
impl Function {
|
||||||
pub fn into_iter() -> IntoIter<Function, 276> {
|
pub fn into_iter() -> IntoIter<Function, 268> {
|
||||||
[
|
[
|
||||||
Function::And,
|
Function::And,
|
||||||
Function::False,
|
Function::False,
|
||||||
@@ -494,23 +489,18 @@ impl Function {
|
|||||||
Function::WorkdayIntl,
|
Function::WorkdayIntl,
|
||||||
Function::Yearfrac,
|
Function::Yearfrac,
|
||||||
Function::Isoweeknum,
|
Function::Isoweeknum,
|
||||||
Function::Accrint,
|
|
||||||
Function::Accrintm,
|
|
||||||
Function::Pmt,
|
Function::Pmt,
|
||||||
Function::Pv,
|
Function::Pv,
|
||||||
Function::Rate,
|
Function::Rate,
|
||||||
Function::Nper,
|
Function::Nper,
|
||||||
Function::Fv,
|
Function::Fv,
|
||||||
Function::Fvschedule,
|
|
||||||
Function::Ppmt,
|
Function::Ppmt,
|
||||||
Function::Price,
|
|
||||||
Function::Ipmt,
|
Function::Ipmt,
|
||||||
Function::Npv,
|
Function::Npv,
|
||||||
Function::Mirr,
|
Function::Mirr,
|
||||||
Function::Irr,
|
Function::Irr,
|
||||||
Function::Xirr,
|
Function::Xirr,
|
||||||
Function::Xnpv,
|
Function::Xnpv,
|
||||||
Function::Yield,
|
|
||||||
Function::Rept,
|
Function::Rept,
|
||||||
Function::Textafter,
|
Function::Textafter,
|
||||||
Function::Textbefore,
|
Function::Textbefore,
|
||||||
@@ -522,25 +512,10 @@ impl Function {
|
|||||||
Function::Syd,
|
Function::Syd,
|
||||||
Function::Nominal,
|
Function::Nominal,
|
||||||
Function::Effect,
|
Function::Effect,
|
||||||
Function::Duration,
|
|
||||||
Function::Mduration,
|
|
||||||
Function::Pduration,
|
Function::Pduration,
|
||||||
Function::Coupdaybs,
|
|
||||||
Function::Coupdays,
|
|
||||||
Function::Coupdaysnc,
|
|
||||||
Function::Coupncd,
|
|
||||||
Function::Coupnum,
|
|
||||||
Function::Couppcd,
|
|
||||||
Function::Tbillyield,
|
Function::Tbillyield,
|
||||||
Function::Tbillprice,
|
Function::Tbillprice,
|
||||||
Function::Tbilleq,
|
Function::Tbilleq,
|
||||||
Function::Pricedisc,
|
|
||||||
Function::Pricemat,
|
|
||||||
Function::Yielddisc,
|
|
||||||
Function::Yieldmat,
|
|
||||||
Function::Disc,
|
|
||||||
Function::Received,
|
|
||||||
Function::Intrate,
|
|
||||||
Function::Dollarde,
|
Function::Dollarde,
|
||||||
Function::Dollarfr,
|
Function::Dollarfr,
|
||||||
Function::Ddb,
|
Function::Ddb,
|
||||||
@@ -611,6 +586,18 @@ impl Function {
|
|||||||
Function::Cell,
|
Function::Cell,
|
||||||
Function::Info,
|
Function::Info,
|
||||||
Function::Sheets,
|
Function::Sheets,
|
||||||
|
Function::Daverage,
|
||||||
|
Function::Dcount,
|
||||||
|
Function::Dget,
|
||||||
|
Function::Dmax,
|
||||||
|
Function::Dmin,
|
||||||
|
Function::Dsum,
|
||||||
|
Function::Dcounta,
|
||||||
|
Function::Dproduct,
|
||||||
|
Function::Dstdev,
|
||||||
|
Function::Dvar,
|
||||||
|
Function::Dvarp,
|
||||||
|
Function::Dstdevp,
|
||||||
]
|
]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
}
|
}
|
||||||
@@ -664,6 +651,14 @@ impl Function {
|
|||||||
Function::Arabic => "_xlfn.ARABIC".to_string(),
|
Function::Arabic => "_xlfn.ARABIC".to_string(),
|
||||||
Function::Combina => "_xlfn.COMBINA".to_string(),
|
Function::Combina => "_xlfn.COMBINA".to_string(),
|
||||||
Function::Sheets => "_xlfn.SHEETS".to_string(),
|
Function::Sheets => "_xlfn.SHEETS".to_string(),
|
||||||
|
Function::Acoth => "_xlfn.ACOTH".to_string(),
|
||||||
|
Function::Cot => "_xlfn.COT".to_string(),
|
||||||
|
Function::Coth => "_xlfn.COTH".to_string(),
|
||||||
|
Function::Csc => "_xlfn.CSC".to_string(),
|
||||||
|
Function::Csch => "_xlfn.CSCH".to_string(),
|
||||||
|
Function::Sec => "_xlfn.SEC".to_string(),
|
||||||
|
Function::Sech => "_xlfn.SECH".to_string(),
|
||||||
|
Function::Acot => "_xlfn.ACOT".to_string(),
|
||||||
|
|
||||||
_ => self.to_string(),
|
_ => self.to_string(),
|
||||||
}
|
}
|
||||||
@@ -699,14 +694,14 @@ impl Function {
|
|||||||
"ASINH" => Some(Function::Asinh),
|
"ASINH" => Some(Function::Asinh),
|
||||||
"ACOSH" => Some(Function::Acosh),
|
"ACOSH" => Some(Function::Acosh),
|
||||||
"ATANH" => Some(Function::Atanh),
|
"ATANH" => Some(Function::Atanh),
|
||||||
"ACOT" => Some(Function::Acot),
|
"ACOT" | "_XLFN.ACOT" => Some(Function::Acot),
|
||||||
"COTH" => Some(Function::Coth),
|
"COTH" | "_XLFN.COTH" => Some(Function::Coth),
|
||||||
"COT" => Some(Function::Cot),
|
"COT" | "_XLFN.COT" => Some(Function::Cot),
|
||||||
"CSC" => Some(Function::Csc),
|
"CSC" | "_XLFN.CSC" => Some(Function::Csc),
|
||||||
"CSCH" => Some(Function::Csch),
|
"CSCH" | "_XLFN.CSCH" => Some(Function::Csch),
|
||||||
"SEC" => Some(Function::Sec),
|
"SEC" | "_XLFN.SEC" => Some(Function::Sec),
|
||||||
"SECH" => Some(Function::Sech),
|
"SECH" | "_XLFN.SECH" => Some(Function::Sech),
|
||||||
"ACOTH" => Some(Function::Acoth),
|
"ACOTH" | "_XLFN.ACOTH" => Some(Function::Acoth),
|
||||||
"FACT" => Some(Function::Fact),
|
"FACT" => Some(Function::Fact),
|
||||||
"FACTDOUBLE" => Some(Function::Factdouble),
|
"FACTDOUBLE" => Some(Function::Factdouble),
|
||||||
"EXP" => Some(Function::Exp),
|
"EXP" => Some(Function::Exp),
|
||||||
@@ -853,20 +848,15 @@ impl Function {
|
|||||||
"YEARFRAC" => Some(Function::Yearfrac),
|
"YEARFRAC" => Some(Function::Yearfrac),
|
||||||
"ISOWEEKNUM" | "_XLFN.ISOWEEKNUM" => Some(Function::Isoweeknum),
|
"ISOWEEKNUM" | "_XLFN.ISOWEEKNUM" => Some(Function::Isoweeknum),
|
||||||
// Financial
|
// Financial
|
||||||
"ACCRINT" => Some(Function::Accrint),
|
|
||||||
"ACCRINTM" => Some(Function::Accrintm),
|
|
||||||
"PMT" => Some(Function::Pmt),
|
"PMT" => Some(Function::Pmt),
|
||||||
"PV" => Some(Function::Pv),
|
"PV" => Some(Function::Pv),
|
||||||
"RATE" => Some(Function::Rate),
|
"RATE" => Some(Function::Rate),
|
||||||
"NPER" => Some(Function::Nper),
|
"NPER" => Some(Function::Nper),
|
||||||
"FV" => Some(Function::Fv),
|
"FV" => Some(Function::Fv),
|
||||||
"FVSCHEDULE" => Some(Function::Fvschedule),
|
|
||||||
"PPMT" => Some(Function::Ppmt),
|
"PPMT" => Some(Function::Ppmt),
|
||||||
"PRICE" => Some(Function::Price),
|
|
||||||
"IPMT" => Some(Function::Ipmt),
|
"IPMT" => Some(Function::Ipmt),
|
||||||
"NPV" => Some(Function::Npv),
|
"NPV" => Some(Function::Npv),
|
||||||
"XNPV" => Some(Function::Xnpv),
|
"XNPV" => Some(Function::Xnpv),
|
||||||
"YIELD" => Some(Function::Yield),
|
|
||||||
"MIRR" => Some(Function::Mirr),
|
"MIRR" => Some(Function::Mirr),
|
||||||
"IRR" => Some(Function::Irr),
|
"IRR" => Some(Function::Irr),
|
||||||
"XIRR" => Some(Function::Xirr),
|
"XIRR" => Some(Function::Xirr),
|
||||||
@@ -877,27 +867,11 @@ impl Function {
|
|||||||
"SYD" => Some(Function::Syd),
|
"SYD" => Some(Function::Syd),
|
||||||
"NOMINAL" => Some(Function::Nominal),
|
"NOMINAL" => Some(Function::Nominal),
|
||||||
"EFFECT" => Some(Function::Effect),
|
"EFFECT" => Some(Function::Effect),
|
||||||
"DURATION" => Some(Function::Duration),
|
|
||||||
"MDURATION" => Some(Function::Mduration),
|
|
||||||
"PDURATION" | "_XLFN.PDURATION" => Some(Function::Pduration),
|
"PDURATION" | "_XLFN.PDURATION" => Some(Function::Pduration),
|
||||||
|
|
||||||
"COUPDAYBS" => Some(Function::Coupdaybs),
|
|
||||||
"COUPDAYS" => Some(Function::Coupdays),
|
|
||||||
"COUPDAYSNC" => Some(Function::Coupdaysnc),
|
|
||||||
"COUPNCD" => Some(Function::Coupncd),
|
|
||||||
"COUPNUM" => Some(Function::Coupnum),
|
|
||||||
"COUPPCD" => Some(Function::Couppcd),
|
|
||||||
|
|
||||||
"TBILLYIELD" => Some(Function::Tbillyield),
|
"TBILLYIELD" => Some(Function::Tbillyield),
|
||||||
"TBILLPRICE" => Some(Function::Tbillprice),
|
"TBILLPRICE" => Some(Function::Tbillprice),
|
||||||
"TBILLEQ" => Some(Function::Tbilleq),
|
"TBILLEQ" => Some(Function::Tbilleq),
|
||||||
"PRICEDISC" => Some(Function::Pricedisc),
|
|
||||||
"PRICEMAT" => Some(Function::Pricemat),
|
|
||||||
"YIELDDISC" => Some(Function::Yielddisc),
|
|
||||||
"YIELDMAT" => Some(Function::Yieldmat),
|
|
||||||
"DISC" => Some(Function::Disc),
|
|
||||||
"RECEIVED" => Some(Function::Received),
|
|
||||||
"INTRATE" => Some(Function::Intrate),
|
|
||||||
|
|
||||||
"DOLLARDE" => Some(Function::Dollarde),
|
"DOLLARDE" => Some(Function::Dollarde),
|
||||||
"DOLLARFR" => Some(Function::Dollarfr),
|
"DOLLARFR" => Some(Function::Dollarfr),
|
||||||
@@ -970,6 +944,19 @@ impl Function {
|
|||||||
"INFO" => Some(Function::Info),
|
"INFO" => Some(Function::Info),
|
||||||
"SHEETS" | "_XLFN.SHEETS" => Some(Function::Sheets),
|
"SHEETS" | "_XLFN.SHEETS" => Some(Function::Sheets),
|
||||||
|
|
||||||
|
"DAVERAGE" => Some(Function::Daverage),
|
||||||
|
"DCOUNT" => Some(Function::Dcount),
|
||||||
|
"DGET" => Some(Function::Dget),
|
||||||
|
"DMAX" => Some(Function::Dmax),
|
||||||
|
"DMIN" => Some(Function::Dmin),
|
||||||
|
"DSUM" => Some(Function::Dsum),
|
||||||
|
"DCOUNTA" => Some(Function::Dcounta),
|
||||||
|
"DPRODUCT" => Some(Function::Dproduct),
|
||||||
|
"DSTDEV" => Some(Function::Dstdev),
|
||||||
|
"DVAR" => Some(Function::Dvar),
|
||||||
|
"DVARP" => Some(Function::Dvarp),
|
||||||
|
"DSTDEVP" => Some(Function::Dstdevp),
|
||||||
|
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1113,23 +1100,18 @@ impl fmt::Display for Function {
|
|||||||
Function::WorkdayIntl => write!(f, "WORKDAY.INTL"),
|
Function::WorkdayIntl => write!(f, "WORKDAY.INTL"),
|
||||||
Function::Yearfrac => write!(f, "YEARFRAC"),
|
Function::Yearfrac => write!(f, "YEARFRAC"),
|
||||||
Function::Isoweeknum => write!(f, "ISOWEEKNUM"),
|
Function::Isoweeknum => write!(f, "ISOWEEKNUM"),
|
||||||
Function::Accrint => write!(f, "ACCRINT"),
|
|
||||||
Function::Accrintm => write!(f, "ACCRINTM"),
|
|
||||||
Function::Pmt => write!(f, "PMT"),
|
Function::Pmt => write!(f, "PMT"),
|
||||||
Function::Pv => write!(f, "PV"),
|
Function::Pv => write!(f, "PV"),
|
||||||
Function::Rate => write!(f, "RATE"),
|
Function::Rate => write!(f, "RATE"),
|
||||||
Function::Nper => write!(f, "NPER"),
|
Function::Nper => write!(f, "NPER"),
|
||||||
Function::Fv => write!(f, "FV"),
|
Function::Fv => write!(f, "FV"),
|
||||||
Function::Fvschedule => write!(f, "FVSCHEDULE"),
|
|
||||||
Function::Ppmt => write!(f, "PPMT"),
|
Function::Ppmt => write!(f, "PPMT"),
|
||||||
Function::Price => write!(f, "PRICE"),
|
|
||||||
Function::Ipmt => write!(f, "IPMT"),
|
Function::Ipmt => write!(f, "IPMT"),
|
||||||
Function::Npv => write!(f, "NPV"),
|
Function::Npv => write!(f, "NPV"),
|
||||||
Function::Mirr => write!(f, "MIRR"),
|
Function::Mirr => write!(f, "MIRR"),
|
||||||
Function::Irr => write!(f, "IRR"),
|
Function::Irr => write!(f, "IRR"),
|
||||||
Function::Xirr => write!(f, "XIRR"),
|
Function::Xirr => write!(f, "XIRR"),
|
||||||
Function::Xnpv => write!(f, "XNPV"),
|
Function::Xnpv => write!(f, "XNPV"),
|
||||||
Function::Yield => write!(f, "YIELD"),
|
|
||||||
Function::Rept => write!(f, "REPT"),
|
Function::Rept => write!(f, "REPT"),
|
||||||
Function::Textafter => write!(f, "TEXTAFTER"),
|
Function::Textafter => write!(f, "TEXTAFTER"),
|
||||||
Function::Textbefore => write!(f, "TEXTBEFORE"),
|
Function::Textbefore => write!(f, "TEXTBEFORE"),
|
||||||
@@ -1141,25 +1123,10 @@ impl fmt::Display for Function {
|
|||||||
Function::Syd => write!(f, "SYD"),
|
Function::Syd => write!(f, "SYD"),
|
||||||
Function::Nominal => write!(f, "NOMINAL"),
|
Function::Nominal => write!(f, "NOMINAL"),
|
||||||
Function::Effect => write!(f, "EFFECT"),
|
Function::Effect => write!(f, "EFFECT"),
|
||||||
Function::Duration => write!(f, "DURATION"),
|
|
||||||
Function::Mduration => write!(f, "MDURATION"),
|
|
||||||
Function::Pduration => write!(f, "PDURATION"),
|
Function::Pduration => write!(f, "PDURATION"),
|
||||||
Function::Coupdaybs => write!(f, "COUPDAYBS"),
|
|
||||||
Function::Coupdays => write!(f, "COUPDAYS"),
|
|
||||||
Function::Coupdaysnc => write!(f, "COUPDAYSNC"),
|
|
||||||
Function::Coupncd => write!(f, "COUPNCD"),
|
|
||||||
Function::Coupnum => write!(f, "COUPNUM"),
|
|
||||||
Function::Couppcd => write!(f, "COUPPCD"),
|
|
||||||
Function::Tbillyield => write!(f, "TBILLYIELD"),
|
Function::Tbillyield => write!(f, "TBILLYIELD"),
|
||||||
Function::Tbillprice => write!(f, "TBILLPRICE"),
|
Function::Tbillprice => write!(f, "TBILLPRICE"),
|
||||||
Function::Tbilleq => write!(f, "TBILLEQ"),
|
Function::Tbilleq => write!(f, "TBILLEQ"),
|
||||||
Function::Pricedisc => write!(f, "PRICEDISC"),
|
|
||||||
Function::Pricemat => write!(f, "PRICEMAT"),
|
|
||||||
Function::Yielddisc => write!(f, "YIELDDISC"),
|
|
||||||
Function::Yieldmat => write!(f, "YIELDMAT"),
|
|
||||||
Function::Disc => write!(f, "DISC"),
|
|
||||||
Function::Received => write!(f, "RECEIVED"),
|
|
||||||
Function::Intrate => write!(f, "INTRATE"),
|
|
||||||
Function::Dollarde => write!(f, "DOLLARDE"),
|
Function::Dollarde => write!(f, "DOLLARDE"),
|
||||||
Function::Dollarfr => write!(f, "DOLLARFR"),
|
Function::Dollarfr => write!(f, "DOLLARFR"),
|
||||||
Function::Ddb => write!(f, "DDB"),
|
Function::Ddb => write!(f, "DDB"),
|
||||||
@@ -1255,6 +1222,18 @@ impl fmt::Display for Function {
|
|||||||
Function::Cell => write!(f, "CELL"),
|
Function::Cell => write!(f, "CELL"),
|
||||||
Function::Info => write!(f, "INFO"),
|
Function::Info => write!(f, "INFO"),
|
||||||
Function::Sheets => write!(f, "SHEETS"),
|
Function::Sheets => write!(f, "SHEETS"),
|
||||||
|
Function::Daverage => write!(f, "DAVERAGE"),
|
||||||
|
Function::Dcount => write!(f, "DCOUNT"),
|
||||||
|
Function::Dget => write!(f, "DGET"),
|
||||||
|
Function::Dmax => write!(f, "DMAX"),
|
||||||
|
Function::Dmin => write!(f, "DMIN"),
|
||||||
|
Function::Dsum => write!(f, "DSUM"),
|
||||||
|
Function::Dcounta => write!(f, "DCOUNTA"),
|
||||||
|
Function::Dproduct => write!(f, "DPRODUCT"),
|
||||||
|
Function::Dstdev => write!(f, "DSTDEV"),
|
||||||
|
Function::Dvar => write!(f, "DVAR"),
|
||||||
|
Function::Dvarp => write!(f, "DVARP"),
|
||||||
|
Function::Dstdevp => write!(f, "DSTDEVP"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1410,24 +1389,18 @@ impl Model {
|
|||||||
Function::WorkdayIntl => self.fn_workday_intl(args, cell),
|
Function::WorkdayIntl => self.fn_workday_intl(args, cell),
|
||||||
Function::Yearfrac => self.fn_yearfrac(args, cell),
|
Function::Yearfrac => self.fn_yearfrac(args, cell),
|
||||||
Function::Isoweeknum => self.fn_isoweeknum(args, cell),
|
Function::Isoweeknum => self.fn_isoweeknum(args, cell),
|
||||||
// Financial
|
|
||||||
Function::Accrint => self.fn_accrint(args, cell),
|
|
||||||
Function::Accrintm => self.fn_accrintm(args, cell),
|
|
||||||
Function::Pmt => self.fn_pmt(args, cell),
|
Function::Pmt => self.fn_pmt(args, cell),
|
||||||
Function::Pv => self.fn_pv(args, cell),
|
Function::Pv => self.fn_pv(args, cell),
|
||||||
Function::Rate => self.fn_rate(args, cell),
|
Function::Rate => self.fn_rate(args, cell),
|
||||||
Function::Nper => self.fn_nper(args, cell),
|
Function::Nper => self.fn_nper(args, cell),
|
||||||
Function::Fv => self.fn_fv(args, cell),
|
Function::Fv => self.fn_fv(args, cell),
|
||||||
Function::Fvschedule => self.fn_fvschedule(args, cell),
|
|
||||||
Function::Ppmt => self.fn_ppmt(args, cell),
|
Function::Ppmt => self.fn_ppmt(args, cell),
|
||||||
Function::Price => self.fn_price(args, cell),
|
|
||||||
Function::Ipmt => self.fn_ipmt(args, cell),
|
Function::Ipmt => self.fn_ipmt(args, cell),
|
||||||
Function::Npv => self.fn_npv(args, cell),
|
Function::Npv => self.fn_npv(args, cell),
|
||||||
Function::Mirr => self.fn_mirr(args, cell),
|
Function::Mirr => self.fn_mirr(args, cell),
|
||||||
Function::Irr => self.fn_irr(args, cell),
|
Function::Irr => self.fn_irr(args, cell),
|
||||||
Function::Xirr => self.fn_xirr(args, cell),
|
Function::Xirr => self.fn_xirr(args, cell),
|
||||||
Function::Xnpv => self.fn_xnpv(args, cell),
|
Function::Xnpv => self.fn_xnpv(args, cell),
|
||||||
Function::Yield => self.fn_yield(args, cell),
|
|
||||||
Function::Rept => self.fn_rept(args, cell),
|
Function::Rept => self.fn_rept(args, cell),
|
||||||
Function::Textafter => self.fn_textafter(args, cell),
|
Function::Textafter => self.fn_textafter(args, cell),
|
||||||
Function::Textbefore => self.fn_textbefore(args, cell),
|
Function::Textbefore => self.fn_textbefore(args, cell),
|
||||||
@@ -1439,25 +1412,10 @@ impl Model {
|
|||||||
Function::Syd => self.fn_syd(args, cell),
|
Function::Syd => self.fn_syd(args, cell),
|
||||||
Function::Nominal => self.fn_nominal(args, cell),
|
Function::Nominal => self.fn_nominal(args, cell),
|
||||||
Function::Effect => self.fn_effect(args, cell),
|
Function::Effect => self.fn_effect(args, cell),
|
||||||
Function::Duration => self.fn_duration(args, cell),
|
|
||||||
Function::Mduration => self.fn_mduration(args, cell),
|
|
||||||
Function::Pduration => self.fn_pduration(args, cell),
|
Function::Pduration => self.fn_pduration(args, cell),
|
||||||
Function::Coupdaybs => self.fn_coupdaybs(args, cell),
|
|
||||||
Function::Coupdays => self.fn_coupdays(args, cell),
|
|
||||||
Function::Coupdaysnc => self.fn_coupdaysnc(args, cell),
|
|
||||||
Function::Coupncd => self.fn_coupncd(args, cell),
|
|
||||||
Function::Coupnum => self.fn_coupnum(args, cell),
|
|
||||||
Function::Couppcd => self.fn_couppcd(args, cell),
|
|
||||||
Function::Tbillyield => self.fn_tbillyield(args, cell),
|
Function::Tbillyield => self.fn_tbillyield(args, cell),
|
||||||
Function::Tbillprice => self.fn_tbillprice(args, cell),
|
Function::Tbillprice => self.fn_tbillprice(args, cell),
|
||||||
Function::Tbilleq => self.fn_tbilleq(args, cell),
|
Function::Tbilleq => self.fn_tbilleq(args, cell),
|
||||||
Function::Pricedisc => self.fn_pricedisc(args, cell),
|
|
||||||
Function::Pricemat => self.fn_pricemat(args, cell),
|
|
||||||
Function::Yielddisc => self.fn_yielddisc(args, cell),
|
|
||||||
Function::Yieldmat => self.fn_yieldmat(args, cell),
|
|
||||||
Function::Disc => self.fn_disc(args, cell),
|
|
||||||
Function::Received => self.fn_received(args, cell),
|
|
||||||
Function::Intrate => self.fn_intrate(args, cell),
|
|
||||||
Function::Dollarde => self.fn_dollarde(args, cell),
|
Function::Dollarde => self.fn_dollarde(args, cell),
|
||||||
Function::Dollarfr => self.fn_dollarfr(args, cell),
|
Function::Dollarfr => self.fn_dollarfr(args, cell),
|
||||||
Function::Ddb => self.fn_ddb(args, cell),
|
Function::Ddb => self.fn_ddb(args, cell),
|
||||||
@@ -1560,6 +1518,18 @@ impl Model {
|
|||||||
Function::Cell => self.fn_cell(args, cell),
|
Function::Cell => self.fn_cell(args, cell),
|
||||||
Function::Info => self.fn_info(args, cell),
|
Function::Info => self.fn_info(args, cell),
|
||||||
Function::Sheets => self.fn_sheets(args, cell),
|
Function::Sheets => self.fn_sheets(args, cell),
|
||||||
|
Function::Daverage => self.fn_daverage(args, cell),
|
||||||
|
Function::Dcount => self.fn_dcount(args, cell),
|
||||||
|
Function::Dget => self.fn_dget(args, cell),
|
||||||
|
Function::Dmax => self.fn_dmax(args, cell),
|
||||||
|
Function::Dmin => self.fn_dmin(args, cell),
|
||||||
|
Function::Dsum => self.fn_dsum(args, cell),
|
||||||
|
Function::Dcounta => self.fn_dcounta(args, cell),
|
||||||
|
Function::Dproduct => self.fn_dproduct(args, cell),
|
||||||
|
Function::Dstdev => self.fn_dstdev(args, cell),
|
||||||
|
Function::Dvar => self.fn_dvar(args, cell),
|
||||||
|
Function::Dvarp => self.fn_dvarp(args, cell),
|
||||||
|
Function::Dstdevp => self.fn_dstdevp(args, cell),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
#[cfg(feature = "use_regex_lite")]
|
#[cfg(feature = "use_regex_lite")]
|
||||||
use regex_lite as regex;
|
use regex_lite as regex;
|
||||||
|
|
||||||
use crate::{calc_result::CalcResult, expressions::token::is_english_error_string};
|
use crate::{
|
||||||
|
calc_result::CalcResult, expressions::token::is_english_error_string,
|
||||||
|
number_format::to_excel_precision,
|
||||||
|
};
|
||||||
|
|
||||||
/// This test for exact match (modulo case).
|
/// This test for exact match (modulo case).
|
||||||
/// * strings are not cast into bools or numbers
|
/// * strings are not cast into bools or numbers
|
||||||
@@ -34,6 +37,8 @@ pub(crate) fn values_are_equal(left: &CalcResult, right: &CalcResult) -> bool {
|
|||||||
pub(crate) fn compare_values(left: &CalcResult, right: &CalcResult) -> i32 {
|
pub(crate) fn compare_values(left: &CalcResult, right: &CalcResult) -> i32 {
|
||||||
match (left, right) {
|
match (left, right) {
|
||||||
(CalcResult::Number(value1), CalcResult::Number(value2)) => {
|
(CalcResult::Number(value1), CalcResult::Number(value2)) => {
|
||||||
|
let value1 = to_excel_precision(*value1, 15);
|
||||||
|
let value2 = to_excel_precision(*value2, 15);
|
||||||
if (value2 - value1).abs() < f64::EPSILON {
|
if (value2 - value1).abs() < f64::EPSILON {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2068,21 +2068,7 @@ impl Model {
|
|||||||
scope: Option<u32>,
|
scope: Option<u32>,
|
||||||
formula: &str,
|
formula: &str,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
if !is_valid_identifier(name) {
|
let sheet_id = self.is_valid_defined_name(name, scope, formula)?;
|
||||||
return Err("Invalid defined name".to_string());
|
|
||||||
};
|
|
||||||
let name_upper = name.to_uppercase();
|
|
||||||
let defined_names = &self.workbook.defined_names;
|
|
||||||
let sheet_id = match scope {
|
|
||||||
Some(index) => Some(self.workbook.worksheet(index)?.sheet_id),
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
// if the defined name already exist return error
|
|
||||||
for df in defined_names {
|
|
||||||
if df.name.to_uppercase() == name_upper && df.sheet_id == sheet_id {
|
|
||||||
return Err("Defined name already exists".to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.workbook.defined_names.push(DefinedName {
|
self.workbook.defined_names.push(DefinedName {
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
formula: formula.to_string(),
|
formula: formula.to_string(),
|
||||||
@@ -2093,6 +2079,48 @@ impl Model {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Validates if a defined name can be created
|
||||||
|
pub fn is_valid_defined_name(
|
||||||
|
&self,
|
||||||
|
name: &str,
|
||||||
|
scope: Option<u32>,
|
||||||
|
formula: &str,
|
||||||
|
) -> Result<Option<u32>, String> {
|
||||||
|
if !is_valid_identifier(name) {
|
||||||
|
return Err("Name: Invalid defined name".to_string());
|
||||||
|
}
|
||||||
|
let name_upper = name.to_uppercase();
|
||||||
|
let defined_names = &self.workbook.defined_names;
|
||||||
|
let sheet_id = match scope {
|
||||||
|
Some(index) => match self.workbook.worksheet(index) {
|
||||||
|
Ok(ws) => Some(ws.sheet_id),
|
||||||
|
Err(_) => return Err("Scope: Invalid sheet index".to_string()),
|
||||||
|
},
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
// if the defined name already exist return error
|
||||||
|
for df in defined_names {
|
||||||
|
if df.name.to_uppercase() == name_upper && df.sheet_id == sheet_id {
|
||||||
|
return Err("Name: Defined name already exists".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the formula is valid
|
||||||
|
match common::ParsedReference::parse_reference_formula(
|
||||||
|
None,
|
||||||
|
formula,
|
||||||
|
&self.locale,
|
||||||
|
|name| self.get_sheet_index_by_name(name),
|
||||||
|
) {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(_) => {
|
||||||
|
return Err("Formula: Invalid defined name formula".to_string());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(sheet_id)
|
||||||
|
}
|
||||||
|
|
||||||
/// Delete defined name of name and scope
|
/// Delete defined name of name and scope
|
||||||
pub fn delete_defined_name(&mut self, name: &str, scope: Option<u32>) -> Result<(), String> {
|
pub fn delete_defined_name(&mut self, name: &str, scope: Option<u32>) -> Result<(), String> {
|
||||||
let name_upper = name.to_uppercase();
|
let name_upper = name.to_uppercase();
|
||||||
@@ -2126,7 +2154,7 @@ impl Model {
|
|||||||
new_formula: &str,
|
new_formula: &str,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
if !is_valid_identifier(new_name) {
|
if !is_valid_identifier(new_name) {
|
||||||
return Err("Invalid defined name".to_string());
|
return Err("Name: Invalid defined name".to_string());
|
||||||
};
|
};
|
||||||
let name_upper = name.to_uppercase();
|
let name_upper = name.to_uppercase();
|
||||||
let new_name_upper = new_name.to_uppercase();
|
let new_name_upper = new_name.to_uppercase();
|
||||||
@@ -2134,18 +2162,28 @@ impl Model {
|
|||||||
if name_upper != new_name_upper || scope != new_scope {
|
if name_upper != new_name_upper || scope != new_scope {
|
||||||
for key in self.parsed_defined_names.keys() {
|
for key in self.parsed_defined_names.keys() {
|
||||||
if key.1.to_uppercase() == new_name_upper && key.0 == new_scope {
|
if key.1.to_uppercase() == new_name_upper && key.0 == new_scope {
|
||||||
return Err("Defined name already exists".to_string());
|
return Err("Name: Defined name already exists".to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let defined_names = &self.workbook.defined_names;
|
let defined_names = &self.workbook.defined_names;
|
||||||
let sheet_id = match scope {
|
let sheet_id = match scope {
|
||||||
Some(index) => Some(self.workbook.worksheet(index)?.sheet_id),
|
Some(index) => Some(
|
||||||
|
self.workbook
|
||||||
|
.worksheet(index)
|
||||||
|
.map_err(|_| "Scope: Invalid sheet index")?
|
||||||
|
.sheet_id,
|
||||||
|
),
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let new_sheet_id = match new_scope {
|
let new_sheet_id = match new_scope {
|
||||||
Some(index) => Some(self.workbook.worksheet(index)?.sheet_id),
|
Some(index) => Some(
|
||||||
|
self.workbook
|
||||||
|
.worksheet(index)
|
||||||
|
.map_err(|_| "Scope: Invalid sheet index")?
|
||||||
|
.sheet_id,
|
||||||
|
),
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -112,29 +112,36 @@ pub fn to_precision(value: f64, precision: usize) -> f64 {
|
|||||||
/// ```
|
/// ```
|
||||||
/// This intends to be equivalent to the js: `${parseFloat(value.toPrecision(precision)})`
|
/// This intends to be equivalent to the js: `${parseFloat(value.toPrecision(precision)})`
|
||||||
/// See ([ecma](https://tc39.es/ecma262/#sec-number.prototype.toprecision)).
|
/// See ([ecma](https://tc39.es/ecma262/#sec-number.prototype.toprecision)).
|
||||||
/// FIXME: There has to be a better algorithm :/
|
|
||||||
pub fn to_excel_precision_str(value: f64) -> String {
|
pub fn to_excel_precision_str(value: f64) -> String {
|
||||||
to_precision_str(value, 15)
|
to_precision_str(value, 15)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn to_excel_precision(value: f64, precision: usize) -> f64 {
|
||||||
|
if !value.is_finite() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
let s = format!("{:.*e}", precision.saturating_sub(1), value);
|
||||||
|
s.parse::<f64>().unwrap_or(value)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn to_precision_str(value: f64, precision: usize) -> String {
|
pub fn to_precision_str(value: f64, precision: usize) -> String {
|
||||||
if value.is_infinite() {
|
if !value.is_finite() {
|
||||||
return "inf".to_string();
|
if value.is_infinite() {
|
||||||
|
return "inf".to_string();
|
||||||
|
} else {
|
||||||
|
return "NaN".to_string();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if value.is_nan() {
|
|
||||||
return "NaN".to_string();
|
let s = format!("{:.*e}", precision.saturating_sub(1), value);
|
||||||
}
|
let parsed = s.parse::<f64>().unwrap_or(value);
|
||||||
let exponent = value.abs().log10().floor();
|
|
||||||
let base = value / 10.0_f64.powf(exponent);
|
|
||||||
let base = format!("{0:.1$}", base, precision - 1);
|
|
||||||
let value = format!("{base}e{exponent}").parse::<f64>().unwrap_or({
|
|
||||||
// TODO: do this in a way that does not require a possible error
|
|
||||||
0.0
|
|
||||||
});
|
|
||||||
// I would love to use the std library. There is not a speed concern here
|
// I would love to use the std library. There is not a speed concern here
|
||||||
// problem is it doesn't do the right thing
|
// problem is it doesn't do the right thing
|
||||||
// Also ryu is my favorite _modern_ algorithm
|
// Also ryu is my favorite _modern_ algorithm
|
||||||
let mut buffer = ryu::Buffer::new();
|
let mut buffer = ryu::Buffer::new();
|
||||||
let text = buffer.format(value);
|
let text = buffer.format(parsed);
|
||||||
// The above algorithm converts 2 to 2.0 regrettably
|
// The above algorithm converts 2 to 2.0 regrettably
|
||||||
if let Some(stripped) = text.strip_suffix(".0") {
|
if let Some(stripped) = text.strip_suffix(".0") {
|
||||||
return stripped.to_string();
|
return stripped.to_string();
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ fn fn_imcot() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(target_os = "windows", ignore)]
|
||||||
#[test]
|
#[test]
|
||||||
fn fn_imtan() {
|
fn fn_imtan() {
|
||||||
let mut model = new_empty_model();
|
let mut model = new_empty_model();
|
||||||
|
|||||||
@@ -11,19 +11,14 @@ mod test_datedif_leap_month_end;
|
|||||||
mod test_days360_month_end;
|
mod test_days360_month_end;
|
||||||
mod test_degrees_radians;
|
mod test_degrees_radians;
|
||||||
mod test_error_propagation;
|
mod test_error_propagation;
|
||||||
mod test_fn_accrint;
|
|
||||||
mod test_fn_accrintm;
|
|
||||||
mod test_fn_average;
|
mod test_fn_average;
|
||||||
mod test_fn_averageifs;
|
mod test_fn_averageifs;
|
||||||
mod test_fn_choose;
|
mod test_fn_choose;
|
||||||
mod test_fn_concatenate;
|
mod test_fn_concatenate;
|
||||||
mod test_fn_count;
|
mod test_fn_count;
|
||||||
mod test_fn_coupon;
|
|
||||||
mod test_fn_day;
|
mod test_fn_day;
|
||||||
mod test_fn_duration;
|
|
||||||
mod test_fn_exact;
|
mod test_fn_exact;
|
||||||
mod test_fn_financial;
|
mod test_fn_financial;
|
||||||
mod test_fn_financial_bonds;
|
|
||||||
mod test_fn_formulatext;
|
mod test_fn_formulatext;
|
||||||
mod test_fn_if;
|
mod test_fn_if;
|
||||||
mod test_fn_maxifs;
|
mod test_fn_maxifs;
|
||||||
@@ -68,7 +63,6 @@ mod test_escape_quotes;
|
|||||||
mod test_extend;
|
mod test_extend;
|
||||||
mod test_fn_fv;
|
mod test_fn_fv;
|
||||||
mod test_fn_round;
|
mod test_fn_round;
|
||||||
mod test_fn_fvschedule;
|
|
||||||
mod test_fn_type;
|
mod test_fn_type;
|
||||||
mod test_frozen_rows_and_columns;
|
mod test_frozen_rows_and_columns;
|
||||||
mod test_geomean;
|
mod test_geomean;
|
||||||
@@ -79,7 +73,9 @@ mod test_issue_483;
|
|||||||
mod test_ln;
|
mod test_ln;
|
||||||
mod test_log;
|
mod test_log;
|
||||||
mod test_log10;
|
mod test_log10;
|
||||||
|
mod test_mod_quotient;
|
||||||
mod test_networkdays;
|
mod test_networkdays;
|
||||||
|
mod test_now;
|
||||||
mod test_percentage;
|
mod test_percentage;
|
||||||
mod test_set_functions_error_handling;
|
mod test_set_functions_error_handling;
|
||||||
mod test_sheet_names;
|
mod test_sheet_names;
|
||||||
|
|||||||
27
base/src/test/test_arabic_roman
Normal file
27
base/src/test/test_arabic_roman
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::test::util::new_empty_model;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn arguments() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
model._set("A1", "=ARABIC()");
|
||||||
|
model._set("A2", "=ARABIC(V)");
|
||||||
|
model._set("A3", "=ARABIC(V, 2)");
|
||||||
|
|
||||||
|
model._set("A4", "=ROMAN()");
|
||||||
|
model._set("A5", "=ROMAN(5)");
|
||||||
|
model._set("A6", "=ROMAN(5, 0)");
|
||||||
|
model._set("A7", "=ROMAN(5, 0, 2)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A2"), *"5");
|
||||||
|
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A5"), *"V");
|
||||||
|
assert_eq!(model._get_text("A6"), *"V");
|
||||||
|
assert_eq!(model._get_text("A7"), *"#ERROR!");
|
||||||
|
}
|
||||||
27
base/src/test/test_combin_combina
Normal file
27
base/src/test/test_combin_combina
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::test::util::new_empty_model;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn arguments() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
model._set("A1", "=COMBIN(5,2)");
|
||||||
|
model._set("A2", "=COMBINA(5,2)");
|
||||||
|
model._set("A3", "=COMBIN()");
|
||||||
|
model._set("A4", "=COMBINA()");
|
||||||
|
model._set("A5", "=COMBIN(2)");
|
||||||
|
model._set("A6", "=COMBINA(2)");
|
||||||
|
model._set("A5", "=COMBIN(1, 2, 3)");
|
||||||
|
model._set("A6", "=COMBINA(1, 2, 3)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"10");
|
||||||
|
assert_eq!(model._get_text("A2"), *"15");
|
||||||
|
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A5"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A6"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A7"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A8"), *"#ERROR!");
|
||||||
|
}
|
||||||
@@ -542,7 +542,6 @@ fn test_yearfrac_function() {
|
|||||||
|
|
||||||
// Edge cases
|
// Edge cases
|
||||||
model._set("A4", "=YEARFRAC(44561,44561,1)"); // Same date = 0
|
model._set("A4", "=YEARFRAC(44561,44561,1)"); // Same date = 0
|
||||||
model._set("A5", "=YEARFRAC(44926,44561,1)"); // Reverse = negative
|
|
||||||
model._set("A6", "=YEARFRAC(44197,44562,1)"); // Exact year (2021)
|
model._set("A6", "=YEARFRAC(44197,44562,1)"); // Exact year (2021)
|
||||||
|
|
||||||
// Error cases
|
// Error cases
|
||||||
@@ -559,7 +558,6 @@ fn test_yearfrac_function() {
|
|||||||
|
|
||||||
// Edge cases
|
// Edge cases
|
||||||
assert_eq!(model._get_text("A4"), *"0"); // Same date
|
assert_eq!(model._get_text("A4"), *"0"); // Same date
|
||||||
assert_eq!(model._get_text("A5"), *"-1"); // Negative
|
|
||||||
assert_eq!(model._get_text("A6"), *"1"); // Exact year
|
assert_eq!(model._get_text("A6"), *"1"); // Exact year
|
||||||
|
|
||||||
// Error cases
|
// Error cases
|
||||||
|
|||||||
23
base/src/test/test_even_odd
Normal file
23
base/src/test/test_even_odd
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::test::util::new_empty_model;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn arguments() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
model._set("A1", "=EVEN(2)");
|
||||||
|
model._set("A2", "=ODD(2)");
|
||||||
|
model._set("A3", "=EVEN()");
|
||||||
|
model._set("A4", "=ODD()");
|
||||||
|
model._set("A5", "=EVEN(1, 2)");
|
||||||
|
model._set("A6", "=ODD(1, 2)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"2");
|
||||||
|
assert_eq!(model._get_text("A2"), *"3");
|
||||||
|
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A5"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A6"), *"#ERROR!");
|
||||||
|
}
|
||||||
26
base/src/test/test_exp_sign.rs
Normal file
26
base/src/test/test_exp_sign.rs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::test::util::new_empty_model;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fn_arguments() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
model._set("A1", "=EXP()");
|
||||||
|
model._set("A2", "=SIGN()");
|
||||||
|
|
||||||
|
model._set("A3", "=EXP(0)");
|
||||||
|
model._set("A4", "=SIGN(-10)");
|
||||||
|
|
||||||
|
model._set("A5", "=EXP(1, 2)");
|
||||||
|
model._set("A6", "=SIGN(1, 2)");
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A2"), *"#ERROR!");
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A3"), *"1");
|
||||||
|
assert_eq!(model._get_text("A4"), *"-1");
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A5"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A6"), *"#ERROR!");
|
||||||
|
}
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
#![allow(clippy::unwrap_used)]
|
|
||||||
|
|
||||||
use crate::{cell::CellValue, test::util::new_empty_model};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fn_accrint() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
model._set("A1", "=DATE(2020,1,1)");
|
|
||||||
model._set("A2", "=DATE(2020,1,1)");
|
|
||||||
model._set("A3", "=DATE(2020,1,31)");
|
|
||||||
model._set("A4", "10%");
|
|
||||||
model._set("A5", "$1,000");
|
|
||||||
model._set("A6", "2");
|
|
||||||
|
|
||||||
model._set("B1", "=ACCRINT(A1,A2,A3,A4,A5,A6)");
|
|
||||||
model._set("C1", "=ACCRINT(A1)");
|
|
||||||
model._set("C2", "=ACCRINT(A1,A2,A3,A4,A5,3)");
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
match model.get_cell_value_by_ref("Sheet1!B1") {
|
|
||||||
Ok(CellValue::Number(v)) => {
|
|
||||||
assert!((v - 8.333333333333334).abs() < 1e-9);
|
|
||||||
}
|
|
||||||
other => unreachable!("Expected number for B1, got {:?}", other),
|
|
||||||
}
|
|
||||||
assert_eq!(model._get_text("C1"), *"#ERROR!");
|
|
||||||
assert_eq!(model._get_text("C2"), *"#NUM!");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fn_accrint_parameters() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
|
|
||||||
model._set("A1", "=DATE(2020,1,1)");
|
|
||||||
model._set("A2", "=DATE(2020,1,1)");
|
|
||||||
model._set("A3", "=DATE(2020,7,1)");
|
|
||||||
model._set("A4", "8%");
|
|
||||||
model._set("A5", "1000");
|
|
||||||
|
|
||||||
model._set("B1", "=ACCRINT(A1,A2,A3,A4,A5,2,0,TRUE)");
|
|
||||||
model._set("B2", "=ACCRINT(A1,A2,A3,A4,A5,2,1,TRUE)");
|
|
||||||
model._set("B3", "=ACCRINT(A1,A2,A3,A4,A5,2,4,TRUE)");
|
|
||||||
model._set("B4", "=ACCRINT(A1,A2,A3,A4,A5,1)");
|
|
||||||
model._set("B5", "=ACCRINT(A1,A2,A3,A4,A5,4)");
|
|
||||||
model._set("B6", "=ACCRINT(A1,A2,A3,A4,A5,2)");
|
|
||||||
model._set("B7", "=ACCRINT(A1,A2,A3,A4,A5,2,0)");
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
match model.get_cell_value_by_ref("Sheet1!B1") {
|
|
||||||
Ok(CellValue::Number(v)) => {
|
|
||||||
assert!((v - 40.0).abs() < 1e-9);
|
|
||||||
}
|
|
||||||
other => unreachable!("Expected number for B1, got {:?}", other),
|
|
||||||
}
|
|
||||||
|
|
||||||
match (
|
|
||||||
model.get_cell_value_by_ref("Sheet1!B1"),
|
|
||||||
model.get_cell_value_by_ref("Sheet1!B6"),
|
|
||||||
) {
|
|
||||||
(Ok(CellValue::Number(v1)), Ok(CellValue::Number(v2))) => {
|
|
||||||
assert!((v1 - v2).abs() < 1e-12);
|
|
||||||
}
|
|
||||||
other => unreachable!("Expected matching numbers, got {:?}", other),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fn_accrint_errors() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
|
|
||||||
model._set("A1", "=DATE(2020,1,1)");
|
|
||||||
model._set("A2", "=DATE(2020,1,1)");
|
|
||||||
model._set("A3", "=DATE(2020,7,1)");
|
|
||||||
model._set("A4", "8%");
|
|
||||||
model._set("A5", "1000");
|
|
||||||
|
|
||||||
model._set("B1", "=ACCRINT()");
|
|
||||||
model._set("B2", "=ACCRINT(A1,A2,A3,A4,A5)");
|
|
||||||
model._set("B3", "=ACCRINT(A1,A2,A3,A4,A5,2,0,TRUE,1)");
|
|
||||||
model._set("C1", "=ACCRINT(A1,A2,A3,A4,A5,0)");
|
|
||||||
model._set("C2", "=ACCRINT(A1,A2,A3,A4,A5,3)");
|
|
||||||
model._set("C3", "=ACCRINT(A1,A2,A3,A4,A5,-1)");
|
|
||||||
model._set("D1", "=ACCRINT(A1,A2,A3,A4,A5,2,-1)");
|
|
||||||
model._set("D2", "=ACCRINT(A1,A2,A3,A4,A5,2,5)");
|
|
||||||
model._set("E1", "=ACCRINT(A3,A2,A1,A4,A5,2)");
|
|
||||||
model._set("E2", "=ACCRINT(A1,A3,A1,A4,A5,2)");
|
|
||||||
model._set("F1", "=ACCRINT(A1,A2,A3,A4,0,2)");
|
|
||||||
model._set("F2", "=ACCRINT(A1,A2,A3,A4,-1000,2)");
|
|
||||||
model._set("F3", "=ACCRINT(A1,A2,A3,-8%,A5,2)");
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
assert_eq!(model._get_text("B1"), *"#ERROR!");
|
|
||||||
assert_eq!(model._get_text("B2"), *"#ERROR!");
|
|
||||||
assert_eq!(model._get_text("B3"), *"#ERROR!");
|
|
||||||
assert_eq!(model._get_text("C1"), *"#NUM!");
|
|
||||||
assert_eq!(model._get_text("C2"), *"#NUM!");
|
|
||||||
assert_eq!(model._get_text("C3"), *"#NUM!");
|
|
||||||
assert_eq!(model._get_text("D1"), *"#NUM!");
|
|
||||||
assert_eq!(model._get_text("D2"), *"#NUM!");
|
|
||||||
assert_eq!(model._get_text("E1"), *"#NUM!");
|
|
||||||
assert_eq!(model._get_text("E2"), *"#NUM!");
|
|
||||||
assert_eq!(model._get_text("F2"), *"#NUM!");
|
|
||||||
assert_eq!(model._get_text("F3"), *"#NUM!");
|
|
||||||
|
|
||||||
match model.get_cell_value_by_ref("Sheet1!F1") {
|
|
||||||
Ok(CellValue::Number(v)) => {
|
|
||||||
assert!((v - 0.0).abs() < 1e-9);
|
|
||||||
}
|
|
||||||
other => unreachable!("Expected 0 for F1, got {:?}", other),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fn_accrint_combined() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
model._set("A1", "=DATE(2018,10,15)");
|
|
||||||
model._set("A2", "=DATE(2019,2,1)");
|
|
||||||
model._set("A3", "5%");
|
|
||||||
model._set("A4", "1000");
|
|
||||||
|
|
||||||
model._set("B1", "=ACCRINT(A1,A1,A2,A3,A4,2)");
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
match model.get_cell_value_by_ref("Sheet1!B1") {
|
|
||||||
Ok(CellValue::Number(v)) => {
|
|
||||||
assert!((v - 14.722222222222221).abs() < 1e-9);
|
|
||||||
}
|
|
||||||
other => unreachable!("Expected number for B1, got {:?}", other),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
#![allow(clippy::unwrap_used)]
|
|
||||||
|
|
||||||
use crate::{cell::CellValue, test::util::new_empty_model};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fn_accrintm() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
model._set("A1", "=DATE(2020,1,1)");
|
|
||||||
model._set("A2", "=DATE(2020,7,1)");
|
|
||||||
model._set("A3", "10%");
|
|
||||||
model._set("A4", "$1,000");
|
|
||||||
|
|
||||||
model._set("B1", "=ACCRINTM(A1,A2,A3,A4)");
|
|
||||||
model._set("C1", "=ACCRINTM(A1)");
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
match model.get_cell_value_by_ref("Sheet1!B1") {
|
|
||||||
Ok(CellValue::Number(v)) => {
|
|
||||||
assert!((v - 50.0).abs() < 1e-9);
|
|
||||||
}
|
|
||||||
other => unreachable!("Expected number for B1, got {:?}", other),
|
|
||||||
}
|
|
||||||
assert_eq!(model._get_text("C1"), *"#ERROR!");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fn_accrintm_parameters() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
|
|
||||||
model._set("A1", "=DATE(2020,1,1)");
|
|
||||||
model._set("A2", "=DATE(2020,7,1)");
|
|
||||||
model._set("A3", "8%");
|
|
||||||
model._set("A4", "1000");
|
|
||||||
|
|
||||||
model._set("B1", "=ACCRINTM(A1,A2,A3,A4,0)");
|
|
||||||
model._set("B2", "=ACCRINTM(A1,A2,A3,A4,1)");
|
|
||||||
model._set("B3", "=ACCRINTM(A1,A2,A3,A4,4)");
|
|
||||||
model._set("C1", "=ACCRINTM(A1,A2,A3,A4)");
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
match (
|
|
||||||
model.get_cell_value_by_ref("Sheet1!B1"),
|
|
||||||
model.get_cell_value_by_ref("Sheet1!B2"),
|
|
||||||
) {
|
|
||||||
(Ok(CellValue::Number(v1)), Ok(CellValue::Number(v2))) => {
|
|
||||||
assert!(v1 > 0.0 && v2 > 0.0);
|
|
||||||
}
|
|
||||||
other => unreachable!("Expected numbers for basis test, got {:?}", other),
|
|
||||||
}
|
|
||||||
|
|
||||||
match (
|
|
||||||
model.get_cell_value_by_ref("Sheet1!B1"),
|
|
||||||
model.get_cell_value_by_ref("Sheet1!C1"),
|
|
||||||
) {
|
|
||||||
(Ok(CellValue::Number(v1)), Ok(CellValue::Number(v2))) => {
|
|
||||||
assert!((v1 - v2).abs() < 1e-12);
|
|
||||||
}
|
|
||||||
other => unreachable!(
|
|
||||||
"Expected matching numbers for default test, got {:?}",
|
|
||||||
other
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fn_accrintm_errors() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
|
|
||||||
model._set("A1", "=DATE(2020,1,1)");
|
|
||||||
model._set("A2", "=DATE(2020,7,1)");
|
|
||||||
model._set("A3", "8%");
|
|
||||||
model._set("A4", "1000");
|
|
||||||
|
|
||||||
model._set("B1", "=ACCRINTM()");
|
|
||||||
model._set("B2", "=ACCRINTM(A1,A2,A3)");
|
|
||||||
model._set("B3", "=ACCRINTM(A1,A2,A3,A4,0,1)");
|
|
||||||
model._set("C1", "=ACCRINTM(A1,A2,A3,A4,-1)");
|
|
||||||
model._set("C2", "=ACCRINTM(A1,A2,A3,A4,5)");
|
|
||||||
model._set("D1", "=ACCRINTM(A2,A1,A3,A4)");
|
|
||||||
model._set("E1", "=ACCRINTM(A1,A2,A3,0)");
|
|
||||||
model._set("E2", "=ACCRINTM(A1,A2,A3,-1000)");
|
|
||||||
model._set("E3", "=ACCRINTM(A1,A2,-8%,A4)");
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
assert_eq!(model._get_text("B1"), *"#ERROR!");
|
|
||||||
assert_eq!(model._get_text("B2"), *"#ERROR!");
|
|
||||||
assert_eq!(model._get_text("B3"), *"#ERROR!");
|
|
||||||
assert_eq!(model._get_text("C1"), *"#NUM!");
|
|
||||||
assert_eq!(model._get_text("C2"), *"#NUM!");
|
|
||||||
assert_eq!(model._get_text("D1"), *"#NUM!");
|
|
||||||
assert_eq!(model._get_text("E2"), *"#NUM!");
|
|
||||||
assert_eq!(model._get_text("E3"), *"#NUM!");
|
|
||||||
|
|
||||||
match model.get_cell_value_by_ref("Sheet1!E1") {
|
|
||||||
Ok(CellValue::Number(v)) => {
|
|
||||||
assert!((v - 0.0).abs() < 1e-9);
|
|
||||||
}
|
|
||||||
other => unreachable!("Expected 0 for E1, got {:?}", other),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fn_accrintm_combined() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
model._set("C1", "=DATE(2016,4,5)");
|
|
||||||
model._set("C2", "=DATE(2019,2,1)");
|
|
||||||
model._set("A3", "5%");
|
|
||||||
model._set("A4", "1000");
|
|
||||||
model._set("B2", "=ACCRINTM(C1,C2,A3,A4)");
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
match model.get_cell_value_by_ref("Sheet1!B2") {
|
|
||||||
Ok(CellValue::Number(v)) => {
|
|
||||||
assert!((v - 141.11111111111111).abs() < 1e-9);
|
|
||||||
}
|
|
||||||
other => unreachable!("Expected number for B2, got {:?}", other),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,260 +0,0 @@
|
|||||||
#![allow(clippy::unwrap_used)]
|
|
||||||
|
|
||||||
use crate::{cell::CellValue, test::util::new_empty_model};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fn_coupon_functions() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
|
|
||||||
// Test with basis 1 (original test)
|
|
||||||
model._set("A1", "=DATE(2001,1,25)");
|
|
||||||
model._set("A2", "=DATE(2001,11,15)");
|
|
||||||
model._set("B1", "=COUPDAYBS(A1,A2,2,1)");
|
|
||||||
model._set("B2", "=COUPDAYS(A1,A2,2,1)");
|
|
||||||
model._set("B3", "=COUPDAYSNC(A1,A2,2,1)");
|
|
||||||
model._set("B4", "=COUPNCD(A1,A2,2,1)");
|
|
||||||
model._set("B5", "=COUPNUM(A1,A2,2,1)");
|
|
||||||
model._set("B6", "=COUPPCD(A1,A2,2,1)");
|
|
||||||
|
|
||||||
// Test with basis 3 for better coverage
|
|
||||||
model._set("C1", "=COUPDAYBS(DATE(2001,1,25),DATE(2001,11,15),2,3)");
|
|
||||||
model._set("C2", "=COUPDAYS(DATE(2001,1,25),DATE(2001,11,15),2,3)");
|
|
||||||
model._set("C3", "=COUPDAYSNC(DATE(2001,1,25),DATE(2001,11,15),2,3)");
|
|
||||||
model._set("C4", "=COUPNCD(DATE(2001,1,25),DATE(2001,11,15),2,3)");
|
|
||||||
model._set("C5", "=COUPNUM(DATE(2007,1,25),DATE(2008,11,15),2,1)");
|
|
||||||
model._set("C6", "=COUPPCD(DATE(2001,1,25),DATE(2001,11,15),2,3)");
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
// Test basis 1
|
|
||||||
assert_eq!(model._get_text("B1"), "71");
|
|
||||||
assert_eq!(model._get_text("B2"), "181");
|
|
||||||
assert_eq!(model._get_text("B3"), "110");
|
|
||||||
assert_eq!(
|
|
||||||
model.get_cell_value_by_ref("Sheet1!B4"),
|
|
||||||
Ok(CellValue::Number(37026.0))
|
|
||||||
);
|
|
||||||
assert_eq!(model._get_text("B5"), "2");
|
|
||||||
assert_eq!(
|
|
||||||
model.get_cell_value_by_ref("Sheet1!B6"),
|
|
||||||
Ok(CellValue::Number(36845.0))
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test basis 3 (more comprehensive coverage)
|
|
||||||
assert_eq!(model._get_text("C1"), "71");
|
|
||||||
assert_eq!(model._get_text("C2"), "181"); // Fixed: actual days
|
|
||||||
assert_eq!(model._get_text("C3"), "110");
|
|
||||||
assert_eq!(model._get_text("C4"), "37026");
|
|
||||||
assert_eq!(model._get_text("C5"), "4");
|
|
||||||
assert_eq!(model._get_text("C6"), "36845");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fn_coupon_functions_error_cases() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
|
|
||||||
// Test invalid frequency
|
|
||||||
model._set("E1", "=COUPDAYBS(DATE(2001,1,25),DATE(2001,11,15),3,1)");
|
|
||||||
// Test invalid basis
|
|
||||||
model._set("E2", "=COUPDAYS(DATE(2001,1,25),DATE(2001,11,15),2,5)");
|
|
||||||
// Test settlement >= maturity
|
|
||||||
model._set("E3", "=COUPDAYSNC(DATE(2001,11,15),DATE(2001,1,25),2,1)");
|
|
||||||
// Test too few arguments
|
|
||||||
model._set("E4", "=COUPNCD(DATE(2001,1,25),DATE(2001,11,15))");
|
|
||||||
// Test too many arguments
|
|
||||||
model._set("E5", "=COUPNUM(DATE(2001,1,25),DATE(2001,11,15),2,1,1)");
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
// All should return errors
|
|
||||||
assert_eq!(model._get_text("E1"), "#NUM!");
|
|
||||||
assert_eq!(model._get_text("E2"), "#NUM!");
|
|
||||||
assert_eq!(model._get_text("E3"), "#NUM!");
|
|
||||||
assert_eq!(model._get_text("E4"), *"#ERROR!");
|
|
||||||
assert_eq!(model._get_text("E5"), *"#ERROR!");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fn_coupdays_actual_day_count_fix() {
|
|
||||||
// Verify COUPDAYS correctly distinguishes between fixed vs actual day count methods
|
|
||||||
// Bug: basis 2&3 were incorrectly using fixed calculations like basis 0&4
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
|
|
||||||
model._set("A1", "=DATE(2023,1,15)");
|
|
||||||
model._set("A2", "=DATE(2023,7,15)");
|
|
||||||
|
|
||||||
model._set("B1", "=COUPDAYS(A1,A2,2,0)"); // 30/360: uses 360/freq
|
|
||||||
model._set("B2", "=COUPDAYS(A1,A2,2,2)"); // Actual/360: uses actual days
|
|
||||||
model._set("B3", "=COUPDAYS(A1,A2,2,3)"); // Actual/365: uses actual days
|
|
||||||
model._set("B4", "=COUPDAYS(A1,A2,2,4)"); // 30/360 European: uses 360/freq
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
// Basis 0&4: theoretical 360/2 = 180 days
|
|
||||||
assert_eq!(model._get_text("B1"), "180");
|
|
||||||
assert_eq!(model._get_text("B4"), "180");
|
|
||||||
|
|
||||||
// Basis 2&3: actual days between Jan 15 and Jul 15 = 181 days
|
|
||||||
assert_eq!(model._get_text("B2"), "181");
|
|
||||||
assert_eq!(model._get_text("B3"), "181");
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// FEBRUARY EDGE CASE TESTS - Day Count Convention Compliance
|
|
||||||
// =============================================================================
|
|
||||||
// These tests verify that financial functions correctly handle February dates
|
|
||||||
// according to the official 30/360 day count convention specifications.
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_coupon_functions_february_consistency() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
|
|
||||||
// Test that coupon functions behave consistently between US and European methods
|
|
||||||
// when February dates are involved
|
|
||||||
|
|
||||||
// Settlement: Last day of February (non-leap year)
|
|
||||||
// Maturity: Some date in following year that creates a clear test case
|
|
||||||
model._set("A1", "=DATE(2023,2,28)"); // Last day of Feb, non-leap year
|
|
||||||
model._set("A2", "=DATE(2024,2,28)"); // Same day next year
|
|
||||||
|
|
||||||
// Test COUPDAYS with different basis values
|
|
||||||
model._set("B1", "=COUPDAYS(A1,A2,2,0)"); // US 30/360 - should treat Feb 28 as day 30
|
|
||||||
model._set("B2", "=COUPDAYS(A1,A2,2,4)"); // European 30/360 - should treat Feb 28 as day 28
|
|
||||||
model._set("B3", "=COUPDAYS(A1,A2,2,1)"); // Actual/actual - should use real days
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
// All should return valid numbers (no errors)
|
|
||||||
assert_ne!(model._get_text("B1"), *"#NUM!");
|
|
||||||
assert_ne!(model._get_text("B2"), *"#NUM!");
|
|
||||||
assert_ne!(model._get_text("B3"), *"#NUM!");
|
|
||||||
|
|
||||||
// US and European 30/360 should potentially give different results for February dates
|
|
||||||
// (though the exact difference depends on the specific coupon calculation logic)
|
|
||||||
let us_result = model._get_text("B1");
|
|
||||||
let european_result = model._get_text("B2");
|
|
||||||
let actual_result = model._get_text("B3");
|
|
||||||
|
|
||||||
// Verify all are numeric
|
|
||||||
assert!(us_result.parse::<f64>().is_ok());
|
|
||||||
assert!(european_result.parse::<f64>().is_ok());
|
|
||||||
assert!(actual_result.parse::<f64>().is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_february_edge_cases_leap_vs_nonleap() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
|
|
||||||
// Test leap year vs non-leap year February handling
|
|
||||||
|
|
||||||
// Feb 28 in non-leap year (this IS the last day of February)
|
|
||||||
model._set("A1", "=DATE(2023,2,28)");
|
|
||||||
model._set("A2", "=DATE(2023,8,28)");
|
|
||||||
|
|
||||||
// Feb 28 in leap year (this is NOT the last day of February)
|
|
||||||
model._set("A3", "=DATE(2024,2,28)");
|
|
||||||
model._set("A4", "=DATE(2024,8,28)");
|
|
||||||
|
|
||||||
// Feb 29 in leap year (this IS the last day of February)
|
|
||||||
model._set("A5", "=DATE(2024,2,29)");
|
|
||||||
model._set("A6", "=DATE(2024,8,29)");
|
|
||||||
|
|
||||||
// Test with basis 0 (US 30/360) - should have special February handling
|
|
||||||
model._set("B1", "=COUPDAYS(A1,A2,2,0)"); // Feb 28 non-leap (last day)
|
|
||||||
model._set("B2", "=COUPDAYS(A3,A4,2,0)"); // Feb 28 leap year (not last day)
|
|
||||||
model._set("B3", "=COUPDAYS(A5,A6,2,0)"); // Feb 29 leap year (last day)
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
// All should succeed
|
|
||||||
assert_ne!(model._get_text("B1"), *"#NUM!");
|
|
||||||
assert_ne!(model._get_text("B2"), *"#NUM!");
|
|
||||||
assert_ne!(model._get_text("B3"), *"#NUM!");
|
|
||||||
|
|
||||||
// Verify they're all numeric
|
|
||||||
assert!(model._get_text("B1").parse::<f64>().is_ok());
|
|
||||||
assert!(model._get_text("B2").parse::<f64>().is_ok());
|
|
||||||
assert!(model._get_text("B3").parse::<f64>().is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_us_nasd_both_february_rule() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
|
|
||||||
// Test the specific US/NASD rule: "If both date A and B fall on the last day of February,
|
|
||||||
// then date B will be changed to the 30th"
|
|
||||||
|
|
||||||
// Case 1: Both dates are Feb 28 in non-leap years (both are last day of February)
|
|
||||||
model._set("A1", "=DATE(2023,2,28)"); // Last day of Feb 2023
|
|
||||||
model._set("A2", "=DATE(2025,2,28)"); // Last day of Feb 2025
|
|
||||||
|
|
||||||
// Case 2: Both dates are Feb 29 in leap years (both are last day of February)
|
|
||||||
model._set("A3", "=DATE(2024,2,29)"); // Last day of Feb 2024
|
|
||||||
model._set("A4", "=DATE(2028,2,29)"); // Last day of Feb 2028
|
|
||||||
|
|
||||||
// Case 3: Mixed - Feb 28 non-leap to Feb 29 leap (both are last day of February)
|
|
||||||
model._set("A5", "=DATE(2023,2,28)"); // Last day of Feb 2023
|
|
||||||
model._set("A6", "=DATE(2024,2,29)"); // Last day of Feb 2024
|
|
||||||
|
|
||||||
// Case 4: Control - Feb 28 in leap year (NOT last day) to Feb 29 (IS last day)
|
|
||||||
model._set("A7", "=DATE(2024,2,28)"); // NOT last day of Feb 2024
|
|
||||||
model._set("A8", "=DATE(2024,2,29)"); // IS last day of Feb 2024
|
|
||||||
|
|
||||||
// Test using coupon functions that should apply US/NASD 30/360 (basis 0)
|
|
||||||
model._set("B1", "=COUPDAYS(A1,A2,1,0)"); // Both last day Feb - Rule 1 should apply
|
|
||||||
model._set("B2", "=COUPDAYS(A3,A4,1,0)"); // Both last day Feb - Rule 1 should apply
|
|
||||||
model._set("B3", "=COUPDAYS(A5,A6,1,0)"); // Both last day Feb - Rule 1 should apply
|
|
||||||
model._set("B4", "=COUPDAYS(A7,A8,1,0)"); // Only end is last day Feb - Rule 1 should NOT apply
|
|
||||||
|
|
||||||
// Compare with European method (basis 4) - should behave differently
|
|
||||||
model._set("C1", "=COUPDAYS(A1,A2,1,4)"); // European - no special Feb handling
|
|
||||||
model._set("C2", "=COUPDAYS(A3,A4,1,4)"); // European - no special Feb handling
|
|
||||||
model._set("C3", "=COUPDAYS(A5,A6,1,4)"); // European - no special Feb handling
|
|
||||||
model._set("C4", "=COUPDAYS(A7,A8,1,4)"); // European - no special Feb handling
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
// All should succeed without errors
|
|
||||||
for row in ["B1", "B2", "B3", "B4", "C1", "C2", "C3", "C4"] {
|
|
||||||
assert_ne!(model._get_text(row), *"#NUM!", "Failed for {row}");
|
|
||||||
assert!(
|
|
||||||
model._get_text(row).parse::<f64>().is_ok(),
|
|
||||||
"Non-numeric result for {row}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_coupon_functions_february_edge_cases() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
|
|
||||||
// Test that coupon functions handle February dates correctly without errors
|
|
||||||
|
|
||||||
// Settlement: February 28, 2023 (non-leap), Maturity: February 28, 2024 (leap)
|
|
||||||
model._set("A1", "=DATE(2023,2,28)");
|
|
||||||
model._set("A2", "=DATE(2024,2,28)");
|
|
||||||
|
|
||||||
// Test with basis 0 (US 30/360 - should use special February handling)
|
|
||||||
model._set("B1", "=COUPDAYBS(A1,A2,2,0)");
|
|
||||||
model._set("B2", "=COUPDAYS(A1,A2,2,0)");
|
|
||||||
model._set("B3", "=COUPDAYSNC(A1,A2,2,0)");
|
|
||||||
|
|
||||||
// Test with basis 4 (European 30/360 - should NOT use special February handling)
|
|
||||||
model._set("C1", "=COUPDAYBS(A1,A2,2,4)");
|
|
||||||
model._set("C2", "=COUPDAYS(A1,A2,2,4)");
|
|
||||||
model._set("C3", "=COUPDAYSNC(A1,A2,2,4)");
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
// With US method (basis 0), February dates should be handled specially
|
|
||||||
// With European method (basis 4), February dates should use actual dates
|
|
||||||
// Key point: both should work without errors
|
|
||||||
|
|
||||||
// We're ensuring functions complete successfully with February dates
|
|
||||||
assert_ne!(model._get_text("B1"), *"#NUM!");
|
|
||||||
assert_ne!(model._get_text("B2"), *"#NUM!");
|
|
||||||
assert_ne!(model._get_text("B3"), *"#NUM!");
|
|
||||||
assert_ne!(model._get_text("C1"), *"#NUM!");
|
|
||||||
assert_ne!(model._get_text("C2"), *"#NUM!");
|
|
||||||
assert_ne!(model._get_text("C3"), *"#NUM!");
|
|
||||||
}
|
|
||||||
24
base/src/test/test_fn_datevalue_timevalue.rs
Normal file
24
base/src/test/test_fn_datevalue_timevalue.rs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::test::util::new_empty_model;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn datevalue_timevalue_arguments() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
model._set("A1", "=DATEVALUE()");
|
||||||
|
model._set("A2", "=TIMEVALUE()");
|
||||||
|
model._set("A3", "=DATEVALUE("2000-01-01")")
|
||||||
|
model._set("A4", "=TIMEVALUE("12:00:00")")
|
||||||
|
model._set("A5", "=DATEVALUE(1,2)");
|
||||||
|
model._set("A6", "=TIMEVALUE(1,2)");
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A2"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A3"), *"36526");
|
||||||
|
assert_eq!(model._get_text("A4"), *"0.5");
|
||||||
|
assert_eq!(model._get_text("A5"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A6"), *"#ERROR!");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1,350 +0,0 @@
|
|||||||
#![allow(clippy::unwrap_used)]
|
|
||||||
#![allow(clippy::panic)]
|
|
||||||
|
|
||||||
use crate::{cell::CellValue, test::util::new_empty_model};
|
|
||||||
|
|
||||||
// Test constants for realistic bond scenarios
|
|
||||||
const BOND_SETTLEMENT: &str = "=DATE(2020,1,1)";
|
|
||||||
const BOND_MATURITY_4Y: &str = "=DATE(2024,1,1)";
|
|
||||||
const BOND_MATURITY_INVALID: &str = "=DATE(2016,1,1)"; // Before settlement
|
|
||||||
const BOND_MATURITY_SAME: &str = "=DATE(2020,1,1)"; // Same as settlement
|
|
||||||
const BOND_MATURITY_1DAY: &str = "=DATE(2020,1,2)"; // Very short term
|
|
||||||
|
|
||||||
// Standard investment-grade corporate bond parameters
|
|
||||||
const STD_COUPON: f64 = 0.08; // 8% annual coupon rate
|
|
||||||
const STD_YIELD: f64 = 0.09; // 9% yield (discount bond scenario)
|
|
||||||
const STD_FREQUENCY: i32 = 2; // Semi-annual payments (most common)
|
|
||||||
|
|
||||||
// Helper function to reduce test repetition
|
|
||||||
fn assert_numerical_result(model: &crate::Model, cell_ref: &str, should_be_positive: bool) {
|
|
||||||
if let Ok(CellValue::Number(v)) = model.get_cell_value_by_ref(cell_ref) {
|
|
||||||
if should_be_positive {
|
|
||||||
assert!(v > 0.0, "Expected positive value at {cell_ref}, got {v}");
|
|
||||||
}
|
|
||||||
// Value is valid - test passes
|
|
||||||
} else {
|
|
||||||
panic!("Expected numerical result at {cell_ref}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fn_duration_mduration_arguments() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
|
|
||||||
// Test argument count validation
|
|
||||||
model._set("A1", "=DURATION()");
|
|
||||||
model._set("A2", "=DURATION(1,2,3,4)");
|
|
||||||
model._set("A3", "=DURATION(1,2,3,4,5,6,7)");
|
|
||||||
|
|
||||||
model._set("B1", "=MDURATION()");
|
|
||||||
model._set("B2", "=MDURATION(1,2,3,4)");
|
|
||||||
model._set("B3", "=MDURATION(1,2,3,4,5,6,7)");
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
// Too few or too many arguments should result in errors
|
|
||||||
assert_eq!(model._get_text("A1"), *"#ERROR!");
|
|
||||||
assert_eq!(model._get_text("A2"), *"#ERROR!");
|
|
||||||
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
|
||||||
|
|
||||||
assert_eq!(model._get_text("B1"), *"#ERROR!");
|
|
||||||
assert_eq!(model._get_text("B2"), *"#ERROR!");
|
|
||||||
assert_eq!(model._get_text("B3"), *"#ERROR!");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fn_duration_mduration_settlement_maturity_errors() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
|
|
||||||
model._set("A1", BOND_SETTLEMENT);
|
|
||||||
model._set("A2", BOND_MATURITY_INVALID); // Before settlement
|
|
||||||
model._set("A3", BOND_MATURITY_SAME); // Same as settlement
|
|
||||||
|
|
||||||
// Both settlement > maturity and settlement = maturity should error
|
|
||||||
model._set(
|
|
||||||
"B1",
|
|
||||||
&format!("=DURATION(A1,A2,{STD_COUPON},{STD_YIELD},{STD_FREQUENCY})"),
|
|
||||||
);
|
|
||||||
model._set(
|
|
||||||
"B2",
|
|
||||||
&format!("=DURATION(A1,A3,{STD_COUPON},{STD_YIELD},{STD_FREQUENCY})"),
|
|
||||||
);
|
|
||||||
model._set(
|
|
||||||
"B3",
|
|
||||||
&format!("=MDURATION(A1,A2,{STD_COUPON},{STD_YIELD},{STD_FREQUENCY})"),
|
|
||||||
);
|
|
||||||
model._set(
|
|
||||||
"B4",
|
|
||||||
&format!("=MDURATION(A1,A3,{STD_COUPON},{STD_YIELD},{STD_FREQUENCY})"),
|
|
||||||
);
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
assert_eq!(model._get_text("B1"), *"#NUM!");
|
|
||||||
assert_eq!(model._get_text("B2"), *"#NUM!");
|
|
||||||
assert_eq!(model._get_text("B3"), *"#NUM!");
|
|
||||||
assert_eq!(model._get_text("B4"), *"#NUM!");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fn_duration_mduration_negative_values_errors() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
|
|
||||||
model._set("A1", BOND_SETTLEMENT);
|
|
||||||
model._set("A2", BOND_MATURITY_4Y);
|
|
||||||
|
|
||||||
// Test negative coupon (coupons must be >= 0)
|
|
||||||
model._set(
|
|
||||||
"B1",
|
|
||||||
&format!("=DURATION(A1,A2,-0.01,{STD_YIELD},{STD_FREQUENCY})"),
|
|
||||||
);
|
|
||||||
model._set(
|
|
||||||
"B2",
|
|
||||||
&format!("=MDURATION(A1,A2,-0.01,{STD_YIELD},{STD_FREQUENCY})"),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test negative yield (yields must be >= 0)
|
|
||||||
model._set(
|
|
||||||
"C1",
|
|
||||||
&format!("=DURATION(A1,A2,{STD_COUPON},-0.01,{STD_FREQUENCY})"),
|
|
||||||
);
|
|
||||||
model._set(
|
|
||||||
"C2",
|
|
||||||
&format!("=MDURATION(A1,A2,{STD_COUPON},-0.01,{STD_FREQUENCY})"),
|
|
||||||
);
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
assert_eq!(model._get_text("B1"), *"#NUM!");
|
|
||||||
assert_eq!(model._get_text("B2"), *"#NUM!");
|
|
||||||
assert_eq!(model._get_text("C1"), *"#NUM!");
|
|
||||||
assert_eq!(model._get_text("C2"), *"#NUM!");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fn_duration_mduration_invalid_frequency_errors() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
|
|
||||||
model._set("A1", BOND_SETTLEMENT);
|
|
||||||
model._set("A2", BOND_MATURITY_4Y);
|
|
||||||
|
|
||||||
// Only 1, 2, and 4 are valid frequencies (annual, semi-annual, quarterly)
|
|
||||||
let invalid_frequencies = [0, 3, 5, 12]; // Common invalid values
|
|
||||||
|
|
||||||
for (i, &freq) in invalid_frequencies.iter().enumerate() {
|
|
||||||
let row = i + 1;
|
|
||||||
model._set(
|
|
||||||
&format!("B{row}"),
|
|
||||||
&format!("=DURATION(A1,A2,{STD_COUPON},{STD_YIELD},{freq})"),
|
|
||||||
);
|
|
||||||
model._set(
|
|
||||||
&format!("C{row}"),
|
|
||||||
&format!("=MDURATION(A1,A2,{STD_COUPON},{STD_YIELD},{freq})"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
for i in 1..=invalid_frequencies.len() {
|
|
||||||
assert_eq!(model._get_text(&format!("B{i}")), *"#NUM!");
|
|
||||||
assert_eq!(model._get_text(&format!("C{i}")), *"#NUM!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fn_duration_mduration_frequency_variations() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
model._set("A1", BOND_SETTLEMENT);
|
|
||||||
model._set("A2", BOND_MATURITY_4Y);
|
|
||||||
|
|
||||||
// Test all valid frequencies: 1=annual, 2=semi-annual, 4=quarterly
|
|
||||||
let valid_frequencies = [1, 2, 4];
|
|
||||||
|
|
||||||
for (i, &freq) in valid_frequencies.iter().enumerate() {
|
|
||||||
let row = i + 1;
|
|
||||||
model._set(
|
|
||||||
&format!("B{row}"),
|
|
||||||
&format!("=DURATION(A1,A2,{STD_COUPON},{STD_YIELD},{freq})"),
|
|
||||||
);
|
|
||||||
model._set(
|
|
||||||
&format!("C{row}"),
|
|
||||||
&format!("=MDURATION(A1,A2,{STD_COUPON},{STD_YIELD},{freq})"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
// All should return positive numerical values
|
|
||||||
for i in 1..=valid_frequencies.len() {
|
|
||||||
assert_numerical_result(&model, &format!("Sheet1!B{i}"), true);
|
|
||||||
assert_numerical_result(&model, &format!("Sheet1!C{i}"), true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fn_duration_mduration_basis_variations() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
model._set("A1", BOND_SETTLEMENT);
|
|
||||||
model._set("A2", BOND_MATURITY_4Y);
|
|
||||||
|
|
||||||
// Test all valid basis values (day count conventions)
|
|
||||||
// 0=30/360 US, 1=Actual/actual, 2=Actual/360, 3=Actual/365, 4=30/360 European
|
|
||||||
for basis in 0..=4 {
|
|
||||||
let row = basis + 1;
|
|
||||||
model._set(
|
|
||||||
&format!("B{row}"),
|
|
||||||
&format!("=DURATION(A1,A2,{STD_COUPON},{STD_YIELD},{STD_FREQUENCY},{basis})"),
|
|
||||||
);
|
|
||||||
model._set(
|
|
||||||
&format!("C{row}"),
|
|
||||||
&format!("=MDURATION(A1,A2,{STD_COUPON},{STD_YIELD},{STD_FREQUENCY},{basis})"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test default basis (should be 0)
|
|
||||||
model._set(
|
|
||||||
"D1",
|
|
||||||
&format!("=DURATION(A1,A2,{STD_COUPON},{STD_YIELD},{STD_FREQUENCY})"),
|
|
||||||
);
|
|
||||||
model._set(
|
|
||||||
"D2",
|
|
||||||
&format!("=MDURATION(A1,A2,{STD_COUPON},{STD_YIELD},{STD_FREQUENCY})"),
|
|
||||||
);
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
// All basis values should work
|
|
||||||
for row in 1..=5 {
|
|
||||||
assert_numerical_result(&model, &format!("Sheet1!B{row}"), true);
|
|
||||||
assert_numerical_result(&model, &format!("Sheet1!C{row}"), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default basis should match basis 0
|
|
||||||
if let (Ok(CellValue::Number(d1)), Ok(CellValue::Number(b1))) = (
|
|
||||||
model.get_cell_value_by_ref("Sheet1!D1"),
|
|
||||||
model.get_cell_value_by_ref("Sheet1!B1"),
|
|
||||||
) {
|
|
||||||
assert!(
|
|
||||||
(d1 - b1).abs() < 1e-10,
|
|
||||||
"Default basis should match basis 0"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fn_duration_mduration_edge_cases() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
|
|
||||||
model._set("A1", BOND_SETTLEMENT);
|
|
||||||
model._set("A2", BOND_MATURITY_1DAY); // Very short term (1 day)
|
|
||||||
model._set("A3", BOND_MATURITY_4Y); // Standard term
|
|
||||||
|
|
||||||
// Edge case scenarios with explanations
|
|
||||||
let test_cases = [
|
|
||||||
("B", "A1", "A2", STD_COUPON, STD_YIELD, "short_term"), // 1-day bond
|
|
||||||
("C", "A1", "A3", 0.0, STD_YIELD, "zero_coupon"), // Zero coupon bond
|
|
||||||
("D", "A1", "A3", STD_COUPON, 0.0, "zero_yield"), // Zero yield
|
|
||||||
("E", "A1", "A3", 1.0, 0.5, "high_rates"), // High coupon/yield (100%/50%)
|
|
||||||
];
|
|
||||||
|
|
||||||
for (col, settlement, maturity, coupon, yield_rate, _scenario) in test_cases {
|
|
||||||
model._set(
|
|
||||||
&format!("{col}1"),
|
|
||||||
&format!("=DURATION({settlement},{maturity},{coupon},{yield_rate},{STD_FREQUENCY})"),
|
|
||||||
);
|
|
||||||
model._set(
|
|
||||||
&format!("{col}2"),
|
|
||||||
&format!("=MDURATION({settlement},{maturity},{coupon},{yield_rate},{STD_FREQUENCY})"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
// All edge cases should return positive values
|
|
||||||
for col in ["B", "C", "D", "E"] {
|
|
||||||
assert_numerical_result(&model, &format!("Sheet1!{col}1"), true);
|
|
||||||
assert_numerical_result(&model, &format!("Sheet1!{col}2"), true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fn_duration_mduration_relationship() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
model._set("A1", BOND_SETTLEMENT);
|
|
||||||
model._set("A2", BOND_MATURITY_4Y);
|
|
||||||
|
|
||||||
// Test mathematical relationship: MDURATION = DURATION / (1 + yield/frequency)
|
|
||||||
model._set(
|
|
||||||
"B1",
|
|
||||||
&format!("=DURATION(A1,A2,{STD_COUPON},{STD_YIELD},{STD_FREQUENCY})"),
|
|
||||||
);
|
|
||||||
model._set(
|
|
||||||
"B2",
|
|
||||||
&format!("=MDURATION(A1,A2,{STD_COUPON},{STD_YIELD},{STD_FREQUENCY})"),
|
|
||||||
);
|
|
||||||
model._set("B3", &format!("=B1/(1+{STD_YIELD}/{STD_FREQUENCY})")); // Manual calculation
|
|
||||||
|
|
||||||
// Test with quarterly frequency and different yield
|
|
||||||
model._set("C1", &format!("=DURATION(A1,A2,{STD_COUPON},0.12,4)"));
|
|
||||||
model._set("C2", &format!("=MDURATION(A1,A2,{STD_COUPON},0.12,4)"));
|
|
||||||
model._set("C3", "=C1/(1+0.12/4)"); // Manual calculation for quarterly
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
// MDURATION should equal DURATION / (1 + yield/frequency) for both scenarios
|
|
||||||
if let (Ok(CellValue::Number(md)), Ok(CellValue::Number(manual))) = (
|
|
||||||
model.get_cell_value_by_ref("Sheet1!B2"),
|
|
||||||
model.get_cell_value_by_ref("Sheet1!B3"),
|
|
||||||
) {
|
|
||||||
assert!(
|
|
||||||
(md - manual).abs() < 1e-10,
|
|
||||||
"MDURATION should equal DURATION/(1+yield/freq)"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let (Ok(CellValue::Number(md)), Ok(CellValue::Number(manual))) = (
|
|
||||||
model.get_cell_value_by_ref("Sheet1!C2"),
|
|
||||||
model.get_cell_value_by_ref("Sheet1!C3"),
|
|
||||||
) {
|
|
||||||
assert!(
|
|
||||||
(md - manual).abs() < 1e-10,
|
|
||||||
"MDURATION should equal DURATION/(1+yield/freq) for quarterly"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fn_duration_mduration_regression() {
|
|
||||||
// Original regression test with known expected values
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
model._set("A1", "=DATE(2016,1,1)");
|
|
||||||
model._set("A2", "=DATE(2020,1,1)");
|
|
||||||
model._set(
|
|
||||||
"B1",
|
|
||||||
&format!("=DURATION(A1,A2,{STD_COUPON},{STD_YIELD},{STD_FREQUENCY})"),
|
|
||||||
);
|
|
||||||
model._set(
|
|
||||||
"B2",
|
|
||||||
&format!("=MDURATION(A1,A2,{STD_COUPON},{STD_YIELD},{STD_FREQUENCY})"),
|
|
||||||
);
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
// Verify exact values for regression testing
|
|
||||||
if let Ok(CellValue::Number(v1)) = model.get_cell_value_by_ref("Sheet1!B1") {
|
|
||||||
assert!(
|
|
||||||
(v1 - 3.410746844012284).abs() < 1e-9,
|
|
||||||
"DURATION regression test failed"
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
panic!("Unexpected value for DURATION");
|
|
||||||
}
|
|
||||||
if let Ok(CellValue::Number(v2)) = model.get_cell_value_by_ref("Sheet1!B2") {
|
|
||||||
assert!(
|
|
||||||
(v2 - 3.263872578002186).abs() < 1e-9,
|
|
||||||
"MDURATION regression test failed"
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
panic!("Unexpected value for MDURATION");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
#![allow(clippy::unwrap_used)]
|
#![allow(clippy::unwrap_used)]
|
||||||
#![allow(clippy::panic)]
|
|
||||||
|
|
||||||
use crate::{cell::CellValue, test::util::new_empty_model};
|
use crate::{cell::CellValue, test::util::new_empty_model};
|
||||||
|
|
||||||
@@ -26,10 +25,6 @@ fn fn_arguments() {
|
|||||||
model._set("E2", "=RATE(1,1)");
|
model._set("E2", "=RATE(1,1)");
|
||||||
model._set("E3", "=RATE(1,1,1,1,1,1)");
|
model._set("E3", "=RATE(1,1,1,1,1,1)");
|
||||||
|
|
||||||
model._set("F1", "=FVSCHEDULE()");
|
|
||||||
model._set("F2", "=FVSCHEDULE(1)");
|
|
||||||
model._set("F3", "=FVSCHEDULE(1,1,1)");
|
|
||||||
|
|
||||||
model.evaluate();
|
model.evaluate();
|
||||||
|
|
||||||
assert_eq!(model._get_text("A1"), *"#ERROR!");
|
assert_eq!(model._get_text("A1"), *"#ERROR!");
|
||||||
@@ -51,10 +46,6 @@ fn fn_arguments() {
|
|||||||
assert_eq!(model._get_text("E1"), *"#ERROR!");
|
assert_eq!(model._get_text("E1"), *"#ERROR!");
|
||||||
assert_eq!(model._get_text("E2"), *"#ERROR!");
|
assert_eq!(model._get_text("E2"), *"#ERROR!");
|
||||||
assert_eq!(model._get_text("E3"), *"#ERROR!");
|
assert_eq!(model._get_text("E3"), *"#ERROR!");
|
||||||
|
|
||||||
assert_eq!(model._get_text("F1"), *"#ERROR!");
|
|
||||||
assert_eq!(model._get_text("F2"), *"#ERROR!");
|
|
||||||
assert_eq!(model._get_text("F3"), *"#ERROR!");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -477,18 +468,3 @@ fn fn_db_misc() {
|
|||||||
|
|
||||||
assert_eq!(model._get_text("B1"), "$0.00");
|
assert_eq!(model._get_text("B1"), "$0.00");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fn_fvschedule() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
model._set("A1", "1000");
|
|
||||||
model._set("A2", "0.08");
|
|
||||||
model._set("A3", "0.09");
|
|
||||||
model._set("A4", "0.1");
|
|
||||||
|
|
||||||
model._set("B1", "=FVSCHEDULE(A1, A2:A4)");
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
assert_eq!(model._get_text("B1"), "1294.92");
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,615 +0,0 @@
|
|||||||
#![allow(clippy::unwrap_used)]
|
|
||||||
|
|
||||||
use crate::{cell::CellValue, test::util::new_empty_model};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fn_price_yield() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
model._set("A1", "=DATE(2023,1,1)");
|
|
||||||
model._set("A2", "=DATE(2024,1,1)");
|
|
||||||
model._set("A3", "5%");
|
|
||||||
|
|
||||||
model._set("B1", "=PRICE(A1,A2,A3,6%,100,1)");
|
|
||||||
model._set("B2", "=YIELD(A1,A2,A3,B1,100,1)");
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
assert_eq!(model._get_text("B1"), "99.056603774");
|
|
||||||
assert_eq!(model._get_text("B2"), "0.06");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fn_price_frequencies() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
model._set("A1", "=DATE(2023,1,1)");
|
|
||||||
model._set("A2", "=DATE(2024,1,1)");
|
|
||||||
|
|
||||||
model._set("B1", "=PRICE(A1,A2,5%,6%,100,1)");
|
|
||||||
model._set("B2", "=PRICE(A1,A2,5%,6%,100,2)");
|
|
||||||
model._set("B3", "=PRICE(A1,A2,5%,6%,100,4)");
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
let annual: f64 = model._get_text("B1").parse().unwrap();
|
|
||||||
let semi: f64 = model._get_text("B2").parse().unwrap();
|
|
||||||
let quarterly: f64 = model._get_text("B3").parse().unwrap();
|
|
||||||
|
|
||||||
assert_ne!(annual, semi);
|
|
||||||
assert_ne!(semi, quarterly);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fn_yield_frequencies() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
model._set("A1", "=DATE(2023,1,1)");
|
|
||||||
model._set("A2", "=DATE(2024,1,1)");
|
|
||||||
|
|
||||||
model._set("B1", "=YIELD(A1,A2,5%,99,100,1)");
|
|
||||||
model._set("B2", "=YIELD(A1,A2,5%,99,100,2)");
|
|
||||||
model._set("B3", "=YIELD(A1,A2,5%,99,100,4)");
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
let annual: f64 = model._get_text("B1").parse().unwrap();
|
|
||||||
let semi: f64 = model._get_text("B2").parse().unwrap();
|
|
||||||
let quarterly: f64 = model._get_text("B3").parse().unwrap();
|
|
||||||
|
|
||||||
assert_ne!(annual, semi);
|
|
||||||
assert_ne!(semi, quarterly);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fn_price_argument_errors() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
model._set("A1", "=DATE(2023,1,1)");
|
|
||||||
model._set("A2", "=DATE(2024,1,1)");
|
|
||||||
|
|
||||||
model._set("B1", "=PRICE()");
|
|
||||||
model._set("B2", "=PRICE(A1,A2,5%,6%,100)");
|
|
||||||
model._set("B3", "=PRICE(A1,A2,5%,6%,100,2,0,99)");
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
assert_eq!(model._get_text("B1"), *"#ERROR!");
|
|
||||||
assert_eq!(model._get_text("B2"), *"#ERROR!");
|
|
||||||
assert_eq!(model._get_text("B3"), *"#ERROR!");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fn_yield_argument_errors() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
model._set("A1", "=DATE(2023,1,1)");
|
|
||||||
model._set("A2", "=DATE(2024,1,1)");
|
|
||||||
|
|
||||||
model._set("B1", "=YIELD()");
|
|
||||||
model._set("B2", "=YIELD(A1,A2,5%,99,100)");
|
|
||||||
model._set("B3", "=YIELD(A1,A2,5%,99,100,2,0,99)");
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
assert_eq!(model._get_text("B1"), *"#ERROR!");
|
|
||||||
assert_eq!(model._get_text("B2"), *"#ERROR!");
|
|
||||||
assert_eq!(model._get_text("B3"), *"#ERROR!");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fn_price_invalid_frequency() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
model._set("A1", "=DATE(2023,1,1)");
|
|
||||||
model._set("A2", "=DATE(2024,1,1)");
|
|
||||||
|
|
||||||
model._set("B1", "=PRICE(A1,A2,5%,6%,100,0)");
|
|
||||||
model._set("B2", "=PRICE(A1,A2,5%,6%,100,3)");
|
|
||||||
model._set("B3", "=PRICE(A1,A2,5%,6%,100,5)");
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
assert_eq!(model._get_text("B1"), *"#NUM!");
|
|
||||||
assert_eq!(model._get_text("B2"), *"#NUM!");
|
|
||||||
assert_eq!(model._get_text("B3"), *"#NUM!");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fn_pricedisc() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
model._set("A2", "=DATE(2022,1,25)");
|
|
||||||
model._set("A3", "=DATE(2022,11,15)");
|
|
||||||
model._set("A4", "3.75%");
|
|
||||||
model._set("A5", "100");
|
|
||||||
|
|
||||||
model._set("B1", "=PRICEDISC(A2,A3,A4,A5)");
|
|
||||||
model._set("C1", "=PRICEDISC(A2,A3)");
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
assert_eq!(model._get_text("B1"), "96.979166667");
|
|
||||||
assert_eq!(model._get_text("C1"), *"#ERROR!");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fn_pricemat() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
model._set("A1", "=DATE(2019,2,15)");
|
|
||||||
model._set("A2", "=DATE(2025,4,13)");
|
|
||||||
model._set("A3", "=DATE(2018,11,11)");
|
|
||||||
model._set("A4", "5.75%");
|
|
||||||
model._set("A5", "6.5%");
|
|
||||||
|
|
||||||
model._set("B1", "=PRICEMAT(A1,A2,A3,A4,A5)");
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
assert_eq!(model._get_text("B1"), "96.271187821");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fn_yielddisc() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
model._set("A1", "=DATE(2022,1,25)");
|
|
||||||
model._set("A2", "=DATE(2022,11,15)");
|
|
||||||
model._set("A3", "97");
|
|
||||||
model._set("A4", "100");
|
|
||||||
|
|
||||||
model._set("B1", "=YIELDDISC(A1,A2,A3,A4)");
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
assert_eq!(model._get_text("B1"), "0.038393175");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fn_yieldmat() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
model._set("A1", "=DATE(2019,2,15)");
|
|
||||||
model._set("A2", "=DATE(2025,4,13)");
|
|
||||||
model._set("A3", "=DATE(2018,11,11)");
|
|
||||||
model._set("A4", "5.75%");
|
|
||||||
model._set("A5", "96.27");
|
|
||||||
|
|
||||||
model._set("B1", "=YIELDMAT(A1,A2,A3,A4,A5)");
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
assert_eq!(model._get_text("B1"), "0.065002762");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fn_disc() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
model._set("A1", "=DATE(2022,1,25)");
|
|
||||||
model._set("A2", "=DATE(2022,11,15)");
|
|
||||||
model._set("A3", "97");
|
|
||||||
model._set("A4", "100");
|
|
||||||
|
|
||||||
model._set("B1", "=DISC(A1,A2,A3,A4)");
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
assert_eq!(model._get_text("B1"), "0.037241379");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fn_received() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
model._set("A1", "=DATE(2020,1,1)");
|
|
||||||
model._set("A2", "=DATE(2023,6,30)");
|
|
||||||
model._set("A3", "20000");
|
|
||||||
model._set("A4", "5%");
|
|
||||||
model._set("A5", "3");
|
|
||||||
|
|
||||||
model._set("B1", "=RECEIVED(A1,A2,A3,A4,A5)");
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
assert_eq!(model._get_text("B1"), "24236.387782205");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fn_intrate() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
model._set("A1", "=DATE(2020,1,1)");
|
|
||||||
model._set("A2", "=DATE(2023,6,30)");
|
|
||||||
model._set("A3", "10000");
|
|
||||||
model._set("A4", "12000");
|
|
||||||
model._set("A5", "3");
|
|
||||||
|
|
||||||
model._set("B1", "=INTRATE(A1,A2,A3,A4,A5)");
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
assert_eq!(model._get_text("B1"), "0.057210031");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fn_bond_functions_arguments() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
|
|
||||||
// PRICEDISC: 4-5 args
|
|
||||||
model._set("A1", "=PRICEDISC()");
|
|
||||||
model._set("A2", "=PRICEDISC(1,2,3)");
|
|
||||||
model._set("A3", "=PRICEDISC(1,2,3,4,5,6)");
|
|
||||||
|
|
||||||
// PRICEMAT: 5-6 args
|
|
||||||
model._set("B1", "=PRICEMAT()");
|
|
||||||
model._set("B2", "=PRICEMAT(1,2,3,4)");
|
|
||||||
model._set("B3", "=PRICEMAT(1,2,3,4,5,6,7)");
|
|
||||||
|
|
||||||
// YIELDDISC: 4-5 args
|
|
||||||
model._set("C1", "=YIELDDISC()");
|
|
||||||
model._set("C2", "=YIELDDISC(1,2,3)");
|
|
||||||
model._set("C3", "=YIELDDISC(1,2,3,4,5,6)");
|
|
||||||
|
|
||||||
// YIELDMAT: 5-6 args
|
|
||||||
model._set("D1", "=YIELDMAT()");
|
|
||||||
model._set("D2", "=YIELDMAT(1,2,3,4)");
|
|
||||||
model._set("D3", "=YIELDMAT(1,2,3,4,5,6,7)");
|
|
||||||
|
|
||||||
// DISC: 4-5 args
|
|
||||||
model._set("E1", "=DISC()");
|
|
||||||
model._set("E2", "=DISC(1,2,3)");
|
|
||||||
model._set("E3", "=DISC(1,2,3,4,5,6)");
|
|
||||||
|
|
||||||
// RECEIVED: 4-5 args
|
|
||||||
model._set("F1", "=RECEIVED()");
|
|
||||||
model._set("F2", "=RECEIVED(1,2,3)");
|
|
||||||
model._set("F3", "=RECEIVED(1,2,3,4,5,6)");
|
|
||||||
|
|
||||||
// INTRATE: 4-5 args
|
|
||||||
model._set("G1", "=INTRATE()");
|
|
||||||
model._set("G2", "=INTRATE(1,2,3)");
|
|
||||||
model._set("G3", "=INTRATE(1,2,3,4,5,6)");
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
assert_eq!(model._get_text("A1"), *"#ERROR!");
|
|
||||||
assert_eq!(model._get_text("A2"), *"#ERROR!");
|
|
||||||
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
|
||||||
|
|
||||||
assert_eq!(model._get_text("B1"), *"#ERROR!");
|
|
||||||
assert_eq!(model._get_text("B2"), *"#ERROR!");
|
|
||||||
assert_eq!(model._get_text("B3"), *"#ERROR!");
|
|
||||||
|
|
||||||
assert_eq!(model._get_text("C1"), *"#ERROR!");
|
|
||||||
assert_eq!(model._get_text("C2"), *"#ERROR!");
|
|
||||||
assert_eq!(model._get_text("C3"), *"#ERROR!");
|
|
||||||
|
|
||||||
assert_eq!(model._get_text("D1"), *"#ERROR!");
|
|
||||||
assert_eq!(model._get_text("D2"), *"#ERROR!");
|
|
||||||
assert_eq!(model._get_text("D3"), *"#ERROR!");
|
|
||||||
|
|
||||||
assert_eq!(model._get_text("E1"), *"#ERROR!");
|
|
||||||
assert_eq!(model._get_text("E2"), *"#ERROR!");
|
|
||||||
assert_eq!(model._get_text("E3"), *"#ERROR!");
|
|
||||||
|
|
||||||
assert_eq!(model._get_text("F1"), *"#ERROR!");
|
|
||||||
assert_eq!(model._get_text("F2"), *"#ERROR!");
|
|
||||||
assert_eq!(model._get_text("F3"), *"#ERROR!");
|
|
||||||
|
|
||||||
assert_eq!(model._get_text("G1"), *"#ERROR!");
|
|
||||||
assert_eq!(model._get_text("G2"), *"#ERROR!");
|
|
||||||
assert_eq!(model._get_text("G3"), *"#ERROR!");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fn_bond_functions_date_boundaries() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
|
|
||||||
// Date boundary values
|
|
||||||
model._set("A1", "0"); // Below MINIMUM_DATE_SERIAL_NUMBER
|
|
||||||
model._set("A2", "1"); // MINIMUM_DATE_SERIAL_NUMBER
|
|
||||||
model._set("A3", "2958465"); // MAXIMUM_DATE_SERIAL_NUMBER
|
|
||||||
model._set("A4", "2958466"); // Above MAXIMUM_DATE_SERIAL_NUMBER
|
|
||||||
|
|
||||||
// Test settlement < minimum
|
|
||||||
model._set("B1", "=PRICEDISC(A1,A2,0.05,100)");
|
|
||||||
model._set("B2", "=YIELDDISC(A1,A2,95,100)");
|
|
||||||
model._set("B3", "=DISC(A1,A2,95,100)");
|
|
||||||
model._set("B4", "=RECEIVED(A1,A2,1000,0.05)");
|
|
||||||
model._set("B5", "=INTRATE(A1,A2,1000,1050)");
|
|
||||||
|
|
||||||
// Test maturity > maximum
|
|
||||||
model._set("C1", "=PRICEDISC(A2,A4,0.05,100)");
|
|
||||||
model._set("C2", "=YIELDDISC(A2,A4,95,100)");
|
|
||||||
model._set("C3", "=DISC(A2,A4,95,100)");
|
|
||||||
model._set("C4", "=RECEIVED(A2,A4,1000,0.05)");
|
|
||||||
model._set("C5", "=INTRATE(A2,A4,1000,1050)");
|
|
||||||
|
|
||||||
// Test PRICEMAT/YIELDMAT with issue < minimum
|
|
||||||
model._set("D1", "=PRICEMAT(A2,A3,A1,0.06,0.05)");
|
|
||||||
model._set("D2", "=YIELDMAT(A2,A3,A1,0.06,99)");
|
|
||||||
|
|
||||||
// Test PRICEMAT/YIELDMAT with issue > maximum
|
|
||||||
model._set("E1", "=PRICEMAT(A2,A3,A4,0.06,0.05)");
|
|
||||||
model._set("E2", "=YIELDMAT(A2,A3,A4,0.06,99)");
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
assert_eq!(model._get_text("B1"), *"#NUM!");
|
|
||||||
assert_eq!(model._get_text("B2"), *"#NUM!");
|
|
||||||
assert_eq!(model._get_text("B3"), *"#NUM!");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fn_yield_invalid_frequency() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
model._set("A1", "=DATE(2023,1,1)");
|
|
||||||
model._set("A2", "=DATE(2024,1,1)");
|
|
||||||
|
|
||||||
model._set("B1", "=YIELD(A1,A2,5%,99,100,0)");
|
|
||||||
model._set("B2", "=YIELD(A1,A2,5%,99,100,3)");
|
|
||||||
model._set("B3", "=YIELD(A1,A2,5%,99,100,5)");
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
assert_eq!(model._get_text("B1"), *"#NUM!");
|
|
||||||
assert_eq!(model._get_text("B2"), *"#NUM!");
|
|
||||||
assert_eq!(model._get_text("B3"), *"#NUM!");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fn_bond_functions_date_ordering() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
|
|
||||||
model._set("A1", "=DATE(2022,1,1)"); // settlement
|
|
||||||
model._set("A2", "=DATE(2021,12,31)"); // maturity (before settlement)
|
|
||||||
model._set("A3", "=DATE(2020,1,1)"); // issue
|
|
||||||
|
|
||||||
// Test settlement >= maturity
|
|
||||||
model._set("B1", "=PRICEDISC(A1,A2,0.05,100)");
|
|
||||||
model._set("B2", "=YIELDDISC(A1,A2,95,100)");
|
|
||||||
model._set("B3", "=DISC(A1,A2,95,100)");
|
|
||||||
model._set("B4", "=RECEIVED(A1,A2,1000,0.05)");
|
|
||||||
model._set("B5", "=INTRATE(A1,A2,1000,1050)");
|
|
||||||
model._set("B6", "=PRICEMAT(A1,A2,A3,0.06,0.05)");
|
|
||||||
model._set("B7", "=YIELDMAT(A1,A2,A3,0.06,99)");
|
|
||||||
|
|
||||||
// Test settlement < issue for YIELDMAT/PRICEMAT
|
|
||||||
model._set("A4", "=DATE(2023,1,1)"); // later issue date
|
|
||||||
model._set("C1", "=PRICEMAT(A1,A2,A4,0.06,0.05)"); // settlement < issue
|
|
||||||
model._set("C2", "=YIELDMAT(A1,A2,A4,0.06,99)"); // settlement < issue
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
assert_eq!(model._get_text("B1"), *"#NUM!");
|
|
||||||
assert_eq!(model._get_text("B2"), *"#NUM!");
|
|
||||||
assert_eq!(model._get_text("B3"), *"#NUM!");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fn_price_invalid_dates() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
model._set("A1", "=DATE(2023,1,1)");
|
|
||||||
model._set("A2", "=DATE(2024,1,1)");
|
|
||||||
|
|
||||||
model._set("B1", "=PRICE(A2,A1,5%,6%,100,2)");
|
|
||||||
model._set("B2", "=PRICE(A1,A1,5%,6%,100,2)");
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
assert_eq!(model._get_text("B1"), *"#NUM!");
|
|
||||||
assert_eq!(model._get_text("B2"), *"#NUM!");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fn_bond_functions_parameter_validation() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
|
|
||||||
model._set("A1", "=DATE(2022,1,1)");
|
|
||||||
model._set("A2", "=DATE(2022,12,31)");
|
|
||||||
model._set("A3", "=DATE(2021,1,1)");
|
|
||||||
|
|
||||||
// Test negative/zero prices and redemptions
|
|
||||||
model._set("B1", "=PRICEDISC(A1,A2,0.05,0)"); // zero redemption
|
|
||||||
model._set("B2", "=PRICEDISC(A1,A2,0,100)"); // zero discount
|
|
||||||
model._set("B3", "=PRICEDISC(A1,A2,-0.05,100)"); // negative discount
|
|
||||||
|
|
||||||
model._set("C1", "=YIELDDISC(A1,A2,0,100)"); // zero price
|
|
||||||
model._set("C2", "=YIELDDISC(A1,A2,95,0)"); // zero redemption
|
|
||||||
model._set("C3", "=YIELDDISC(A1,A2,-95,100)"); // negative price
|
|
||||||
|
|
||||||
model._set("D1", "=DISC(A1,A2,0,100)"); // zero price
|
|
||||||
model._set("D2", "=DISC(A1,A2,95,0)"); // zero redemption
|
|
||||||
model._set("D3", "=DISC(A1,A2,-95,100)"); // negative price
|
|
||||||
|
|
||||||
model._set("E1", "=RECEIVED(A1,A2,0,0.05)"); // zero investment
|
|
||||||
model._set("E2", "=RECEIVED(A1,A2,1000,0)"); // zero discount
|
|
||||||
model._set("E3", "=RECEIVED(A1,A2,-1000,0.05)"); // negative investment
|
|
||||||
|
|
||||||
model._set("F1", "=INTRATE(A1,A2,0,1050)"); // zero investment
|
|
||||||
model._set("F2", "=INTRATE(A1,A2,1000,0)"); // zero redemption
|
|
||||||
model._set("F3", "=INTRATE(A1,A2,-1000,1050)"); // negative investment
|
|
||||||
|
|
||||||
model._set("G1", "=PRICEMAT(A1,A2,A3,-0.06,0.05)"); // negative rate
|
|
||||||
model._set("G2", "=PRICEMAT(A1,A2,A3,0.06,-0.05)"); // negative yield
|
|
||||||
|
|
||||||
model._set("H1", "=YIELDMAT(A1,A2,A3,0.06,0)"); // zero price
|
|
||||||
model._set("H2", "=YIELDMAT(A1,A2,A3,-0.06,99)"); // negative rate
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
assert_eq!(model._get_text("B1"), *"#NUM!");
|
|
||||||
assert_eq!(model._get_text("B2"), *"#NUM!");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fn_yield_invalid_dates() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
model._set("A1", "=DATE(2023,1,1)");
|
|
||||||
model._set("A2", "=DATE(2024,1,1)");
|
|
||||||
|
|
||||||
model._set("B1", "=YIELD(A2,A1,5%,99,100,2)");
|
|
||||||
model._set("B2", "=YIELD(A1,A1,5%,99,100,2)");
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
assert_eq!(model._get_text("B1"), *"#NUM!");
|
|
||||||
assert_eq!(model._get_text("B2"), *"#NUM!");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fn_price_with_basis() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
model._set("A1", "=DATE(2023,1,1)");
|
|
||||||
model._set("A2", "=DATE(2024,1,1)");
|
|
||||||
|
|
||||||
model._set("B1", "=PRICE(A1,A2,5%,6%,100,2,0)");
|
|
||||||
model._set("B2", "=PRICE(A1,A2,5%,6%,100,2,1)");
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
assert!(model._get_text("B1").parse::<f64>().is_ok());
|
|
||||||
assert!(model._get_text("B2").parse::<f64>().is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fn_yield_with_basis() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
model._set("A1", "=DATE(2023,1,1)");
|
|
||||||
model._set("A2", "=DATE(2024,1,1)");
|
|
||||||
|
|
||||||
model._set("B1", "=YIELD(A1,A2,5%,99,100,2,0)");
|
|
||||||
model._set("B2", "=YIELD(A1,A2,5%,99,100,2,1)");
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
assert!(model._get_text("B1").parse::<f64>().is_ok());
|
|
||||||
assert!(model._get_text("B2").parse::<f64>().is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fn_price_yield_inverse_functions() {
|
|
||||||
// Verifies PRICE and YIELD are mathematical inverses
|
|
||||||
// Regression test for periods calculation type mismatch
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
|
|
||||||
model._set("A1", "=DATE(2023,1,15)");
|
|
||||||
model._set("A2", "=DATE(2024,7,15)"); // ~1.5 years, fractional periods
|
|
||||||
model._set("A3", "4.75%"); // coupon
|
|
||||||
model._set("A4", "5.125%"); // yield
|
|
||||||
|
|
||||||
model._set("B1", "=PRICE(A1,A2,A3,A4,100,2)");
|
|
||||||
model._set("B2", "=YIELD(A1,A2,A3,B1,100,2)");
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
let calculated_yield: f64 = model._get_text("B2").parse().unwrap();
|
|
||||||
let expected_yield = 0.05125;
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
(calculated_yield - expected_yield).abs() < 1e-12,
|
|
||||||
"YIELD should recover original yield: expected {expected_yield}, got {calculated_yield}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fn_price_yield_round_trip_stability() {
|
|
||||||
// Tests numerical stability through multiple PRICE->YIELD->PRICE cycles
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
|
|
||||||
model._set("A1", "=DATE(2023,3,10)");
|
|
||||||
model._set("A2", "=DATE(2024,11,22)"); // Irregular period length
|
|
||||||
model._set("A3", "3.25%"); // coupon rate
|
|
||||||
model._set("A4", "4.875%"); // initial yield
|
|
||||||
|
|
||||||
// First round-trip
|
|
||||||
model._set("B1", "=PRICE(A1,A2,A3,A4,100,4)");
|
|
||||||
model._set("B2", "=YIELD(A1,A2,A3,B1,100,4)");
|
|
||||||
|
|
||||||
// Second round-trip
|
|
||||||
model._set("B3", "=PRICE(A1,A2,A3,B2,100,4)");
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
let price1: f64 = model._get_text("B1").parse().unwrap();
|
|
||||||
let price2: f64 = model._get_text("B3").parse().unwrap();
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
(price1 - price2).abs() < 1e-10,
|
|
||||||
"Round-trip should be stable: {price1} vs {price2}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fn_bond_functions_basis_validation() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
|
|
||||||
model._set("A1", "=DATE(2022,1,1)");
|
|
||||||
model._set("A2", "=DATE(2022,12,31)");
|
|
||||||
model._set("A3", "=DATE(2021,1,1)");
|
|
||||||
|
|
||||||
// Test valid basis values (0-4)
|
|
||||||
model._set("B1", "=PRICEDISC(A1,A2,0.05,100,0)");
|
|
||||||
model._set("B2", "=PRICEDISC(A1,A2,0.05,100,1)");
|
|
||||||
model._set("B3", "=PRICEDISC(A1,A2,0.05,100,2)");
|
|
||||||
model._set("B4", "=PRICEDISC(A1,A2,0.05,100,3)");
|
|
||||||
model._set("B5", "=PRICEDISC(A1,A2,0.05,100,4)");
|
|
||||||
|
|
||||||
// Test invalid basis values
|
|
||||||
model._set("C1", "=PRICEDISC(A1,A2,0.05,100,-1)");
|
|
||||||
model._set("C2", "=PRICEDISC(A1,A2,0.05,100,5)");
|
|
||||||
model._set("C3", "=YIELDDISC(A1,A2,95,100,10)");
|
|
||||||
model._set("C4", "=DISC(A1,A2,95,100,-5)");
|
|
||||||
model._set("C5", "=RECEIVED(A1,A2,1000,0.05,99)");
|
|
||||||
model._set("C6", "=INTRATE(A1,A2,1000,1050,-2)");
|
|
||||||
model._set("C7", "=PRICEMAT(A1,A2,A3,0.06,0.05,7)");
|
|
||||||
model._set("C8", "=YIELDMAT(A1,A2,A3,0.06,99,-3)");
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
// Valid basis should work
|
|
||||||
assert_ne!(model._get_text("B1"), *"#ERROR!");
|
|
||||||
assert_ne!(model._get_text("B2"), *"#ERROR!");
|
|
||||||
assert_ne!(model._get_text("B3"), *"#ERROR!");
|
|
||||||
assert_ne!(model._get_text("B4"), *"#ERROR!");
|
|
||||||
assert_ne!(model._get_text("B5"), *"#ERROR!");
|
|
||||||
|
|
||||||
// Invalid basis should error
|
|
||||||
assert_eq!(model._get_text("C1"), *"#NUM!");
|
|
||||||
assert_eq!(model._get_text("C2"), *"#NUM!");
|
|
||||||
assert_eq!(model._get_text("C3"), *"#NUM!");
|
|
||||||
assert_eq!(model._get_text("C4"), *"#NUM!");
|
|
||||||
assert_eq!(model._get_text("C5"), *"#NUM!");
|
|
||||||
assert_eq!(model._get_text("C6"), *"#NUM!");
|
|
||||||
assert_eq!(model._get_text("C7"), *"#NUM!");
|
|
||||||
assert_eq!(model._get_text("C8"), *"#NUM!");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fn_bond_functions_relationships() {
|
|
||||||
// Test mathematical relationships between functions
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
model._set("A1", "=DATE(2021,1,1)");
|
|
||||||
model._set("A2", "=DATE(2021,7,1)");
|
|
||||||
|
|
||||||
model._set("B1", "=PRICEDISC(A1,A2,5%,100)");
|
|
||||||
model._set("B2", "=YIELDDISC(A1,A2,B1,100)");
|
|
||||||
model._set("B3", "=DISC(A1,A2,B1,100)");
|
|
||||||
model._set("B4", "=RECEIVED(A1,A2,1000,5%)");
|
|
||||||
model._set("B5", "=INTRATE(A1,A2,1000,1050)");
|
|
||||||
model._set("B6", "=PRICEMAT(A1,A2,DATE(2020,7,1),6%,5%)");
|
|
||||||
model._set("B7", "=YIELDMAT(A1,A2,DATE(2020,7,1),6%,99)");
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
model.get_cell_value_by_ref("Sheet1!B1"),
|
|
||||||
Ok(CellValue::Number(97.5))
|
|
||||||
);
|
|
||||||
if let Ok(CellValue::Number(v)) = model.get_cell_value_by_ref("Sheet1!B2") {
|
|
||||||
assert!((v - 0.051282051).abs() < 1e-6);
|
|
||||||
}
|
|
||||||
if let Ok(CellValue::Number(v)) = model.get_cell_value_by_ref("Sheet1!B3") {
|
|
||||||
assert!((v - 0.05).abs() < 1e-6);
|
|
||||||
}
|
|
||||||
if let Ok(CellValue::Number(v)) = model.get_cell_value_by_ref("Sheet1!B4") {
|
|
||||||
assert!((v - 1025.641025).abs() < 1e-6);
|
|
||||||
}
|
|
||||||
if let Ok(CellValue::Number(v)) = model.get_cell_value_by_ref("Sheet1!B5") {
|
|
||||||
assert!((v - 0.10).abs() < 1e-6);
|
|
||||||
}
|
|
||||||
if let Ok(CellValue::Number(v)) = model.get_cell_value_by_ref("Sheet1!B6") {
|
|
||||||
assert!((v - 100.414634).abs() < 1e-6);
|
|
||||||
}
|
|
||||||
if let Ok(CellValue::Number(v)) = model.get_cell_value_by_ref("Sheet1!B7") {
|
|
||||||
assert!((v - 0.078431372).abs() < 1e-6);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
#![allow(clippy::unwrap_used)]
|
|
||||||
|
|
||||||
use crate::{cell::CellValue, test::util::new_empty_model};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn computation() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
model._set("B1", "0.1");
|
|
||||||
model._set("B2", "0.2");
|
|
||||||
model._set("A1", "=FVSCHEDULE(100,B1:B2)");
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
assert_eq!(model._get_text("A1"), "132");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fvschedule_basic_with_precise_assertion() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
model._set("A1", "1000");
|
|
||||||
model._set("B1", "0.09");
|
|
||||||
model._set("B2", "0.11");
|
|
||||||
model._set("B3", "0.1");
|
|
||||||
|
|
||||||
model._set("C1", "=FVSCHEDULE(A1,B1:B3)");
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
model.get_cell_value_by_ref("Sheet1!C1"),
|
|
||||||
Ok(CellValue::Number(1330.89))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fvschedule_compound_rates() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
model._set("A1", "1");
|
|
||||||
model._set("A2", "0.1");
|
|
||||||
model._set("A3", "0.2");
|
|
||||||
model._set("A4", "0.3");
|
|
||||||
|
|
||||||
model._set("B1", "=FVSCHEDULE(A1, A2:A4)");
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
// 1 * (1+0.1) * (1+0.2) * (1+0.3) = 1 * 1.1 * 1.2 * 1.3 = 1.716
|
|
||||||
assert_eq!(model._get_text("B1"), "1.716");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fvschedule_ignore_non_numbers() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
model._set("A1", "1");
|
|
||||||
model._set("A2", "0.1");
|
|
||||||
model._set("A3", "foo"); // non-numeric value should be ignored
|
|
||||||
model._set("A4", "0.2");
|
|
||||||
|
|
||||||
model._set("B1", "=FVSCHEDULE(A1, A2:A4)");
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
// 1 * (1+0.1) * (1+0.2) = 1 * 1.1 * 1.2 = 1.32
|
|
||||||
assert_eq!(model._get_text("B1"), "1.32");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fvschedule_argument_count() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
model._set("A1", "=FVSCHEDULE()");
|
|
||||||
model._set("A2", "=FVSCHEDULE(1)");
|
|
||||||
model._set("A3", "=FVSCHEDULE(1,1,1)");
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
assert_eq!(model._get_text("A1"), *"#ERROR!");
|
|
||||||
assert_eq!(model._get_text("A2"), *"#ERROR!");
|
|
||||||
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fvschedule_edge_cases() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
|
|
||||||
// Test with zero principal
|
|
||||||
model._set("A1", "0");
|
|
||||||
model._set("A2", "0.1");
|
|
||||||
model._set("A3", "0.2");
|
|
||||||
model._set("B1", "=FVSCHEDULE(A1, A2:A3)");
|
|
||||||
|
|
||||||
// Test with negative principal
|
|
||||||
model._set("C1", "-100");
|
|
||||||
model._set("D1", "=FVSCHEDULE(C1, A2:A3)");
|
|
||||||
|
|
||||||
// Test with zero rates
|
|
||||||
model._set("E1", "100");
|
|
||||||
model._set("E2", "0");
|
|
||||||
model._set("E3", "0");
|
|
||||||
model._set("F1", "=FVSCHEDULE(E1, E2:E3)");
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
assert_eq!(model._get_text("B1"), "0"); // 0 * anything = 0
|
|
||||||
assert_eq!(model._get_text("D1"), "-132"); // -100 * 1.1 * 1.2 = -132
|
|
||||||
assert_eq!(model._get_text("F1"), "100"); // 100 * 1 * 1 = 100
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fvschedule_rate_validation() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
|
|
||||||
// Test with rate exactly -1 (should cause error due to validation in patch 1)
|
|
||||||
model._set("A1", "100");
|
|
||||||
model._set("A2", "-1");
|
|
||||||
model._set("A3", "0.1");
|
|
||||||
model._set("B1", "=FVSCHEDULE(A1, A2:A3)");
|
|
||||||
|
|
||||||
// Test with rate less than -1 (should cause error)
|
|
||||||
model._set("C1", "100");
|
|
||||||
model._set("C2", "-1.5");
|
|
||||||
model._set("C3", "0.1");
|
|
||||||
model._set("D1", "=FVSCHEDULE(C1, C2:C3)");
|
|
||||||
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
assert_eq!(model._get_text("B1"), "#NUM!");
|
|
||||||
assert_eq!(model._get_text("D1"), "#NUM!");
|
|
||||||
}
|
|
||||||
22
base/src/test/test_mod_quotient.rs
Normal file
22
base/src/test/test_mod_quotient.rs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::test::util::new_empty_model;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn arguments() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
model._set("A1", "=MOD(5,2)");
|
||||||
|
model._set("A2", "=MOD()");
|
||||||
|
model._set("A3", "=MOD(5, 2, 1)");
|
||||||
|
model._set("A4", "=QUOTIENT(5, 2)");
|
||||||
|
model._set("A5", "=QUOTIENT()");
|
||||||
|
model._set("A6", "=QUOTIENT(5, 2, 1)");
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"1");
|
||||||
|
assert_eq!(model._get_text("A2"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A4"), *"2");
|
||||||
|
assert_eq!(model._get_text("A5"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A6"), *"#ERROR!");
|
||||||
|
}
|
||||||
40
base/src/test/test_mround_trunc_int
Normal file
40
base/src/test/test_mround_trunc_int
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::test::util::new_empty_model;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn arguments() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
model._set("A1", "=MROUND()");
|
||||||
|
model._set("A2", "=MROUND(10)");
|
||||||
|
model._set("A3", "=MROUND(10, 3)");
|
||||||
|
model._set("A4", "=MROUND(10, 3, 1)");
|
||||||
|
|
||||||
|
model._set("A5", "=TRUNC()");
|
||||||
|
model._set("A6", "=TRUNC(10)");
|
||||||
|
model._set("A7", "=TRUNC(10.22, 1)");
|
||||||
|
model._set("A8", "=TRUNC(10, 3, 1)");
|
||||||
|
|
||||||
|
model._set("A9", "=INT()");
|
||||||
|
model._set("A10", "=INT(10.22)");
|
||||||
|
model._set("A11", "=INT(10.22, 1)");
|
||||||
|
model._set("A12", "=INT(10.22, 1, 2)");
|
||||||
|
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A2"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A3"), *"9");
|
||||||
|
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A5"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A6"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A7"), *"10.2");
|
||||||
|
assert_eq!(model._get_text("A8"), *"#ERROR!");
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A9"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A10"), *"10");
|
||||||
|
assert_eq!(model._get_text("A11"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A12"), *"#ERROR!");
|
||||||
|
}
|
||||||
30
base/src/test/test_now.rs
Normal file
30
base/src/test/test_now.rs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::{mock_time, test::util::new_empty_model};
|
||||||
|
|
||||||
|
// 14:44 20 Mar 2023 Berlin
|
||||||
|
const TIMESTAMP_2023: i64 = 1679319865208;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn arguments() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
|
||||||
|
model._set("A1", "=NOW(1)");
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
model._get_text("A1"),
|
||||||
|
"#ERROR!",
|
||||||
|
"NOW should not accept arguments"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn returns_date_time() {
|
||||||
|
mock_time::set_mock_time(TIMESTAMP_2023);
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
model._set("A1", "=NOW()");
|
||||||
|
model.evaluate();
|
||||||
|
let text = model._get_text("A1");
|
||||||
|
assert_eq!(text, *"20/03/2023 13:44:25");
|
||||||
|
}
|
||||||
@@ -8,6 +8,15 @@ fn test_simple_format() {
|
|||||||
assert_eq!(formatted.text, "2.3".to_string());
|
assert_eq!(formatted.text, "2.3".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_maximum_zeros() {
|
||||||
|
let formatted = format_number(1.0 / 3.0, "#,##0.0000000000000000000", "en");
|
||||||
|
assert_eq!(formatted.text, "0.3333333333333330000".to_string());
|
||||||
|
|
||||||
|
let formatted = format_number(1234.0 + 1.0 / 3.0, "#,##0.0000000000000000000", "en");
|
||||||
|
assert_eq!(formatted.text, "1,234.3333333333300000000".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore = "not yet implemented"]
|
#[ignore = "not yet implemented"]
|
||||||
fn test_wrong_locale() {
|
fn test_wrong_locale() {
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ fn now_basic_utc() {
|
|||||||
model.evaluate();
|
model.evaluate();
|
||||||
|
|
||||||
assert_eq!(model._get_text("A1"), *"20/03/2023");
|
assert_eq!(model._get_text("A1"), *"20/03/2023");
|
||||||
assert_eq!(model._get_text("A2"), *"45005.572511574");
|
// 45005.572511574
|
||||||
|
assert_eq!(model._get_text("A2"), *"20/03/2023 13:44:25");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -46,5 +47,5 @@ fn now_basic_europe_berlin() {
|
|||||||
|
|
||||||
assert_eq!(model._get_text("A1"), *"20/03/2023");
|
assert_eq!(model._get_text("A1"), *"20/03/2023");
|
||||||
// This is UTC + 1 hour: 45005.572511574 + 1/24
|
// This is UTC + 1 hour: 45005.572511574 + 1/24
|
||||||
assert_eq!(model._get_text("A2"), *"45005.614178241");
|
assert_eq!(model._get_text("A2"), *"20/03/2023 14:44:25");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,3 +96,14 @@ fn test_fn_tan_pi2() {
|
|||||||
// This is consistent with IEEE 754 but inconsistent with Excel
|
// This is consistent with IEEE 754 but inconsistent with Excel
|
||||||
assert_eq!(model._get_text("A1"), *"1.63312E+16");
|
assert_eq!(model._get_text("A1"), *"1.63312E+16");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_trigonometric_identity() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
model._set("A1", "=COTH(1)*CSCH(1)");
|
||||||
|
model._set("A2", "=COSH(1)/(SINH(1))^2");
|
||||||
|
model._set("A3", "=A1=A2");
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A3"), *"TRUE");
|
||||||
|
}
|
||||||
|
|||||||
53
base/src/test/test_trigonometric_reciprocals.rs
Normal file
53
base/src/test/test_trigonometric_reciprocals.rs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::test::util::new_empty_model;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fn_arguments() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
model._set("A1", "=CSC()");
|
||||||
|
model._set("A2", "=SEC()");
|
||||||
|
model._set("A3", "=COT()");
|
||||||
|
|
||||||
|
model._set("A4", "=CSCH()");
|
||||||
|
model._set("A5", "=SECH()");
|
||||||
|
model._set("A6", "=COTH()");
|
||||||
|
|
||||||
|
model._set("A7", "=ACOT()");
|
||||||
|
model._set("A8", "=ACOTH()");
|
||||||
|
|
||||||
|
model._set("B1", "=CSC(1, 2)");
|
||||||
|
model._set("B2", "=SEC(1, 2)");
|
||||||
|
model._set("B3", "=COT(1, 2)");
|
||||||
|
|
||||||
|
model._set("B4", "=CSCH(1, 2)");
|
||||||
|
model._set("B5", "=SECH(1, 2)");
|
||||||
|
model._set("B6", "=COTH(1, 2)");
|
||||||
|
|
||||||
|
model._set("B7", "=ACOT(1, 2)");
|
||||||
|
model._set("B8", "=ACOTH(1, 2)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A2"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A5"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A6"), *"#ERROR!");
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A7"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A8"), *"#ERROR!");
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("B1"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("B2"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("B3"), *"#ERROR!");
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("B4"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("B5"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("B6"), *"#ERROR!");
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("B7"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("B8"), *"#ERROR!");
|
||||||
|
}
|
||||||
@@ -26,8 +26,8 @@ fn test_yearfrac_basis_2_actual_360() {
|
|||||||
panic!("Expected numeric value in A2");
|
panic!("Expected numeric value in A2");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Negative symmetric of A1
|
// always positive A1
|
||||||
assert_eq!(model._get_text("A3"), *"-1");
|
assert_eq!(model._get_text("A3"), *"1");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -254,19 +254,19 @@ fn invalid_names() {
|
|||||||
// spaces
|
// spaces
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
model.new_defined_name("A real", None, "Sheet1!$A$1"),
|
model.new_defined_name("A real", None, "Sheet1!$A$1"),
|
||||||
Err("Invalid defined name".to_string())
|
Err("Name: Invalid defined name".to_string())
|
||||||
);
|
);
|
||||||
|
|
||||||
// Starts with number
|
// Starts with number
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
model.new_defined_name("2real", None, "Sheet1!$A$1"),
|
model.new_defined_name("2real", None, "Sheet1!$A$1"),
|
||||||
Err("Invalid defined name".to_string())
|
Err("Name: Invalid defined name".to_string())
|
||||||
);
|
);
|
||||||
|
|
||||||
// Updating also fails
|
// Updating also fails
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
model.update_defined_name("MyName", None, "My Name", None, "Sheet1!$A$1"),
|
model.update_defined_name("MyName", None, "My Name", None, "Sheet1!$A$1"),
|
||||||
Err("Invalid defined name".to_string())
|
Err("Name: Invalid defined name".to_string())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,13 +284,13 @@ fn already_existing() {
|
|||||||
// Can't create a new name with the same name
|
// Can't create a new name with the same name
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
model.new_defined_name("MyName", None, "Sheet1!$A$2"),
|
model.new_defined_name("MyName", None, "Sheet1!$A$2"),
|
||||||
Err("Defined name already exists".to_string())
|
Err("Name: Defined name already exists".to_string())
|
||||||
);
|
);
|
||||||
|
|
||||||
// Can't update one into an existing
|
// Can't update one into an existing
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
model.update_defined_name("Another", None, "MyName", None, "Sheet1!$A$1"),
|
model.update_defined_name("Another", None, "MyName", None, "Sheet1!$A$1"),
|
||||||
Err("Defined name already exists".to_string())
|
Err("Name: Defined name already exists".to_string())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,17 +304,17 @@ fn invalid_sheet() {
|
|||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
model.new_defined_name("Mything", Some(2), "Sheet1!$A$1"),
|
model.new_defined_name("Mything", Some(2), "Sheet1!$A$1"),
|
||||||
Err("Invalid sheet index".to_string())
|
Err("Scope: Invalid sheet index".to_string())
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
model.update_defined_name("MyName", None, "MyName", Some(2), "Sheet1!$A$1"),
|
model.update_defined_name("MyName", None, "MyName", Some(2), "Sheet1!$A$1"),
|
||||||
Err("Invalid sheet index".to_string())
|
Err("Scope: Invalid sheet index".to_string())
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
model.update_defined_name("MyName", Some(9), "YourName", None, "Sheet1!$A$1"),
|
model.update_defined_name("MyName", Some(9), "YourName", None, "Sheet1!$A$1"),
|
||||||
Err("Invalid sheet index".to_string())
|
Err("General: Failed to get old name".to_string())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,7 +322,7 @@ fn invalid_sheet() {
|
|||||||
fn invalid_formula() {
|
fn invalid_formula() {
|
||||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||||
model.set_user_input(0, 1, 1, "Hello").unwrap();
|
model.set_user_input(0, 1, 1, "Hello").unwrap();
|
||||||
model.new_defined_name("MyName", None, "A1").unwrap();
|
assert!(model.new_defined_name("MyName", None, "A1").is_err());
|
||||||
|
|
||||||
model.set_user_input(0, 1, 2, "=MyName").unwrap();
|
model.set_user_input(0, 1, 2, "=MyName").unwrap();
|
||||||
|
|
||||||
|
|||||||
@@ -445,11 +445,13 @@ impl Default for Fill {
|
|||||||
|
|
||||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
|
#[derive(Default)]
|
||||||
pub enum HorizontalAlignment {
|
pub enum HorizontalAlignment {
|
||||||
Center,
|
Center,
|
||||||
CenterContinuous,
|
CenterContinuous,
|
||||||
Distributed,
|
Distributed,
|
||||||
Fill,
|
Fill,
|
||||||
|
#[default]
|
||||||
General,
|
General,
|
||||||
Justify,
|
Justify,
|
||||||
Left,
|
Left,
|
||||||
@@ -457,11 +459,6 @@ pub enum HorizontalAlignment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Note that alignment in "General" depends on type
|
// Note that alignment in "General" depends on type
|
||||||
impl Default for HorizontalAlignment {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::General
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HorizontalAlignment {
|
impl HorizontalAlignment {
|
||||||
fn is_default(&self) -> bool {
|
fn is_default(&self) -> bool {
|
||||||
@@ -487,7 +484,9 @@ impl Display for HorizontalAlignment {
|
|||||||
|
|
||||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
|
#[derive(Default)]
|
||||||
pub enum VerticalAlignment {
|
pub enum VerticalAlignment {
|
||||||
|
#[default]
|
||||||
Bottom,
|
Bottom,
|
||||||
Center,
|
Center,
|
||||||
Distributed,
|
Distributed,
|
||||||
@@ -501,12 +500,6 @@ impl VerticalAlignment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for VerticalAlignment {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Bottom
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for VerticalAlignment {
|
impl Display for VerticalAlignment {
|
||||||
fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
|
|||||||
@@ -329,6 +329,7 @@ impl Model {
|
|||||||
Function::Tbillyield => self.units_fn_percentage_2(args, cell),
|
Function::Tbillyield => self.units_fn_percentage_2(args, cell),
|
||||||
Function::Date => self.units_fn_dates(args, cell),
|
Function::Date => self.units_fn_dates(args, cell),
|
||||||
Function::Today => self.units_fn_dates(args, cell),
|
Function::Today => self.units_fn_dates(args, cell),
|
||||||
|
Function::Now => self.units_fn_date_times(args, cell),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -375,4 +376,8 @@ impl Model {
|
|||||||
// TODO: update locale and use it here
|
// TODO: update locale and use it here
|
||||||
Some(Units::Date("dd/mm/yyyy".to_string()))
|
Some(Units::Date("dd/mm/yyyy".to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn units_fn_date_times(&self, _args: &[Node], _cell: &CellReferenceIndex) -> Option<Units> {
|
||||||
|
Some(Units::Date("dd/mm/yyyy hh:mm:ss".to_string()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2001,7 +2001,10 @@ impl UserModel {
|
|||||||
new_scope: Option<u32>,
|
new_scope: Option<u32>,
|
||||||
new_formula: &str,
|
new_formula: &str,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let old_formula = self.model.get_defined_name_formula(name, scope)?;
|
let old_formula = self
|
||||||
|
.model
|
||||||
|
.get_defined_name_formula(name, scope)
|
||||||
|
.map_err(|_| "General: Failed to get old name")?;
|
||||||
let diff_list = vec![Diff::UpdateDefinedName {
|
let diff_list = vec![Diff::UpdateDefinedName {
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
scope,
|
scope,
|
||||||
@@ -2017,6 +2020,16 @@ impl UserModel {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// validates a new defined name
|
||||||
|
pub fn is_valid_defined_name(
|
||||||
|
&self,
|
||||||
|
name: &str,
|
||||||
|
scope: Option<u32>,
|
||||||
|
formula: &str,
|
||||||
|
) -> Result<Option<u32>, String> {
|
||||||
|
self.model.is_valid_defined_name(name, scope, formula)
|
||||||
|
}
|
||||||
|
|
||||||
// **** Private methods ****** //
|
// **** Private methods ****** //
|
||||||
|
|
||||||
pub(crate) fn push_diff_list(&mut self, diff_list: DiffList) {
|
pub(crate) fn push_diff_list(&mut self, diff_list: DiffList) {
|
||||||
|
|||||||
@@ -775,4 +775,17 @@ impl Model {
|
|||||||
.get_first_non_empty_in_row_after_column(sheet, row, column)
|
.get_first_non_empty_in_row_after_column(sheet, row, column)
|
||||||
.map_err(to_js_error)
|
.map_err(to_js_error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = "isValidDefinedName")]
|
||||||
|
pub fn is_valid_defined_name(
|
||||||
|
&self,
|
||||||
|
name: &str,
|
||||||
|
scope: Option<u32>,
|
||||||
|
formula: &str,
|
||||||
|
) -> Result<(), JsError> {
|
||||||
|
match self.model.is_valid_defined_name(name, scope, formula) {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(e) => Err(to_js_error(e.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2036,6 +2036,10 @@ export default defineConfig({
|
|||||||
text: "How to contribute",
|
text: "How to contribute",
|
||||||
link: "/contributing/how-to-contribute",
|
link: "/contributing/how-to-contribute",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: "Function documentation guide",
|
||||||
|
link: "/contributing/function-documentation-guide",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
389
docs/src/contributing/function-documentation-guide.md
Normal file
389
docs/src/contributing/function-documentation-guide.md
Normal file
@@ -0,0 +1,389 @@
|
|||||||
|
---
|
||||||
|
layout: doc
|
||||||
|
outline: deep
|
||||||
|
lang: en-US
|
||||||
|
---
|
||||||
|
|
||||||
|
# Function Documentation Guide
|
||||||
|
|
||||||
|
This guide explains how to document IronCalc functions following our established format and style conventions.
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
Function documentation files should be placed in the appropriate category directory under `src/functions/`. For example:
|
||||||
|
|
||||||
|
- Financial functions: `src/functions/financial/function-name.md`
|
||||||
|
- Text functions: `src/functions/text/function-name.md`
|
||||||
|
- Logical functions: `src/functions/logical/function-name.md`
|
||||||
|
|
||||||
|
## Required Frontmatter
|
||||||
|
|
||||||
|
Every function documentation file must start with this frontmatter:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
layout: doc
|
||||||
|
outline: deep
|
||||||
|
lang: en-US
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
## Document Structure
|
||||||
|
|
||||||
|
A complete function documentation should include the following sections in order:
|
||||||
|
|
||||||
|
### 1. Title
|
||||||
|
|
||||||
|
The title should be the function name followed by the word "function":
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# FV function
|
||||||
|
```
|
||||||
|
|
||||||
|
The function name should be written in uppercase when mentioned in the documentation.
|
||||||
|
|
||||||
|
### 2. Draft Warning (Optional)
|
||||||
|
|
||||||
|
If the function hasn't been implemented, include this warning box:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
::: warning
|
||||||
|
**Note:** This draft page is under construction 🚧
|
||||||
|
:::
|
||||||
|
```
|
||||||
|
|
||||||
|
If the function has been implemented but not documented, include this warning box:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
::: warning
|
||||||
|
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
||||||
|
:::
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Overview
|
||||||
|
|
||||||
|
Provide a brief, clear description of what the function does. If the function name is an acronym, expand it using underlined text:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
FV (<u>F</u>uture <u>V</u>alue) is a function of the Financial category that can be used to predict the future value of an investment or asset based on its present value.
|
||||||
|
```
|
||||||
|
|
||||||
|
Include:
|
||||||
|
|
||||||
|
- Category (Financial, Text, Logical, etc.)
|
||||||
|
- Primary purpose
|
||||||
|
- Key use cases (if helpful)
|
||||||
|
|
||||||
|
### 4. Usage
|
||||||
|
|
||||||
|
This section contains multiple subsections:
|
||||||
|
|
||||||
|
#### 4.1 Syntax
|
||||||
|
|
||||||
|
Format the function syntax with color-coded argument types. Use the following color scheme:
|
||||||
|
|
||||||
|
- **Numbers**: `#2F80ED` (blue)
|
||||||
|
- **Booleans**: `#27AE60` (green)
|
||||||
|
- **Text/Strings**: `#2F80ED` (orange)
|
||||||
|
- **Arrays/Ranges**: `#EB5757` (red)
|
||||||
|
|
||||||
|
**Format:**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
### Syntax
|
||||||
|
|
||||||
|
**FUNCTION_NAME(<span title="Type" style="color:#HEXCODE">arg1</span>, <span title="Type" style="color:#HEXCODE">arg2</span>=default, ...) => <span title="ReturnType" style="color:#HEXCODE">return_value</span>**
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
**FV(<span title="Number" style="color:#2F80ED">rate</span>, <span title="Number" style="color:#2F80ED">nper</span>, <span title="Number" style="color:#2F80ED">pmt</span>, [<span title="Number" style="color:#2F80ED">pv</span>], [<span title="Boolean" style="color:#27AE60">type</span>] => <span title="Number" style="color:#2F80ED">fv</span>**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
### Syntax
|
||||||
|
|
||||||
|
**FV(<span title="Number" style="color:#2F80ED">rate</span>, <span title="Number" style="color:#2F80ED">nper</span>, <span title="Number" style="color:#2F80ED">pmt</span>, <span title="Number" style="color:#2F80ED">pv</span>=0, <span title="Boolean" style="color:#27AE60">type</span>=FALSE) => <span title="Number" style="color:#2F80ED">fv</span>**
|
||||||
|
```
|
||||||
|
|
||||||
|
**Guidelines:**
|
||||||
|
|
||||||
|
- Use `title` attribute to specify the data type
|
||||||
|
- Use `style="color:#HEXCODE"` for syntax highlighting
|
||||||
|
- Use square brackets for optional arguments
|
||||||
|
- Show the return type after `=>`
|
||||||
|
- Make the entire syntax **bold**
|
||||||
|
|
||||||
|
#### 4.2 Argument Descriptions
|
||||||
|
|
||||||
|
List each argument with:
|
||||||
|
|
||||||
|
- Argument name in _italics_
|
||||||
|
- Data type link (e.g., `[number](/features/value-types#numbers)`)
|
||||||
|
- Required or optional indicator
|
||||||
|
- Description
|
||||||
|
|
||||||
|
**Format:**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
### Argument descriptions
|
||||||
|
|
||||||
|
- _argname_ ([datatype](/features/value-types#datatype), [required|optional](/features/optional-arguments.md)). Description of the argument.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
### Argument descriptions
|
||||||
|
|
||||||
|
- _rate_ ([number](/features/value-types#numbers), required). The fixed percentage interest rate or yield per period.
|
||||||
|
- _pv_ ([number](/features/value-types#numbers), [optional](/features/optional-arguments.md)). "pv" is the <u>p</u>resent <u>v</u>alue or starting amount of the asset (default 0).
|
||||||
|
- _type_ ([Boolean](/features/value-types#booleans), [optional](/features/optional-arguments.md)). A logical value indicating whether the payment due dates are at the end (FALSE or 0) of the compounding periods or at the beginning (TRUE or any non-zero value). The default is FALSE when omitted.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Guidelines:**
|
||||||
|
|
||||||
|
- Use bullet points (`*`)
|
||||||
|
- Italicize argument names with `*argname*`
|
||||||
|
- Link to value types documentation
|
||||||
|
- Link to optional arguments page when applicable
|
||||||
|
- Expand acronyms in descriptions using `<u>` tags if helpful
|
||||||
|
- Mention default values for optional arguments
|
||||||
|
|
||||||
|
#### 4.3 Additional Guidance
|
||||||
|
|
||||||
|
Provide tips, best practices and important notes about using the function:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
### Additional guidance
|
||||||
|
|
||||||
|
- Make sure that the _rate_ argument specifies the interest rate or yield applicable to the compounding period.
|
||||||
|
- The _pmt_ and _pv_ arguments should be expressed in the same currency unit.
|
||||||
|
- To ensure a worthwhile result, one of the _pmt_ and _pv_ arguments should be non-zero.
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4.4 Returned Value
|
||||||
|
|
||||||
|
Describe what the function returns:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
### Returned value
|
||||||
|
|
||||||
|
FV returns a [number](/features/value-types#numbers) representing the future value expressed in the same [currency unit](/features/units) that was used for the _pmt_ and _pv_ arguments.
|
||||||
|
```
|
||||||
|
|
||||||
|
Include:
|
||||||
|
|
||||||
|
- Return type (with link to value types if applicable)
|
||||||
|
- Units or format if relevant
|
||||||
|
- Any important characteristics
|
||||||
|
|
||||||
|
#### 4.5 Error Conditions
|
||||||
|
|
||||||
|
List all error scenarios the function may encounter:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
### Error conditions
|
||||||
|
|
||||||
|
- In common with many other IronCalc functions, FV propagates errors that are found in any of its arguments.
|
||||||
|
- If too few or too many arguments are supplied, FV returns the [`#ERROR!`](/features/error-types.md#error) error.
|
||||||
|
- If the value of any of the _rate_, _nper_, _pmt_ or _pv_ arguments is not (or cannot be converted to) a [number](/features/value-types#numbers), then FV returns the [`#VALUE!`](/features/error-types.md#value) error.
|
||||||
|
- If the value of the _type_ argument is not (or cannot be converted to) a [Boolean](/features/value-types#booleans), then FV again returns the [`#VALUE!`](/features/error-types.md#value) error.
|
||||||
|
- For some combinations of valid argument values, FV may return a [`#NUM!`](/features/error-types.md#num) error or a [`#DIV/0!`](/features/error-types.md#div-0) error.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Guidelines:**
|
||||||
|
|
||||||
|
- Use bullet points
|
||||||
|
- Format error types using backticks and link to the error types page: `` [`#ERROR!`](/features/error-types.md#error) ``
|
||||||
|
- Reference argument names in italics when discussing specific arguments
|
||||||
|
- Add the include directive at the end if using the error details snippet:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
<!--@include: ../markdown-snippets/error-type-details.txt-->
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Details (Optional but Recommended)
|
||||||
|
|
||||||
|
For functions with mathematical formulas or complex behavior, include a Details section. This section can also include plots, graphs or charts to help clarify the function's behavior.
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Details
|
||||||
|
|
||||||
|
- If $\text{type} \neq 0$, $\text{fv}$ is given by the equation:
|
||||||
|
$$ \text{fv} = -\text{pv} \times (1 + \text{rate})^\text{nper} - \dfrac{\text{pmt}\times\big({(1+\text{rate})^\text{nper}-1}\big) \times(1+\text{rate})}{\text{rate}}$$
|
||||||
|
|
||||||
|
- If $\text{type} = 0$, $\text{fv}$ is given by the equation:
|
||||||
|
$$ \text{fv} = -\text{pv} \times (1 + \text{rate})^{\text{nper}} - \dfrac{\text{pmt}\times\big({(1+\text{rate})^\text{nper}-1}\big)}{\text{rate}}$$
|
||||||
|
```
|
||||||
|
|
||||||
|
**Guidelines:**
|
||||||
|
|
||||||
|
- Use LaTeX math notation with `$` for inline and `$$` for block equations
|
||||||
|
- Use `\text{}` for variable names in equations
|
||||||
|
- Explain special cases or edge conditions
|
||||||
|
|
||||||
|
### 6. Examples
|
||||||
|
|
||||||
|
Link to interactive examples in IronCalc:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
[See some examples in IronCalc](https://app.ironcalc.com/?example=functionname).
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace `functionname` with the actual function name (lowercase).
|
||||||
|
|
||||||
|
### 7. Links
|
||||||
|
|
||||||
|
Provide external references and related functions:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Links
|
||||||
|
|
||||||
|
- For more information about the concept of "future value" in finance, visit Wikipedia's [Future value](https://en.wikipedia.org/wiki/Future_value) page.
|
||||||
|
- See also IronCalc's [NPER](/functions/financial/nper), [PMT](/functions/financial/pmt), [PV](/functions/financial/pv) and [RATE](/functions/financial/rate) functions.
|
||||||
|
- Visit Microsoft Excel's [FV function](https://support.microsoft.com/en-gb/office/fv-function-2eef9f44-a084-4c61-bdd8-4fe4bb1b71b3) page.
|
||||||
|
- Both [Google Sheets](https://support.google.com/docs/answer/3093224) and [LibreOffice Calc](https://wiki.documentfoundation.org/Documentation/Calc_Functions/FV) provide versions of the FV function.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Guidelines:**
|
||||||
|
|
||||||
|
- Include Wikipedia links for concepts when available
|
||||||
|
- Link to related IronCalc functions in the same category
|
||||||
|
- Include links to equivalent functions in Excel, Google Sheets, and LibreOffice Calc
|
||||||
|
- Use bullet points
|
||||||
|
|
||||||
|
## Syntax Coloring Reference
|
||||||
|
|
||||||
|
### Color Codes
|
||||||
|
|
||||||
|
| Data Type | Hex Color | Usage |
|
||||||
|
| ----------- | --------- | --------------------------------------- |
|
||||||
|
| Number | `#2F80ED` | All numeric arguments and return values |
|
||||||
|
| Boolean | `#27AE60` | TRUE/FALSE arguments |
|
||||||
|
| Text/String | `#F2994A` | Text arguments |
|
||||||
|
| Array/Range | `#EB5757` | Array or range arguments |
|
||||||
|
|
||||||
|
### Syntax Highlighting Template
|
||||||
|
|
||||||
|
```html
|
||||||
|
<span title="Type" style="color:#HEXCODE">argument_name</span>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
- Number: `<span title="Number" style="color:#2F80ED">rate</span>`
|
||||||
|
- Boolean: `<span title="Boolean" style="color:#27AE60">type</span>`
|
||||||
|
- Text: `<span title="Text" style="color:#F2994A">text</span>`
|
||||||
|
|
||||||
|
## Formatting Conventions
|
||||||
|
|
||||||
|
### Text Formatting
|
||||||
|
|
||||||
|
- **Function names**: Use exact case as in IronCalc
|
||||||
|
- **Argument names**: Use _italics_ when referencing in prose
|
||||||
|
- **Acronyms**: Expand using `<u>` tags: `<u>F</u>uture <u>V</u>alue`
|
||||||
|
- **Code/values**: Use backticks for error codes: `` `#ERROR!` ``
|
||||||
|
- **Links**: Use descriptive link text, not raw URLs
|
||||||
|
|
||||||
|
### Section Headers
|
||||||
|
|
||||||
|
- Use `#` for the page title with the function name (e.g., FV Function)
|
||||||
|
- Use `##` for main sections (Overview, Usage, Details, Examples, Links)
|
||||||
|
- Use `###` for subsections (Syntax, Argument descriptions, etc.)
|
||||||
|
|
||||||
|
### Lists
|
||||||
|
|
||||||
|
- Use bullet points (`*`) for argument descriptions and error conditions
|
||||||
|
- Use numbered lists only when order matters
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
Before submitting a function documentation, ensure:
|
||||||
|
|
||||||
|
- [ ] Frontmatter is correct
|
||||||
|
- [ ] Title follows the format "FUNCTION_NAME function"
|
||||||
|
- [ ] Overview clearly explains the function's purpose
|
||||||
|
- [ ] Syntax is color-coded correctly
|
||||||
|
- [ ] All arguments are documented with correct types
|
||||||
|
- [ ] Required vs optional arguments are clearly marked
|
||||||
|
- [ ] Return value is described
|
||||||
|
- [ ] Error conditions are comprehensive
|
||||||
|
- [ ] Examples link is included
|
||||||
|
- [ ] Links section includes relevant references
|
||||||
|
- [ ] Mathematical formulas (if any) use proper LaTeX syntax
|
||||||
|
- [ ] All internal links use relative paths
|
||||||
|
- [ ] Spelling and grammar are correct
|
||||||
|
|
||||||
|
## Example Template
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
layout: doc
|
||||||
|
outline: deep
|
||||||
|
lang: en-US
|
||||||
|
---
|
||||||
|
|
||||||
|
# FUNCTION_NAME function
|
||||||
|
|
||||||
|
::: warning
|
||||||
|
**Note:** This draft page is under construction 🚧
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
FUNCTION_NAME (<u>A</u>cronym <u>E</u>xplanation) is a function of the [Category] category that can be used to [primary purpose].
|
||||||
|
|
||||||
|
[Additional context about when to use this function or related functions.]
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Syntax
|
||||||
|
|
||||||
|
**FUNCTION_NAME(<span title="Type" style="color:#2F80ED">arg1</span>, [<span title="Type" style="color:#2F80ED">arg2</span>], [<span title="Boolean" style="color:#27AE60">arg3</span>]) => <span title="Type" style="color:#2F80ED">return_value</span>**
|
||||||
|
|
||||||
|
### Argument descriptions
|
||||||
|
|
||||||
|
- _arg1_ ([type](/features/value-types#type), required). Description.
|
||||||
|
- _arg2_ ([type](/features/value-types#type), [optional](/features/optional-arguments.md)). Description (default value).
|
||||||
|
- _arg3_ ([Boolean](/features/value-types#booleans), [optional](/features/optional-arguments.md)). Description (default FALSE).
|
||||||
|
|
||||||
|
### Additional guidance
|
||||||
|
|
||||||
|
- Tip or best practice.
|
||||||
|
- Another important note.
|
||||||
|
|
||||||
|
### Returned value
|
||||||
|
|
||||||
|
FUNCTION_NAME returns a [type](/features/value-types#type) representing [description].
|
||||||
|
|
||||||
|
### Error conditions
|
||||||
|
|
||||||
|
- General error propagation note.
|
||||||
|
- Specific error condition with [`#ERROR!`](/features/error-types.md#error) link.
|
||||||
|
- Another error condition.
|
||||||
|
|
||||||
|
<!--@include: ../markdown-snippets/error-type-details.txt-->
|
||||||
|
|
||||||
|
## Details
|
||||||
|
|
||||||
|
[Mathematical formulas or detailed explanations if needed]
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
[See some examples in IronCalc](https://app.ironcalc.com/?example=functionname).
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
- Wikipedia link if applicable.
|
||||||
|
- Related IronCalc functions.
|
||||||
|
- Microsoft Excel documentation.
|
||||||
|
- Google Sheets and LibreOffice Calc links.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Questions?
|
||||||
|
|
||||||
|
If you have questions about documenting functions, reach out on our [Discord Channel](https://discord.com/invite/zZYWfh3RHJ) or check existing function documentation for examples.
|
||||||
@@ -6,4 +6,10 @@ lang: en-US
|
|||||||
|
|
||||||
# How to Contribute
|
# How to Contribute
|
||||||
|
|
||||||
If you want to give us a hand, take a look at our [GitHub repository](https://github.com/ironcalc/IronCalc?tab=readme-ov-file#collaborators-needed-call-to-action), join our [Discord Channel](https://discord.com/invite/zZYWfh3RHJ) or send us an email to [hello@ironcalc.com](mailto:hello@ironcalc.com).
|
If you want to give us a hand, take a look at our [GitHub repository](https://github.com/ironcalc/IronCalc?tab=readme-ov-file#collaborators-needed-call-to-action) – we've marked some issues there with the tag 'good first issue' that could serve you as a starting point.
|
||||||
|
|
||||||
|
If you also want to discuss topics, share your thoughts or just say 'hi', you can join our [Discord Channel](https://discord.com/invite/zZYWfh3RHJ) or send us an email to [hello@ironcalc.com](mailto:hello@ironcalc.com).
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
If you're interested in contributing to our documentation, especially function documentation, please see our [Function Documentation Guide](/contributing/function-documentation-guide).
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ All Date and Time functions are already supported in IronCalc.
|
|||||||
| ---------------- | ---------------------------------------------- | ------------- |
|
| ---------------- | ---------------------------------------------- | ------------- |
|
||||||
| DATE | <Badge type="tip" text="Available" /> | – |
|
| DATE | <Badge type="tip" text="Available" /> | – |
|
||||||
| DATEDIF | <Badge type="tip" text="Available" /> | – |
|
| DATEDIF | <Badge type="tip" text="Available" /> | – |
|
||||||
| DATEVALUE | <Badge type="tip" text="Available" /> | – |
|
| DATEVALUE | <Badge type="tip" text="Available" /> | [DATEVALUE](date_and_time/datevalue) |
|
||||||
| DAY | <Badge type="tip" text="Available" /> | [DAY](date_and_time/day) |
|
| DAY | <Badge type="tip" text="Available" /> | [DAY](date_and_time/day) |
|
||||||
| DAYS | <Badge type="tip" text="Available" /> | – |
|
| DAYS | <Badge type="tip" text="Available" /> | – |
|
||||||
| DAYS360 | <Badge type="tip" text="Available" /> | – |
|
| DAYS360 | <Badge type="tip" text="Available" /> | – |
|
||||||
@@ -27,7 +27,7 @@ All Date and Time functions are already supported in IronCalc.
|
|||||||
| NOW | <Badge type="tip" text="Available" /> | – |
|
| NOW | <Badge type="tip" text="Available" /> | – |
|
||||||
| SECOND | <Badge type="tip" text="Available" /> | – |
|
| SECOND | <Badge type="tip" text="Available" /> | – |
|
||||||
| TIME | <Badge type="tip" text="Available" /> | – |
|
| TIME | <Badge type="tip" text="Available" /> | – |
|
||||||
| TIMEVALUE | <Badge type="tip" text="Available" /> | – |
|
| TIMEVALUE | <Badge type="tip" text="Available" /> | [TIMEVALUE](date_and_time/timevalue) |
|
||||||
| TODAY | <Badge type="tip" text="Available" /> | – |
|
| TODAY | <Badge type="tip" text="Available" /> | – |
|
||||||
| WEEKDAY | <Badge type="tip" text="Available" /> | – |
|
| WEEKDAY | <Badge type="tip" text="Available" /> | – |
|
||||||
| WEEKNUM | <Badge type="tip" text="Available" /> | – |
|
| WEEKNUM | <Badge type="tip" text="Available" /> | – |
|
||||||
|
|||||||
@@ -4,8 +4,41 @@ outline: deep
|
|||||||
lang: en-US
|
lang: en-US
|
||||||
---
|
---
|
||||||
|
|
||||||
# DATEVALUE
|
# DATEVALUE function
|
||||||
|
|
||||||
::: warning
|
## Overview
|
||||||
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
DATEVALUE is a function of the Date and Time category that converts a date stored as text to a [serial number](/features/serial-numbers.md) corresponding to a date value.
|
||||||
:::
|
|
||||||
|
## Usage
|
||||||
|
### Syntax
|
||||||
|
**DATEVALUE(<span title="Text" style="color:#1E88E5">date_text</span>) => <span title="Number" style="color:#1E88E5">datevalue</span>**
|
||||||
|
|
||||||
|
### Argument descriptions
|
||||||
|
* *date_text* ([text](/features/value-types#strings), required). A text string that represents a date in a known format. The text must represent a date between December 31, 1899 and December 31, 9999.
|
||||||
|
|
||||||
|
### Additional guidance
|
||||||
|
* If the year portion of the *date_text* argument is omitted, DATEVALUE uses the current year from the system clock.
|
||||||
|
* Time information in the *date_text* argument is ignored. DATEVALUE processes only the date portion.
|
||||||
|
|
||||||
|
### Returned value
|
||||||
|
DATEVALUE returns a [number](/features/value-types#numbers) that represents the date as a [serial number](/features/serial-numbers.md). The serial number corresponds to the number of days since December 31, 1899.
|
||||||
|
|
||||||
|
### Error conditions
|
||||||
|
* In common with many other IronCalc functions, DATEVALUE propagates errors that are found in its argument.
|
||||||
|
* If no argument, or more than one argument, is supplied, then DATEVALUE returns the [`#ERROR!`](/features/error-types.md#error) error.
|
||||||
|
* If the value of the *date_text* argument is not (or cannot be converted to) a [text](/features/value-types#strings) value, then DATEVALUE returns the [`#VALUE!`](/features/error-types.md#value) error.
|
||||||
|
* If the *date_text* argument represents a date outside the valid range (before December 31, 1899 or after December 31, 9999), then DATEVALUE returns the [`#VALUE!`](/features/error-types.md#value) error.
|
||||||
|
* If the *date_text* argument cannot be recognized as a valid date format, then DATEVALUE returns the [`#VALUE!`](/features/error-types.md#value) error.
|
||||||
|
<!--@include: ../markdown-snippets/error-type-details.txt-->
|
||||||
|
|
||||||
|
<!-- ## Details
|
||||||
|
For more information on how IronCalc processes Date and Time functions and values, visit [Date and Time](/features/serial-numbers.md)
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
[See some examples in IronCalc](https://app.ironcalc.com/?example=datevalue).
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Links
|
||||||
|
* See also IronCalc's [TIMEVALUE](/functions/date_and_time/timevalue.md) function for converting time text to serial numbers.
|
||||||
|
* Visit Microsoft Excel's [DATEVALUE function](https://support.microsoft.com/en-us/office/datevalue-function-df8b07d4-7761-4a93-bc33-b7471bbff252) page.
|
||||||
|
* Both [Google Sheets](https://support.google.com/docs/answer/3093039) and [LibreOffice Calc](https://wiki.documentfoundation.org/Documentation/Calc_Functions/DATEVALUE) provide versions of the DATEVALUE function.
|
||||||
@@ -4,9 +4,42 @@ outline: deep
|
|||||||
lang: en-US
|
lang: en-US
|
||||||
---
|
---
|
||||||
|
|
||||||
# TIMEVALUE
|
# TIMEVALUE function
|
||||||
|
|
||||||
::: warning
|
## Overview
|
||||||
**Note:** This draft page is under construction 🚧
|
TIMEVALUE is a function of the Date and Time category that converts a time stored as text to a [serial number](/features/serial-numbers.md) corresponding to a time value. The serial number represents time as a decimal fraction of a 24-hour day (e.g., 0.5 represents 12:00:00 noon).
|
||||||
The TIMEVALUE function is implemented and available in IronCalc.
|
|
||||||
:::
|
## Usage
|
||||||
|
### Syntax
|
||||||
|
**TIMEVALUE(<span title="Text" style="color:#1E88E5">time_text</span>) => <span title="Number" style="color:#1E88E5">timevalue</span>**
|
||||||
|
|
||||||
|
### Argument descriptions
|
||||||
|
* *time_text* ([text](/features/value-types#strings), required). A text string that represents a time in a known format. The text must represent a time between 00:00:00 and 23:59:59.
|
||||||
|
|
||||||
|
### Additional guidance
|
||||||
|
* Date information in the *time_text* argument is ignored. TIMEVALUE processes only the time portion.
|
||||||
|
* The function can handle various time formats, including both 12-hour and 24-hour formats, as well as text that includes both date and time information.
|
||||||
|
|
||||||
|
### Returned value
|
||||||
|
TIMEVALUE returns a [number](/features/value-types#numbers) that represents the time as a [serial number](/features/serial-numbers.md). The serial number is a decimal fraction of a 24-hour day, where:
|
||||||
|
* 0.0 represents 00:00:00 (midnight)
|
||||||
|
* 0.5 represents 12:00:00 (midday)
|
||||||
|
* 0.99999... represents 23:59:59 (just before midnight)
|
||||||
|
|
||||||
|
### Error conditions
|
||||||
|
* In common with many other IronCalc functions, TIMEVALUE propagates errors that are found in its argument.
|
||||||
|
* If no argument, or more than one argument, is supplied, then TIMEVALUE returns the [`#ERROR!`](/features/error-types.md#error) error.
|
||||||
|
* If the value of the *time_text* argument is not (or cannot be converted to) a [text](/features/value-types#strings) value, then TIMEVALUE returns the [`#VALUE!`](/features/error-types.md#value) error.
|
||||||
|
* If the *time_text* argument represents a time outside the valid range, then TIMEVALUE returns the [`#VALUE!`](/features/error-types.md#value) error.
|
||||||
|
* If the *time_text* argument cannot be recognized as a valid time format, then TIMEVALUE returns the [`#VALUE!`](/features/error-types.md#value) error.
|
||||||
|
<!--@include: ../markdown-snippets/error-type-details.txt-->
|
||||||
|
|
||||||
|
<!--
|
||||||
|
## Examples
|
||||||
|
[See some examples in IronCalc](https://app.ironcalc.com/?example=timevalue).
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Links
|
||||||
|
* See also IronCalc's [DATEVALUE](/functions/date_and_time/datevalue.md) function for converting date text to serial numbers.
|
||||||
|
* Visit Microsoft Excel's [TIMEVALUE function](https://support.microsoft.com/en-us/office/timevalue-function-0b615c12-33d8-4431-bf3d-f3eb6d186645) page.
|
||||||
|
* Both [Google Sheets](https://support.google.com/docs/answer/3267350) and [LibreOffice Calc](https://wiki.documentfoundation.org/Documentation/Calc_Functions/TIMEVALUE) provide versions of the TIMEVALUE function.
|
||||||
@@ -11,32 +11,32 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
|
|||||||
|
|
||||||
| Function | Status | Documentation |
|
| Function | Status | Documentation |
|
||||||
| ---------- | ---------------------------------------------- | ------------------ |
|
| ---------- | ---------------------------------------------- | ------------------ |
|
||||||
| ACCRINT | <Badge type="tip" text="Available" /> | [ACCRINT](financial/accrint) |
|
| ACCRINT | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| ACCRINTM | <Badge type="tip" text="Available" /> | [ACCRINTM](financial/accrintm) |
|
| ACCRINTM | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| AMORDEGRC | <Badge type="info" text="Not implemented yet" /> | – |
|
| AMORDEGRC | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| AMORLINC | <Badge type="info" text="Not implemented yet" /> | – |
|
| AMORLINC | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| COUPDAYBS | <Badge type="tip" text="Available" /> | – |
|
| COUPDAYBS | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| COUPDAYS | <Badge type="tip" text="Available" /> | – |
|
| COUPDAYS | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| COUPDAYSNC | <Badge type="tip" text="Available" /> | – |
|
| COUPDAYSNC | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| COUPNCD | <Badge type="tip" text="Available" /> | – |
|
| COUPNCD | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| COUPNUM | <Badge type="tip" text="Available" /> | – |
|
| COUPNUM | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| COUPPCD | <Badge type="tip" text="Available" /> | – |
|
| COUPPCD | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| CUMIPMT | <Badge type="tip" text="Available" /> | – |
|
| CUMIPMT | <Badge type="tip" text="Available" /> | – |
|
||||||
| CUMPRINC | <Badge type="tip" text="Available" /> | – |
|
| CUMPRINC | <Badge type="tip" text="Available" /> | – |
|
||||||
| DB | <Badge type="tip" text="Available" /> | – |
|
| DB | <Badge type="tip" text="Available" /> | – |
|
||||||
| DDB | <Badge type="tip" text="Available" /> | – |
|
| DDB | <Badge type="tip" text="Available" /> | – |
|
||||||
| DISC | <Badge type="tip" text="Available" /> | – |
|
| DISC | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| DOLLARDE | <Badge type="tip" text="Available" /> | – |
|
| DOLLARDE | <Badge type="tip" text="Available" /> | – |
|
||||||
| DOLLARFR | <Badge type="tip" text="Available" /> | – |
|
| DOLLARFR | <Badge type="tip" text="Available" /> | – |
|
||||||
| DURATION | <Badge type="tip" text="Available" /> | – |
|
| DURATION | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| EFFECT | <Badge type="tip" text="Available" /> | – |
|
| EFFECT | <Badge type="tip" text="Available" /> | – |
|
||||||
| FV | <Badge type="tip" text="Available" /> | [FV](financial/fv) |
|
| FV | <Badge type="tip" text="Available" /> | [FV](financial/fv) |
|
||||||
| FVSCHEDULE | <Badge type="tip" text="Available" /> | [FVSCHEDULE](financial/fvschedule) |
|
| FVSCHEDULE | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| INTRATE | <Badge type="tip" text="Available" /> | – |
|
| INTRATE | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| IPMT | <Badge type="tip" text="Available" /> | – |
|
| IPMT | <Badge type="tip" text="Available" /> | – |
|
||||||
| IRR | <Badge type="tip" text="Available" /> | – |
|
| IRR | <Badge type="tip" text="Available" /> | – |
|
||||||
| ISPMT | <Badge type="tip" text="Available" /> | – |
|
| ISPMT | <Badge type="tip" text="Available" /> | – |
|
||||||
| MDURATION | <Badge type="tip" text="Available" /> | – |
|
| MDURATION | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| MIRR | <Badge type="tip" text="Available" /> | – |
|
| MIRR | <Badge type="tip" text="Available" /> | – |
|
||||||
| NOMINAL | <Badge type="tip" text="Available" /> | – |
|
| NOMINAL | <Badge type="tip" text="Available" /> | – |
|
||||||
| NPER | <Badge type="tip" text="Available" /> | – |
|
| NPER | <Badge type="tip" text="Available" /> | – |
|
||||||
@@ -48,12 +48,12 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
|
|||||||
| PDURATION | <Badge type="tip" text="Available" /> | – |
|
| PDURATION | <Badge type="tip" text="Available" /> | – |
|
||||||
| PMT | <Badge type="tip" text="Available" /> | – |
|
| PMT | <Badge type="tip" text="Available" /> | – |
|
||||||
| PPMT | <Badge type="tip" text="Available" /> | – |
|
| PPMT | <Badge type="tip" text="Available" /> | – |
|
||||||
| PRICE | <Badge type="tip" text="Available" /> | – |
|
| PRICE | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| PRICEDISC | <Badge type="tip" text="Available" /> | – |
|
| PRICEDISC | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| PRICEMAT | <Badge type="tip" text="Available" /> | – |
|
| PRICEMAT | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| PV | <Badge type="tip" text="Available" /> | [PV](financial/pv) |
|
| PV | <Badge type="tip" text="Available" /> | [PV](financial/pv) |
|
||||||
| RATE | <Badge type="tip" text="Available" /> | – |
|
| RATE | <Badge type="tip" text="Available" /> | – |
|
||||||
| RECEIVED | <Badge type="tip" text="Available" /> | – |
|
| RECEIVED | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| RRI | <Badge type="tip" text="Available" /> | - |
|
| RRI | <Badge type="tip" text="Available" /> | - |
|
||||||
| SLN | <Badge type="tip" text="Available" /> | – |
|
| SLN | <Badge type="tip" text="Available" /> | – |
|
||||||
| SYD | <Badge type="tip" text="Available" /> | – |
|
| SYD | <Badge type="tip" text="Available" /> | – |
|
||||||
@@ -63,9 +63,6 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
|
|||||||
| VDB | <Badge type="info" text="Not implemented yet" /> | – |
|
| VDB | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| XIRR | <Badge type="tip" text="Available" /> | – |
|
| XIRR | <Badge type="tip" text="Available" /> | – |
|
||||||
| XNPV | <Badge type="tip" text="Available" /> | – |
|
| XNPV | <Badge type="tip" text="Available" /> | – |
|
||||||
| YIELD | <Badge type="tip" text="Available" /> | – |
|
| YIELD | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| YIELDDISC | <Badge type="info" text="Not implemented yet" /> | – |
|
| YIELDDISC | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| YIELDMAT | <Badge type="info" text="Not implemented yet" /> | – |
|
| YIELDMAT | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| YIELD | <Badge type="tip" text="Available" /> | – |
|
|
||||||
| YIELDDISC | <Badge type="tip" text="Available" /> | – |
|
|
||||||
| YIELDMAT | <Badge type="tip" text="Available" /> | – |
|
|
||||||
|
|||||||
@@ -7,5 +7,6 @@ lang: en-US
|
|||||||
# ACCRINT
|
# ACCRINT
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
🚧 This function is not yet available in IronCalc.
|
||||||
|
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
||||||
:::
|
:::
|
||||||
@@ -7,5 +7,6 @@ lang: en-US
|
|||||||
# ACCRINTM
|
# ACCRINTM
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
🚧 This function is not yet available in IronCalc.
|
||||||
|
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
||||||
:::
|
:::
|
||||||
@@ -7,5 +7,6 @@ lang: en-US
|
|||||||
# COUPDAYBS
|
# COUPDAYBS
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
🚧 This function is not yet available in IronCalc.
|
||||||
|
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
||||||
:::
|
:::
|
||||||
@@ -7,5 +7,6 @@ lang: en-US
|
|||||||
# COUPDAYS
|
# COUPDAYS
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
🚧 This function is not yet available in IronCalc.
|
||||||
|
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
||||||
:::
|
:::
|
||||||
@@ -7,5 +7,6 @@ lang: en-US
|
|||||||
# COUPDAYSNC
|
# COUPDAYSNC
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
🚧 This function is not yet available in IronCalc.
|
||||||
|
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
||||||
:::
|
:::
|
||||||
@@ -7,5 +7,6 @@ lang: en-US
|
|||||||
# COUPNCD
|
# COUPNCD
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
🚧 This function is not yet available in IronCalc.
|
||||||
|
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
||||||
:::
|
:::
|
||||||
@@ -7,5 +7,6 @@ lang: en-US
|
|||||||
# COUPNUM
|
# COUPNUM
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
🚧 This function is not yet available in IronCalc.
|
||||||
|
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
||||||
:::
|
:::
|
||||||
@@ -7,5 +7,6 @@ lang: en-US
|
|||||||
# COUPPCD
|
# COUPPCD
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
🚧 This function is not yet available in IronCalc.
|
||||||
|
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
||||||
:::
|
:::
|
||||||
@@ -7,5 +7,6 @@ lang: en-US
|
|||||||
# DISC
|
# DISC
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
🚧 This function is not yet available in IronCalc.
|
||||||
|
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
||||||
:::
|
:::
|
||||||
@@ -7,5 +7,6 @@ lang: en-US
|
|||||||
# DURATION
|
# DURATION
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
🚧 This function is not yet available in IronCalc.
|
||||||
|
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
||||||
:::
|
:::
|
||||||
@@ -7,5 +7,6 @@ lang: en-US
|
|||||||
# FVSCHEDULE
|
# FVSCHEDULE
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
🚧 This function is not yet available in IronCalc.
|
||||||
|
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
||||||
:::
|
:::
|
||||||
@@ -7,5 +7,6 @@ lang: en-US
|
|||||||
# INTRATE
|
# INTRATE
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
🚧 This function is not yet available in IronCalc.
|
||||||
|
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
||||||
:::
|
:::
|
||||||
@@ -7,5 +7,6 @@ lang: en-US
|
|||||||
# MDURATION
|
# MDURATION
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
🚧 This function is not yet available in IronCalc.
|
||||||
|
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
||||||
:::
|
:::
|
||||||
@@ -7,5 +7,6 @@ lang: en-US
|
|||||||
# PRICE
|
# PRICE
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
🚧 This function is not yet available in IronCalc.
|
||||||
|
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
||||||
:::
|
:::
|
||||||
@@ -7,5 +7,6 @@ lang: en-US
|
|||||||
# PRICEDISC
|
# PRICEDISC
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
🚧 This function is not yet available in IronCalc.
|
||||||
|
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
||||||
:::
|
:::
|
||||||
@@ -7,5 +7,6 @@ lang: en-US
|
|||||||
# PRICEMAT
|
# PRICEMAT
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
🚧 This function is not yet available in IronCalc.
|
||||||
|
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
||||||
:::
|
:::
|
||||||
@@ -7,5 +7,6 @@ lang: en-US
|
|||||||
# RECEIVED
|
# RECEIVED
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
🚧 This function is not yet available in IronCalc.
|
||||||
|
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
||||||
:::
|
:::
|
||||||
@@ -7,5 +7,6 @@ lang: en-US
|
|||||||
# YIELD
|
# YIELD
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
🚧 This function is not yet available in IronCalc.
|
||||||
|
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
||||||
:::
|
:::
|
||||||
@@ -7,5 +7,6 @@ lang: en-US
|
|||||||
# YIELDDISC
|
# YIELDDISC
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
🚧 This function is not yet available in IronCalc.
|
||||||
|
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
||||||
:::
|
:::
|
||||||
@@ -7,5 +7,6 @@ lang: en-US
|
|||||||
# YIELDMAT
|
# YIELDMAT
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
🚧 This function is not yet available in IronCalc.
|
||||||
|
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
||||||
:::
|
:::
|
||||||
@@ -14,10 +14,10 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
|
|||||||
| ABS | <Badge type="tip" text="Available" /> | – |
|
| ABS | <Badge type="tip" text="Available" /> | – |
|
||||||
| ACOS | <Badge type="tip" text="Available" /> | [ACOS](math_and_trigonometry/acos) |
|
| ACOS | <Badge type="tip" text="Available" /> | [ACOS](math_and_trigonometry/acos) |
|
||||||
| ACOSH | <Badge type="tip" text="Available" /> | [ACOSH](math_and_trigonometry/acosh) |
|
| ACOSH | <Badge type="tip" text="Available" /> | [ACOSH](math_and_trigonometry/acosh) |
|
||||||
| ACOT | <Badge type="info" text="Not implemented yet" /> | – |
|
| ACOT | <Badge type="tip" text="Available" /> | – |
|
||||||
| ACOTH | <Badge type="info" text="Not implemented yet" /> | – |
|
| ACOTH | <Badge type="tip" text="Available" /> | – |
|
||||||
| AGGREGATE | <Badge type="info" text="Not implemented yet" /> | – |
|
| AGGREGATE | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| ARABIC | <Badge type="info" text="Not implemented yet" /> | – |
|
| ARABIC | <Badge type="tip" text="Available" /> | – |
|
||||||
| ASIN | <Badge type="tip" text="Available" /> | [ASIN](math_and_trigonometry/asin) |
|
| ASIN | <Badge type="tip" text="Available" /> | [ASIN](math_and_trigonometry/asin) |
|
||||||
| ASINH | <Badge type="tip" text="Available" /> | [ASINH](math_and_trigonometry/asinh) |
|
| ASINH | <Badge type="tip" text="Available" /> | [ASINH](math_and_trigonometry/asinh) |
|
||||||
| ATAN | <Badge type="tip" text="Available" /> | [ATAN](math_and_trigonometry/atan) |
|
| ATAN | <Badge type="tip" text="Available" /> | [ATAN](math_and_trigonometry/atan) |
|
||||||
@@ -27,64 +27,64 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
|
|||||||
| CEILING | <Badge type="info" text="Not implemented yet" /> | – |
|
| CEILING | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| CEILING.MATH | <Badge type="info" text="Not implemented yet" /> | – |
|
| CEILING.MATH | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| CEILING.PRECISE | <Badge type="info" text="Not implemented yet" /> | – |
|
| CEILING.PRECISE | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| COMBIN | <Badge type="info" text="Not implemented yet" /> | – |
|
| COMBIN | <Badge type="tip" text="Available" /> | – |
|
||||||
| COMBINA | <Badge type="info" text="Not implemented yet" /> | – |
|
| COMBINA | <Badge type="tip" text="Available" /> | – |
|
||||||
| COS | <Badge type="tip" text="Available" /> | [COS](math_and_trigonometry/cos) |
|
| COS | <Badge type="tip" text="Available" /> | [COS](math_and_trigonometry/cos) |
|
||||||
| COSH | <Badge type="tip" text="Available" /> | [COSH](math_and_trigonometry/cosh) |
|
| COSH | <Badge type="tip" text="Available" /> | [COSH](math_and_trigonometry/cosh) |
|
||||||
| COT | <Badge type="info" text="Not implemented yet" /> | – |
|
| COT | <Badge type="tip" text="Available" /> | – |
|
||||||
| COTH | <Badge type="info" text="Not implemented yet" /> | – |
|
| COTH | <Badge type="tip" text="Available" /> | – |
|
||||||
| CSC | <Badge type="info" text="Not implemented yet" /> | – |
|
| CSC | <Badge type="tip" text="Available" /> | – |
|
||||||
| CSCH | <Badge type="info" text="Not implemented yet" /> | – |
|
| CSCH | <Badge type="tip" text="Available" /> | – |
|
||||||
| DECIMAL | <Badge type="info" text="Not implemented yet" /> | – |
|
| DECIMAL | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| DEGREES | <Badge type="info" text="Not implemented yet" /> | – |
|
| DEGREES | <Badge type="tip" text="Available" /> | [DEGREES](math_and_trigonometry/degrees) |
|
||||||
| EVEN | <Badge type="info" text="Not implemented yet" /> | – |
|
| EVEN | <Badge type="tip" text="Available" /> | [EVEN](math_and_trigonometry/even) |
|
||||||
| EXP | <Badge type="info" text="Not implemented yet" /> | – |
|
| EXP | <Badge type="tip" text="Available" /> | – |
|
||||||
| FACT | <Badge type="info" text="Not implemented yet" /> | – |
|
| FACT | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| FACTDOUBLE | <Badge type="info" text="Not implemented yet" /> | – |
|
| FACTDOUBLE | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| FLOOR | <Badge type="info" text="Not implemented yet" /> | – |
|
| FLOOR | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| FLOOR.MATH | <Badge type="info" text="Not implemented yet" /> | – |
|
| FLOOR.MATH | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| FLOOR.PRECISE | <Badge type="info" text="Not implemented yet" /> | – |
|
| FLOOR.PRECISE | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| GCD | <Badge type="info" text="Not implemented yet" /> | – |
|
| GCD | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| INT | <Badge type="info" text="Not implemented yet" /> | – |
|
| INT | <Badge type="tip" text="Available" /> | – |
|
||||||
| ISO.CEILING | <Badge type="info" text="Not implemented yet" /> | – |
|
| ISO.CEILING | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| LCM | <Badge type="info" text="Not implemented yet" /> | – |
|
| LCM | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| LET | <Badge type="info" text="Not implemented yet" /> | – |
|
| LET | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| LN | <Badge type="info" text="Available" /> | – |
|
| LN | <Badge type="tip" text="Available" /> | – |
|
||||||
| LOG | <Badge type="info" text="Available" /> | – |
|
| LOG | <Badge type="tip" text="Available" /> | – |
|
||||||
| LOG10 | <Badge type="info" text="Available" /> | – |
|
| LOG10 | <Badge type="tip" text="Available" /> | – |
|
||||||
| MDETERM | <Badge type="info" text="Not implemented yet" /> | – |
|
| MDETERM | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| MINVERSE | <Badge type="info" text="Not implemented yet" /> | – |
|
| MINVERSE | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| MMULT | <Badge type="info" text="Not implemented yet" /> | – |
|
| MMULT | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| MOD | <Badge type="info" text="Not implemented yet" /> | – |
|
| MOD | <Badge type="tip" text="Available" /> | [MOD](math_and_trigonometry/mod) |
|
||||||
| MROUND | <Badge type="info" text="Not implemented yet" /> | – |
|
| MROUND | <Badge type="tip" text="Available" /> | – |
|
||||||
| MULTINOMIAL | <Badge type="info" text="Not implemented yet" /> | – |
|
| MULTINOMIAL | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| MUNIT | <Badge type="info" text="Not implemented yet" /> | – |
|
| MUNIT | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| ODD | <Badge type="info" text="Not implemented yet" /> | – |
|
| ODD | <Badge type="tip" text="Available" /> | [ODD](math_and_trigonometry/odd) |
|
||||||
| PI | <Badge type="info" text="Not implemented yet" /> | – |
|
| PI | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| POWER | <Badge type="tip" text="Available" /> | – |
|
| POWER | <Badge type="tip" text="Available" /> | – |
|
||||||
| PRODUCT | <Badge type="tip" text="Available" /> | – |
|
| PRODUCT | <Badge type="tip" text="Available" /> | – |
|
||||||
| QUOTIENT | <Badge type="info" text="Not implemented yet" /> | – |
|
| QUOTIENT | <Badge type="tip" text="Available" /> | [QUOTIENT](math_and_trigonometry/quotient) |
|
||||||
| RADIANS | <Badge type="info" text="Not implemented yet" /> | – |
|
| RADIANS | <Badge type="tip" text="Available" /> | [RADIANS](math_and_trigonometry/radians) |
|
||||||
| RAND | <Badge type="tip" text="Available" /> | – |
|
| RAND | <Badge type="tip" text="Available" /> | – |
|
||||||
| RANDARRAY | <Badge type="info" text="Not implemented yet" /> | – |
|
| RANDARRAY | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| RANDBETWEEN | <Badge type="tip" text="Available" /> | – |
|
| RANDBETWEEN | <Badge type="tip" text="Available" /> | – |
|
||||||
| ROMAN | <Badge type="info" text="Not implemented yet" /> | – |
|
| ROMAN | <Badge type="tip" text="Available" /> | – |
|
||||||
| ROUND | <Badge type="tip" text="Available" /> | – |
|
| ROUND | <Badge type="tip" text="Available" /> | – |
|
||||||
| ROUNDDOWN | <Badge type="tip" text="Available" /> | – |
|
| ROUNDDOWN | <Badge type="tip" text="Available" /> | – |
|
||||||
| ROUNDUP | <Badge type="tip" text="Available" /> | – |
|
| ROUNDUP | <Badge type="tip" text="Available" /> | – |
|
||||||
| SEC | <Badge type="info" text="Not implemented yet" /> | – |
|
| SEC | <Badge type="tip" text="Available" /> | – |
|
||||||
| SECH | <Badge type="info" text="Not implemented yet" /> | – |
|
| SECH | <Badge type="tip" text="Available" /> | – |
|
||||||
| SERIESSUM | <Badge type="info" text="Not implemented yet" /> | – |
|
| SERIESSUM | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| SEQUENCE | <Badge type="info" text="Not implemented yet" /> | – |
|
| SEQUENCE | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| SIGN | <Badge type="info" text="Not implemented yet" /> | – |
|
| SIGN | <Badge type="tip" text="Available" /> | – |
|
||||||
| SIN | <Badge type="tip" text="Available" /> | [SIN](math_and_trigonometry/sin) |
|
| SIN | <Badge type="tip" text="Available" /> | [SIN](math_and_trigonometry/sin) |
|
||||||
| SINH | <Badge type="tip" text="Available" /> | [SINH](math_and_trigonometry/sinh) |
|
| SINH | <Badge type="tip" text="Available" /> | [SINH](math_and_trigonometry/sinh) |
|
||||||
| SQRT | <Badge type="tip" text="Available" /> | – |
|
| SQRT | <Badge type="tip" text="Available" /> | – |
|
||||||
| SQRTPI | <Badge type="info" text="Available" /> | – |
|
| SQRTPI | <Badge type="tip" text="Available" /> | – |
|
||||||
| SUBTOTAL | <Badge type="info" text="Not implemented yet" /> | – |
|
| SUBTOTAL | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| SUM | <Badge type="tip" text="Available" /> | – |
|
| SUM | <Badge type="tip" text="Available" /> | – |
|
||||||
| SUMIF | <Badge type="tip" text="Available" /> | – |
|
| SUMIF | <Badge type="tip" text="Available" /> | – |
|
||||||
| SUMIFS | <Badge type="info" text="Available" /> | – |
|
| SUMIFS | <Badge type="tip" text="Available" /> | – |
|
||||||
| SUMPRODUCT | <Badge type="info" text="Not implemented yet" /> | – |
|
| SUMPRODUCT | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| SUMSQ | <Badge type="info" text="Not implemented yet" /> | – |
|
| SUMSQ | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| SUMX2MY2 | <Badge type="info" text="Not implemented yet" /> | – |
|
| SUMX2MY2 | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
@@ -92,4 +92,4 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
|
|||||||
| SUMXMY2 | <Badge type="info" text="Not implemented yet" /> | – |
|
| SUMXMY2 | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| TAN | <Badge type="tip" text="Available" /> | [TAN](math_and_trigonometry/tan) |
|
| TAN | <Badge type="tip" text="Available" /> | [TAN](math_and_trigonometry/tan) |
|
||||||
| TANH | <Badge type="tip" text="Available" /> | [TANH](math_and_trigonometry/tanh) |
|
| TANH | <Badge type="tip" text="Available" /> | [TANH](math_and_trigonometry/tanh) |
|
||||||
| TRUNC | <Badge type="info" text="Not implemented yet" /> | – |
|
| TRUNC | <Badge type="tip" text="Available" /> | – |
|
||||||
|
|||||||
@@ -7,6 +7,5 @@ lang: en-US
|
|||||||
# ACOT
|
# ACOT
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
🚧 This function is not yet available in IronCalc.
|
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
||||||
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
|
||||||
:::
|
:::
|
||||||
@@ -7,6 +7,5 @@ lang: en-US
|
|||||||
# ACOTH
|
# ACOTH
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
🚧 This function is not yet available in IronCalc.
|
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
||||||
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
|
||||||
:::
|
:::
|
||||||
@@ -7,6 +7,5 @@ lang: en-US
|
|||||||
# ARABIC
|
# ARABIC
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
🚧 This function is not yet available in IronCalc.
|
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
||||||
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
|
||||||
:::
|
:::
|
||||||
@@ -7,6 +7,5 @@ lang: en-US
|
|||||||
# COMBIN
|
# COMBIN
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
🚧 This function is not yet available in IronCalc.
|
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
||||||
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
|
||||||
:::
|
:::
|
||||||
@@ -7,6 +7,5 @@ lang: en-US
|
|||||||
# COMBINA
|
# COMBINA
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
🚧 This function is not yet available in IronCalc.
|
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
||||||
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
|
||||||
:::
|
:::
|
||||||
@@ -7,6 +7,5 @@ lang: en-US
|
|||||||
# COT
|
# COT
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
🚧 This function is not yet available in IronCalc.
|
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
||||||
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
|
||||||
:::
|
:::
|
||||||
@@ -7,6 +7,5 @@ lang: en-US
|
|||||||
# COTH
|
# COTH
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
🚧 This function is not yet available in IronCalc.
|
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
||||||
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
|
||||||
:::
|
:::
|
||||||
@@ -7,6 +7,5 @@ lang: en-US
|
|||||||
# CSC
|
# CSC
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
🚧 This function is not yet available in IronCalc.
|
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
||||||
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
|
||||||
:::
|
:::
|
||||||
@@ -7,6 +7,5 @@ lang: en-US
|
|||||||
# CSCH
|
# CSCH
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
🚧 This function is not yet available in IronCalc.
|
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
||||||
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
|
||||||
:::
|
:::
|
||||||
@@ -4,9 +4,40 @@ outline: deep
|
|||||||
lang: en-US
|
lang: en-US
|
||||||
---
|
---
|
||||||
|
|
||||||
# DEGREES
|
# DEGREES function
|
||||||
|
|
||||||
::: warning
|
## Overview
|
||||||
🚧 This function is not yet available in IronCalc.
|
DEGREES is a function of the Math and Trigonometry category that converts an angle measured in radians to an equivalent angle measured in degrees.
|
||||||
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
|
||||||
:::
|
## Usage
|
||||||
|
### Syntax
|
||||||
|
**DEGREES(<span title="Number" style="color:#1E88E5">angle</span>) => <span title="Number" style="color:#1E88E5">degrees</span>**
|
||||||
|
|
||||||
|
### Argument descriptions
|
||||||
|
* *angle* ([number](/features/value-types#numbers), required). The angle in radians that is to be converted to degrees.
|
||||||
|
|
||||||
|
### Additional guidance
|
||||||
|
The conversion from radians to degrees is based on the relationship:
|
||||||
|
$$
|
||||||
|
1~\:~\text{radian} = \dfrac{180}{\pi}~\text{degrees} \approx 57.29577951~\text{degrees}
|
||||||
|
$$
|
||||||
|
|
||||||
|
### Returned value
|
||||||
|
DEGREES returns a [number](/features/value-types#numbers) that represents the value of the given angle expressed in degrees.
|
||||||
|
|
||||||
|
### Error conditions
|
||||||
|
* In common with many other IronCalc functions, DEGREES propagates errors that are found in its argument.
|
||||||
|
* If no argument, or more than one argument, is supplied, then DEGREES returns the [`#ERROR!`](/features/error-types.md#error) error.
|
||||||
|
* If the value of the *angle* argument is not (or cannot be converted to) a [number](/features/value-types#numbers), then DEGREES returns the [`#VALUE!`](/features/error-types.md#value) error.
|
||||||
|
<!--@include: ../markdown-snippets/error-type-details.txt-->
|
||||||
|
|
||||||
|
<!--
|
||||||
|
## Examples
|
||||||
|
[See some examples in IronCalc](https://app.ironcalc.com/?example=degrees).
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Links
|
||||||
|
* For more information about angle conversions, visit Wikipedia's [Degree (angle)](https://en.wikipedia.org/wiki/Degree_(angle)) page.
|
||||||
|
* See also IronCalc's [RADIANS](/functions/math_and_trigonometry/radians) function for converting degrees to radians.
|
||||||
|
* Visit Microsoft Excel's [DEGREES function](https://support.microsoft.com/en-us/office/degrees-function-4d6ec4db-e694-4b94-ace0-1cc3f61f9ba1) page.
|
||||||
|
* Both [Google Sheets](https://support.google.com/docs/answer/3093481) and [LibreOffice Calc](https://wiki.documentfoundation.org/Documentation/Calc_Functions/DEGREES) provide versions of the DEGREES function.
|
||||||
@@ -4,9 +4,41 @@ outline: deep
|
|||||||
lang: en-US
|
lang: en-US
|
||||||
---
|
---
|
||||||
|
|
||||||
# EVEN
|
# EVEN function
|
||||||
|
|
||||||
::: warning
|
## Overview
|
||||||
🚧 This function is not yet available in IronCalc.
|
EVEN is a function of the Math and Trigonometry category that rounds a number up (away from zero) to the nearest even integer.
|
||||||
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
|
||||||
:::
|
## Usage
|
||||||
|
### Syntax
|
||||||
|
**EVEN(<span title="Number" style="color:#1E88E5">number</span>) => <span title="Number" style="color:#1E88E5">even</span>**
|
||||||
|
|
||||||
|
### Argument descriptions
|
||||||
|
* *number* ([number](/features/value-types#numbers), required). The number that is to be rounded to the nearest even integer.
|
||||||
|
|
||||||
|
### Additional guidance
|
||||||
|
* EVEN rounds away from zero, meaning:
|
||||||
|
* Positive numbers are rounded up to the next even integer.
|
||||||
|
* Negative numbers are rounded down (toward negative infinity) to the next even integer.
|
||||||
|
* If the *number* argument is already an even integer, EVEN returns it unchanged.
|
||||||
|
* Since zero is considered an even number, the EVEN function returns 0 when *number* is 0.
|
||||||
|
|
||||||
|
### Returned value
|
||||||
|
EVEN returns a [number](/features/value-types#numbers) that is the nearest even integer, rounded away from zero.
|
||||||
|
|
||||||
|
### Error conditions
|
||||||
|
* In common with many other IronCalc functions, EVEN propagates errors that are found in its argument.
|
||||||
|
* If no argument, or more than one argument, is supplied, then EVEN returns the [`#ERROR!`](/features/error-types.md#error) error.
|
||||||
|
* If the value of the *number* argument is not (or cannot be converted to) a [number](/features/value-types#numbers), then EVEN returns the [`#VALUE!`](/features/error-types.md#value) error.
|
||||||
|
<!--@include: ../markdown-snippets/error-type-details.txt-->
|
||||||
|
|
||||||
|
<!--
|
||||||
|
## Examples
|
||||||
|
[See some examples in IronCalc](https://app.ironcalc.com/?example=even).
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Links
|
||||||
|
* For more information about even and odd numbers, visit Wikipedia's [Parity](https://en.wikipedia.org/wiki/Parity_(mathematics)) page.
|
||||||
|
* See also IronCalc's [ODD](/functions/math_and_trigonometry/odd) function.
|
||||||
|
* Visit Microsoft Excel's [EVEN function](https://support.microsoft.com/en-us/office/even-function-197b5f06-c795-4c1e-8696-3c3b8a646cf9) page.
|
||||||
|
* Both [Google Sheets](https://support.google.com/docs/answer/3093409) and [LibreOffice Calc](https://wiki.documentfoundation.org/Documentation/Calc_Functions/EVEN) provide versions of the EVEN function.
|
||||||
@@ -7,6 +7,5 @@ lang: en-US
|
|||||||
# EXP
|
# EXP
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
🚧 This function is not yet available in IronCalc.
|
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
||||||
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
|
||||||
:::
|
:::
|
||||||
@@ -7,6 +7,5 @@ lang: en-US
|
|||||||
# INT
|
# INT
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
🚧 This function is not yet available in IronCalc.
|
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
||||||
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
|
||||||
:::
|
:::
|
||||||
@@ -4,9 +4,44 @@ outline: deep
|
|||||||
lang: en-US
|
lang: en-US
|
||||||
---
|
---
|
||||||
|
|
||||||
# MOD
|
# MOD function
|
||||||
|
|
||||||
::: warning
|
## Overview
|
||||||
🚧 This function is not yet available in IronCalc.
|
MOD is a function of the Math and Trigonometry category that returns the remainder after one number (the dividend) is divided by another number (the divisor). The result has the same sign as the divisor.
|
||||||
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
|
||||||
:::
|
## Usage
|
||||||
|
### Syntax
|
||||||
|
**MOD(<span title="Number" style="color:#1E88E5">dividend</span>, <span title="Number" style="color:#1E88E5">divisor</span>) => <span title="Number" style="color:#1E88E5">remainder</span>**
|
||||||
|
|
||||||
|
### Argument descriptions
|
||||||
|
* *dividend* ([number](/features/value-types#numbers), required). The number whose remainder is to be calculated.
|
||||||
|
* *divisor* ([number](/features/value-types#numbers), required). The number by which the dividend is divided.
|
||||||
|
|
||||||
|
### Additional guidance
|
||||||
|
None.
|
||||||
|
|
||||||
|
### Returned value
|
||||||
|
MOD returns a [number](/features/value-types#numbers) that is the remainder after division, with the same sign as the divisor.
|
||||||
|
|
||||||
|
### Error conditions
|
||||||
|
* In common with many other IronCalc functions, MOD propagates errors that are found in its arguments.
|
||||||
|
* If no argument, or more than two arguments, are supplied, then MOD returns the [`#ERROR!`](/features/error-types.md#error) error.
|
||||||
|
* If either argument is not (or cannot be converted to) a [number](/features/value-types#numbers), then MOD returns the [`#VALUE!`](/features/error-types.md#value) error.
|
||||||
|
* If the value of the *divisor* argument is 0, then MOD returns the [`#DIV/0!`](/features/error-types.md#div-0) error.
|
||||||
|
<!--@include: ../markdown-snippets/error-type-details.txt-->
|
||||||
|
|
||||||
|
## Details
|
||||||
|
* MOD follows the formula:
|
||||||
|
$$
|
||||||
|
\operatorname{MOD}(n, d) = n - d \times \operatorname{INT}\!\left(\dfrac{n}{d}\right)
|
||||||
|
$$
|
||||||
|
Since `INT` returns the greatest integer less than or equal to its argument (it rounds down), the remainder's sign matches the divisor, even though this might appear counterintuitive when the dividend and divisor have different signs.
|
||||||
|
<!--
|
||||||
|
## Examples
|
||||||
|
[See some examples in IronCalc](https://app.ironcalc.com/?example=mod).
|
||||||
|
-->
|
||||||
|
## Links
|
||||||
|
* For more information about the modulo operation, visit Wikipedia's [Modulo](https://en.wikipedia.org/wiki/Modulo) page.
|
||||||
|
* See also IronCalc's [QUOTIENT](/functions/math_and_trigonometry/quotient) function.
|
||||||
|
* Visit Microsoft Excel's [MOD function](https://support.microsoft.com/en-us/office/mod-function-9b6cd169-b6ee-406a-a97b-edf2a9dc24f3) page.
|
||||||
|
* Both [Google Sheets](https://support.google.com/docs/answer/3093497) and [LibreOffice Calc](https://wiki.documentfoundation.org/Documentation/Calc_Functions/MOD) provide versions of the MOD function.
|
||||||
@@ -7,6 +7,5 @@ lang: en-US
|
|||||||
# MROUND
|
# MROUND
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
🚧 This function is not yet available in IronCalc.
|
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
||||||
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
|
||||||
:::
|
:::
|
||||||
@@ -4,9 +4,41 @@ outline: deep
|
|||||||
lang: en-US
|
lang: en-US
|
||||||
---
|
---
|
||||||
|
|
||||||
# ODD
|
# ODD function
|
||||||
|
|
||||||
::: warning
|
## Overview
|
||||||
🚧 This function is not yet available in IronCalc.
|
ODD is a function of the Math and Trigonometry category that rounds a number up (away from zero) to the nearest odd integer.
|
||||||
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
|
||||||
:::
|
## Usage
|
||||||
|
### Syntax
|
||||||
|
**ODD(<span title="Number" style="color:#1E88E5">number</span>) => <span title="Number" style="color:#1E88E5">odd</span>**
|
||||||
|
|
||||||
|
### Argument descriptions
|
||||||
|
* *number* ([number](/features/value-types#numbers), required). The number that is to be rounded to the nearest odd integer.
|
||||||
|
|
||||||
|
### Additional guidance
|
||||||
|
* ODD rounds away from zero, meaning:
|
||||||
|
* Positive numbers are rounded up to the next odd integer.
|
||||||
|
* Negative numbers are rounded down (toward negative infinity) to the next odd integer.
|
||||||
|
* If the *number* argument is already an odd integer, ODD returns it unchanged.
|
||||||
|
* Since zero is considered an even number, the ODD function returns 1 when *number* is 0.
|
||||||
|
|
||||||
|
### Returned value
|
||||||
|
ODD returns a [number](/features/value-types#numbers) that is the nearest odd integer, rounded away from zero.
|
||||||
|
|
||||||
|
### Error conditions
|
||||||
|
* In common with many other IronCalc functions, ODD propagates errors that are found in its argument.
|
||||||
|
* If no argument, or more than one argument, is supplied, then ODD returns the [`#ERROR!`](/features/error-types.md#error) error.
|
||||||
|
* If the value of the *number* argument is not (or cannot be converted to) a [number](/features/value-types#numbers), then ODD returns the [`#VALUE!`](/features/error-types.md#value) error.
|
||||||
|
<!--@include: ../markdown-snippets/error-type-details.txt-->
|
||||||
|
|
||||||
|
<!--
|
||||||
|
## Examples
|
||||||
|
[See some examples in IronCalc](https://app.ironcalc.com/?example=odd).
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Links
|
||||||
|
* For more information about even and odd numbers, visit Wikipedia's [Parity](https://en.wikipedia.org/wiki/Parity_(mathematics)) page.
|
||||||
|
* See also IronCalc's [EVEN](/functions/math_and_trigonometry/even) function.
|
||||||
|
* Visit Microsoft Excel's [ODD function](https://support.microsoft.com/en-us/office/odd-function-deae64eb-e08a-4c88-8b40-6d0b42575c98) page.
|
||||||
|
* Both [Google Sheets](https://support.google.com/docs/answer/3093499) and [LibreOffice Calc](https://wiki.documentfoundation.org/Documentation/Calc_Functions/ODD) provide versions of the ODD function.
|
||||||
@@ -4,9 +4,44 @@ outline: deep
|
|||||||
lang: en-US
|
lang: en-US
|
||||||
---
|
---
|
||||||
|
|
||||||
# QUOTIENT
|
# QUOTIENT function
|
||||||
|
|
||||||
::: warning
|
## Overview
|
||||||
🚧 This function is not yet available in IronCalc.
|
QUOTIENT is a function of the Math and Trigonometry category that returns the integer portion of a division. It divides one number (dividend) by another (divisor) and discards the remainder by truncating toward zero.
|
||||||
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
|
||||||
:::
|
## Usage
|
||||||
|
### Syntax
|
||||||
|
**QUOTIENT(<span title="Number" style="color:#1E88E5">dividend</span>, <span title="Number" style="color:#1E88E5">divisor</span>) => <span title="Number" style="color:#1E88E5">quotient</span>**
|
||||||
|
|
||||||
|
### Argument descriptions
|
||||||
|
* *dividend* ([number](/features/value-types#numbers), required). The number to be divided.
|
||||||
|
* *divisor* ([number](/features/value-types#numbers), required). The number by which to divide the dividend.
|
||||||
|
|
||||||
|
### Additional guidance
|
||||||
|
* QUOTIENT returns the integer part of the division and ignores any remainder. For negative results, it truncates toward zero.
|
||||||
|
|
||||||
|
### Returned value
|
||||||
|
QUOTIENT returns a [number](/features/value-types#numbers) that is the integer portion of the division of the dividend by the divisor.
|
||||||
|
|
||||||
|
### Error conditions
|
||||||
|
* In common with many other IronCalc functions, QUOTIENT propagates errors that are found in its arguments.
|
||||||
|
* If no argument, or more than two arguments, are supplied, then QUOTIENT returns the [`#ERROR!`](/features/error-types.md#error) error.
|
||||||
|
* If either argument is not (or cannot be converted to) a [number](/features/value-types#numbers), then QUOTIENT returns the [`#VALUE!`](/features/error-types.md#value) error.
|
||||||
|
* If the value of the *divisor* argument is 0, then QUOTIENT returns the [`#DIV/0!`](/features/error-types.md#div-0) error.
|
||||||
|
<!--@include: ../markdown-snippets/error-type-details.txt-->
|
||||||
|
|
||||||
|
## Details
|
||||||
|
* QUOTIENT corresponds to truncating the exact quotient toward zero:
|
||||||
|
$$
|
||||||
|
\operatorname{QUOTIENT}(n, d) = \operatorname{TRUNC}\!\left(\dfrac{n}{d}\right),\quad d \ne 0
|
||||||
|
$$
|
||||||
|
This differs from using `INT(n/d)` when the quotient is negative, because `INT` rounds down toward −∞, whereas `TRUNC` and QUOTIENT truncate toward zero.
|
||||||
|
<!--
|
||||||
|
## Examples
|
||||||
|
[See some examples in IronCalc](https://app.ironcalc.com/?example=quotient).
|
||||||
|
-->
|
||||||
|
## Links
|
||||||
|
* For more information about the quotient, visit Wikipedia's [Quotient](https://en.wikipedia.org/wiki/Quotient) page.
|
||||||
|
* See also IronCalc's [MOD](/functions/math_and_trigonometry/mod) function.
|
||||||
|
* Visit Microsoft Excel's [QUOTIENT function](https://support.microsoft.com/en-gb/office/quotient-function-9f7bf099-2a18-4282-8fa4-65290cc99dee) page.
|
||||||
|
* Both [Google Sheets](https://support.google.com/docs/answer/3093436) and [LibreOffice Calc](https://wiki.documentfoundation.org/Documentation/Calc_Functions/QUOTIENT) provide versions of the QUOTIENT function.
|
||||||
@@ -4,9 +4,38 @@ outline: deep
|
|||||||
lang: en-US
|
lang: en-US
|
||||||
---
|
---
|
||||||
|
|
||||||
# RADIANS
|
# RADIANS function
|
||||||
|
|
||||||
::: warning
|
## Overview
|
||||||
🚧 This function is not yet available in IronCalc.
|
RADIANS is a function of the Math and Trigonometry category that converts an angle measured in degrees to an equivalent angle measured in radians.
|
||||||
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
|
||||||
:::
|
## Usage
|
||||||
|
### Syntax
|
||||||
|
**RADIANS(<span title="Number" style="color:#1E88E5">angle</span>) => <span title="Number" style="color:#1E88E5">radians</span>**
|
||||||
|
|
||||||
|
### Argument descriptions
|
||||||
|
* *angle* ([number](/features/value-types#numbers), required). The angle in degrees that is to be converted to radians.
|
||||||
|
|
||||||
|
### Additional guidance
|
||||||
|
The conversion from degrees to radians is based on the relationship:
|
||||||
|
$$
|
||||||
|
1~\:~\text{degree} = \dfrac{\pi}{180}~\text{radians} \approx 0.01745329252~\text{radians}
|
||||||
|
$$
|
||||||
|
|
||||||
|
### Returned value
|
||||||
|
RADIANS returns a [number](/features/value-types#numbers) that represents the value of the given angle expressed in radians.
|
||||||
|
|
||||||
|
### Error conditions
|
||||||
|
* In common with many other IronCalc functions, RADIANS propagates errors that are found in its argument.
|
||||||
|
* If no argument, or more than one argument, is supplied, then RADIANS returns the [`#ERROR!`](/features/error-types.md#error) error.
|
||||||
|
* If the value of the *angle* argument is not (or cannot be converted to) a [number](/features/value-types#numbers), then RADIANS returns the [`#VALUE!`](/features/error-types.md#value) error.
|
||||||
|
<!--@include: ../markdown-snippets/error-type-details.txt-->
|
||||||
|
<!--
|
||||||
|
## Examples
|
||||||
|
[See some examples in IronCalc](https://app.ironcalc.com/?example=radians).
|
||||||
|
-->
|
||||||
|
## Links
|
||||||
|
* For more information about angle conversions, visit Wikipedia's [Radian](https://en.wikipedia.org/wiki/Radian) page.
|
||||||
|
* See also IronCalc's [DEGREES](/functions/math_and_trigonometry/degrees) function for converting radians to degrees.
|
||||||
|
* Visit Microsoft Excel's [RADIANS function](https://support.microsoft.com/en-us/office/radians-function-907f0ede-ef2e-4f7b-911a-015e2f8ab878) page.
|
||||||
|
* Both [Google Sheets](https://support.google.com/docs/answer/3093481) and [LibreOffice Calc](https://wiki.documentfoundation.org/Documentation/Calc_Functions/RADIANS) provide versions of the RADIANS function.
|
||||||
@@ -7,6 +7,5 @@ lang: en-US
|
|||||||
# ROMAN
|
# ROMAN
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
🚧 This function is not yet available in IronCalc.
|
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
||||||
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
|
||||||
:::
|
:::
|
||||||
@@ -7,6 +7,5 @@ lang: en-US
|
|||||||
# SEC
|
# SEC
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
🚧 This function is not yet available in IronCalc.
|
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
||||||
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
|
||||||
:::
|
:::
|
||||||
@@ -7,6 +7,5 @@ lang: en-US
|
|||||||
# SECH
|
# SECH
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
🚧 This function is not yet available in IronCalc.
|
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
||||||
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
|
||||||
:::
|
:::
|
||||||
@@ -7,6 +7,5 @@ lang: en-US
|
|||||||
# SIGN
|
# SIGN
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
🚧 This function is not yet available in IronCalc.
|
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
||||||
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
|
||||||
:::
|
:::
|
||||||
@@ -7,6 +7,5 @@ lang: en-US
|
|||||||
# TRUNC
|
# TRUNC
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
🚧 This function is not yet available in IronCalc.
|
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
||||||
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
|
||||||
:::
|
:::
|
||||||
@@ -16,6 +16,9 @@ const config: StorybookConfig = {
|
|||||||
}
|
}
|
||||||
config.server.fs.allow = ["../.."];
|
config.server.fs.allow = ["../.."];
|
||||||
return config;
|
return config;
|
||||||
}
|
},
|
||||||
|
core: {
|
||||||
|
disableTelemetry: true,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
{
|
{
|
||||||
"organizeImports": { "enabled": true },
|
|
||||||
"linter": {
|
"linter": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"rules": {
|
"rules": {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user