merge fvschedule #56
# Conflicts: # base/src/functions/mod.rs # base/src/test/mod.rs
This commit is contained in:
@@ -550,6 +550,14 @@ 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> {
|
||||
if arg_count == 2 {
|
||||
vec![Signature::Vector; arg_count]
|
||||
@@ -752,6 +760,7 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
|
||||
Function::Dollarfr => 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::Fvschedule => args_signature_fvschedule(arg_count),
|
||||
Function::Ipmt => args_signature_scalars(arg_count, 4, 2),
|
||||
Function::Irr => args_signature_irr(arg_count),
|
||||
Function::Ispmt => args_signature_scalars(arg_count, 4, 0),
|
||||
@@ -1020,6 +1029,7 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
|
||||
Function::Dollarfr => not_implemented(args),
|
||||
Function::Effect => not_implemented(args),
|
||||
Function::Fv => not_implemented(args),
|
||||
Function::Fvschedule => not_implemented(args),
|
||||
Function::Ipmt => not_implemented(args),
|
||||
Function::Irr => not_implemented(args),
|
||||
Function::Ispmt => not_implemented(args),
|
||||
|
||||
@@ -641,6 +641,35 @@ impl Model {
|
||||
}
|
||||
}
|
||||
|
||||
// FVSCHEDULE(principal, schedule)
|
||||
pub(crate) fn fn_fvschedule(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 2 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let principal = match self.get_number(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let schedule = match self.get_array_of_numbers(&args[1], &cell) {
|
||||
Ok(s) => s,
|
||||
Err(err) => return err,
|
||||
};
|
||||
let mut result = principal;
|
||||
for rate in schedule {
|
||||
if rate <= -1.0 {
|
||||
return CalcResult::new_error(Error::NUM, cell, "Rate must be > -1".to_string());
|
||||
}
|
||||
result *= 1.0 + rate;
|
||||
}
|
||||
if result.is_infinite() {
|
||||
return CalcResult::new_error(Error::DIV, cell, "Division by 0".to_string());
|
||||
}
|
||||
if result.is_nan() {
|
||||
return CalcResult::new_error(Error::NUM, cell, "Invalid result".to_string());
|
||||
}
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
// IPMT(rate, per, nper, pv, [fv], [type])
|
||||
pub(crate) fn fn_ipmt(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
let arg_count = args.len();
|
||||
|
||||
@@ -225,6 +225,7 @@ pub enum Function {
|
||||
Dollarfr,
|
||||
Effect,
|
||||
Fv,
|
||||
Fvschedule,
|
||||
Ipmt,
|
||||
Irr,
|
||||
Ispmt,
|
||||
@@ -317,7 +318,7 @@ pub enum Function {
|
||||
}
|
||||
|
||||
impl Function {
|
||||
pub fn into_iter() -> IntoIter<Function, 260> {
|
||||
pub fn into_iter() -> IntoIter<Function, 261> {
|
||||
[
|
||||
Function::And,
|
||||
Function::False,
|
||||
@@ -483,6 +484,7 @@ impl Function {
|
||||
Function::Rate,
|
||||
Function::Nper,
|
||||
Function::Fv,
|
||||
Function::Fvschedule,
|
||||
Function::Ppmt,
|
||||
Function::Price,
|
||||
Function::Ipmt,
|
||||
@@ -826,6 +828,7 @@ impl Function {
|
||||
"RATE" => Some(Function::Rate),
|
||||
"NPER" => Some(Function::Nper),
|
||||
"FV" => Some(Function::Fv),
|
||||
"FVSCHEDULE" => Some(Function::Fvschedule),
|
||||
"PPMT" => Some(Function::Ppmt),
|
||||
"PRICE" => Some(Function::Price),
|
||||
"IPMT" => Some(Function::Ipmt),
|
||||
@@ -1069,6 +1072,7 @@ impl fmt::Display for Function {
|
||||
Function::Rate => write!(f, "RATE"),
|
||||
Function::Nper => write!(f, "NPER"),
|
||||
Function::Fv => write!(f, "FV"),
|
||||
Function::Fvschedule => write!(f, "FVSCHEDULE"),
|
||||
Function::Ppmt => write!(f, "PPMT"),
|
||||
Function::Price => write!(f, "PRICE"),
|
||||
Function::Ipmt => write!(f, "IPMT"),
|
||||
@@ -1350,6 +1354,7 @@ impl Model {
|
||||
Function::Rate => self.fn_rate(args, cell),
|
||||
Function::Nper => self.fn_nper(args, cell),
|
||||
Function::Fv => self.fn_fv(args, cell),
|
||||
Function::Fvschedule => self.fn_fvschedule(args, cell),
|
||||
Function::Ppmt => self.fn_ppmt(args, cell),
|
||||
Function::Price => self.fn_price(args, cell),
|
||||
Function::Ipmt => self.fn_ipmt(args, cell),
|
||||
|
||||
@@ -65,6 +65,7 @@ mod test_escape_quotes;
|
||||
mod test_extend;
|
||||
mod test_fn_fv;
|
||||
mod test_fn_round;
|
||||
mod test_fn_fvschedule;
|
||||
mod test_fn_type;
|
||||
mod test_frozen_rows_and_columns;
|
||||
mod test_geomean;
|
||||
|
||||
@@ -26,6 +26,10 @@ fn fn_arguments() {
|
||||
model._set("E2", "=RATE(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();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"#ERROR!");
|
||||
@@ -47,6 +51,10 @@ fn fn_arguments() {
|
||||
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!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -469,3 +477,18 @@ fn fn_db_misc() {
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
127
base/src/test/test_fn_fvschedule.rs
Normal file
127
base/src/test/test_fn_fvschedule.rs
Normal file
@@ -0,0 +1,127 @@
|
||||
#![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!");
|
||||
}
|
||||
Reference in New Issue
Block a user