merge fvschedule #56

# Conflicts:
#	base/src/functions/mod.rs
#	base/src/test/mod.rs
This commit is contained in:
Nicolás Hatcher
2025-11-06 21:44:56 +01:00
parent 9c68fcc8ec
commit 04e012b518
8 changed files with 198 additions and 4 deletions

View File

@@ -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> { 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]
@@ -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::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),
@@ -1020,6 +1029,7 @@ 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),

View File

@@ -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]) // IPMT(rate, per, nper, pv, [fv], [type])
pub(crate) fn fn_ipmt(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { pub(crate) fn fn_ipmt(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let arg_count = args.len(); let arg_count = args.len();

View File

@@ -225,6 +225,7 @@ pub enum Function {
Dollarfr, Dollarfr,
Effect, Effect,
Fv, Fv,
Fvschedule,
Ipmt, Ipmt,
Irr, Irr,
Ispmt, Ispmt,
@@ -317,7 +318,7 @@ pub enum Function {
} }
impl Function { impl Function {
pub fn into_iter() -> IntoIter<Function, 260> { pub fn into_iter() -> IntoIter<Function, 261> {
[ [
Function::And, Function::And,
Function::False, Function::False,
@@ -483,6 +484,7 @@ impl Function {
Function::Rate, Function::Rate,
Function::Nper, Function::Nper,
Function::Fv, Function::Fv,
Function::Fvschedule,
Function::Ppmt, Function::Ppmt,
Function::Price, Function::Price,
Function::Ipmt, Function::Ipmt,
@@ -826,6 +828,7 @@ impl Function {
"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), "PRICE" => Some(Function::Price),
"IPMT" => Some(Function::Ipmt), "IPMT" => Some(Function::Ipmt),
@@ -1069,6 +1072,7 @@ impl fmt::Display for Function {
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::Price => write!(f, "PRICE"),
Function::Ipmt => write!(f, "IPMT"), Function::Ipmt => write!(f, "IPMT"),
@@ -1350,6 +1354,7 @@ impl Model {
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::Price => self.fn_price(args, cell),
Function::Ipmt => self.fn_ipmt(args, cell), Function::Ipmt => self.fn_ipmt(args, cell),

View File

@@ -65,6 +65,7 @@ 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;

View File

@@ -26,6 +26,10 @@ 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!");
@@ -47,6 +51,10 @@ 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]
@@ -469,3 +477,18 @@ 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");
}

View 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!");
}

View File

@@ -31,7 +31,7 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
| DURATION | <Badge type="tip" text="Available" /> | | | DURATION | <Badge type="tip" text="Available" /> | |
| 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="info" text="Not implemented yet" /> | | | FVSCHEDULE | <Badge type="tip" text="Available" /> | [FVSCHEDULE](financial/fvschedule) |
| INTRATE | <Badge type="info" text="Not implemented yet" /> | | | 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" /> | |

View File

@@ -7,6 +7,5 @@ lang: en-US
# FVSCHEDULE # FVSCHEDULE
::: 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)
::: :::