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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions base/src/expressions/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
10 changes: 10 additions & 0 deletions base/src/expressions/parser/static_analysis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -753,6 +761,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),
Expand Down Expand Up @@ -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),
Expand Down
29 changes: 29 additions & 0 deletions base/src/functions/financial.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
6 changes: 5 additions & 1 deletion base/src/functions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,7 @@ pub enum Function {
Dollarfr,
Effect,
Fv,
Fvschedule,
Ipmt,
Irr,
Ispmt,
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -1168,7 +1170,7 @@ impl Function {
Function::Steyx => functions.steyx.clone(),
}
}
pub fn into_iter() -> IntoIter<Function, 345> {
pub fn into_iter() -> IntoIter<Function, 346> {
[
Function::And,
Function::False,
Expand Down Expand Up @@ -1338,6 +1340,7 @@ impl Function {
Function::Rate,
Function::Nper,
Function::Fv,
Function::Fvschedule,
Function::Ppmt,
Function::Ipmt,
Function::Npv,
Expand Down Expand Up @@ -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),
Expand Down
1 change: 1 addition & 0 deletions base/src/test/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
23 changes: 23 additions & 0 deletions base/src/test/test_fn_financial.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!");
Expand All @@ -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]
Expand Down Expand Up @@ -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");
}
127 changes: 127 additions & 0 deletions base/src/test/test_fn_fvschedule.rs
Original file line number Diff line number Diff line change
@@ -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!");
}
2 changes: 1 addition & 1 deletion docs/src/functions/financial.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
| DURATION | <Badge type="info" text="Not implemented yet" /> | – |
| EFFECT | <Badge type="tip" text="Available" /> | – |
| FV | <Badge type="tip" text="Available" /> | [FV](financial/fv) |
| FVSCHEDULE | <Badge type="info" text="Not implemented yet" /> | |
| FVSCHEDULE | <Badge type="tip" text="Available" /> | [FVSCHEDULE](financial/fvschedule) |
| INTRATE | <Badge type="info" text="Not implemented yet" /> | – |
| IPMT | <Badge type="tip" text="Available" /> | – |
| IRR | <Badge type="tip" text="Available" /> | – |
Expand Down
3 changes: 1 addition & 2 deletions docs/src/functions/financial/fvschedule.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
:::