diff --git a/base/src/expressions/parser/mod.rs b/base/src/expressions/parser/mod.rs index ed9d28148..e24c9c4e2 100644 --- a/base/src/expressions/parser/mod.rs +++ b/base/src/expressions/parser/mod.rs @@ -787,6 +787,13 @@ impl<'a> Parser<'a> { args, }; } + let trimmed_name = name.trim_start_matches("_xlfn."); + if trimmed_name.eq_ignore_ascii_case("FVSCHEDULE") { + return Node::FunctionKind { + kind: Function::Fvschedule, + args, + }; + } return Node::InvalidFunctionKind { name, args }; } let context = &self.context; diff --git a/base/src/expressions/parser/static_analysis.rs b/base/src/expressions/parser/static_analysis.rs index 04edf5614..03b40c586 100644 --- a/base/src/expressions/parser/static_analysis.rs +++ b/base/src/expressions/parser/static_analysis.rs @@ -550,6 +550,14 @@ fn args_signature_irr(arg_count: usize) -> Vec { } } +fn args_signature_fvschedule(arg_count: usize) -> Vec { + if arg_count == 2 { + vec![Signature::Scalar, Signature::Vector] + } else { + vec![Signature::Error; arg_count] + } +} + fn args_signature_xirr(arg_count: usize) -> Vec { if arg_count == 2 { vec![Signature::Vector; arg_count] @@ -753,6 +761,7 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec 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), @@ -1147,6 +1156,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), diff --git a/base/src/functions/financial.rs b/base/src/functions/financial.rs index ec1c986d6..a744afd41 100644 --- a/base/src/functions/financial.rs +++ b/base/src/functions/financial.rs @@ -641,6 +641,35 @@ impl<'a> Model<'a> { } } + // 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(); diff --git a/base/src/functions/mod.rs b/base/src/functions/mod.rs index 3a8edf115..7f71abf1d 100644 --- a/base/src/functions/mod.rs +++ b/base/src/functions/mod.rs @@ -316,6 +316,7 @@ pub enum Function { Dollarfr, Effect, Fv, + Fvschedule, Ipmt, Irr, Ispmt, @@ -1074,6 +1075,7 @@ impl Function { Function::Dollarfr => functions.dollarfr.clone(), Function::Effect => functions.effect.clone(), Function::Fv => functions.fv.clone(), + Function::Fvschedule => "FVSCHEDULE".to_string(), Function::Ipmt => functions.ipmt.clone(), Function::Irr => functions.irr.clone(), Function::Ispmt => functions.ispmt.clone(), @@ -1168,7 +1170,7 @@ impl Function { Function::Steyx => functions.steyx.clone(), } } - pub fn into_iter() -> IntoIter { + pub fn into_iter() -> IntoIter { [ Function::And, Function::False, @@ -1338,6 +1340,7 @@ impl Function { Function::Rate, Function::Nper, Function::Fv, + Function::Fvschedule, Function::Ppmt, Function::Ipmt, Function::Npv, @@ -1794,6 +1797,7 @@ impl<'a> Model<'a> { 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::Ipmt => self.fn_ipmt(args, cell), Function::Npv => self.fn_npv(args, cell), diff --git a/base/src/test/mod.rs b/base/src/test/mod.rs index 3c2b0f052..cf0c80f8d 100644 --- a/base/src/test/mod.rs +++ b/base/src/test/mod.rs @@ -77,6 +77,7 @@ mod test_extend; mod test_floor; mod test_fn_datevalue_timevalue; mod test_fn_fv; +mod test_fn_fvschedule; mod test_fn_round; mod test_fn_type; mod test_frozen_rows_and_columns; diff --git a/base/src/test/test_fn_financial.rs b/base/src/test/test_fn_financial.rs index a5f31f036..ab56937cc 100644 --- a/base/src/test/test_fn_financial.rs +++ b/base/src/test/test_fn_financial.rs @@ -25,6 +25,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!"); @@ -46,6 +50,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] @@ -468,3 +476,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"); +} diff --git a/base/src/test/test_fn_fvschedule.rs b/base/src/test/test_fn_fvschedule.rs new file mode 100644 index 000000000..b2e746723 --- /dev/null +++ b/base/src/test/test_fn_fvschedule.rs @@ -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!"); +} diff --git a/docs/src/functions/financial.md b/docs/src/functions/financial.md index e8593cdf2..f04e84239 100644 --- a/docs/src/functions/financial.md +++ b/docs/src/functions/financial.md @@ -31,7 +31,7 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir | DURATION | | – | | EFFECT | | – | | FV | | [FV](financial/fv) | -| FVSCHEDULE | | – | +| FVSCHEDULE | | [FVSCHEDULE](financial/fvschedule) | | INTRATE | | – | | IPMT | | – | | IRR | | – | diff --git a/docs/src/functions/financial/fvschedule.md b/docs/src/functions/financial/fvschedule.md index 797ca7d90..bb860a1d7 100644 --- a/docs/src/functions/financial/fvschedule.md +++ b/docs/src/functions/financial/fvschedule.md @@ -7,6 +7,5 @@ lang: en-US # FVSCHEDULE ::: warning -🚧 This function is not yet available in IronCalc. -[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions) +🚧 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). ::: \ No newline at end of file