2801 lines
95 KiB
Rust
2801 lines
95 KiB
Rust
use chrono::Datelike;
|
|
|
|
use crate::{
|
|
calc_result::CalcResult,
|
|
constants::{LAST_COLUMN, LAST_ROW, MAXIMUM_DATE_SERIAL_NUMBER, MINIMUM_DATE_SERIAL_NUMBER},
|
|
expressions::{parser::Node, token::Error, types::CellReferenceIndex},
|
|
formatter::dates::from_excel_date,
|
|
model::Model,
|
|
};
|
|
|
|
use super::financial_util::{compute_irr, compute_npv, compute_rate, compute_xirr, compute_xnpv};
|
|
|
|
// Financial calculation constants
|
|
const DAYS_IN_YEAR_360: i32 = 360;
|
|
const DAYS_ACTUAL: i32 = 365;
|
|
const DAYS_LEAP_YEAR: i32 = 366;
|
|
const DAYS_IN_MONTH_360: i32 = 30;
|
|
const TBILL_MATURITY_THRESHOLD: f64 = 183.0;
|
|
|
|
// See:
|
|
// https://github.com/apache/openoffice/blob/c014b5f2b55cff8d4b0c952d5c16d62ecde09ca1/main/scaddins/source/analysis/financial.cxx
|
|
|
|
fn is_less_than_one_year(start_date: i64, end_date: i64) -> Result<bool, String> {
|
|
let end = from_excel_date(end_date)?;
|
|
let start = from_excel_date(start_date)?;
|
|
if end_date - start_date < DAYS_ACTUAL as i64 {
|
|
return Ok(true);
|
|
}
|
|
let end_year = end.year();
|
|
let start_year = start.year();
|
|
if end_year == start_year {
|
|
return Ok(true);
|
|
}
|
|
if end_year != start_year + 1 {
|
|
return Ok(false);
|
|
}
|
|
let start_month = start.month();
|
|
let end_month = end.month();
|
|
if end_month < start_month {
|
|
return Ok(true);
|
|
}
|
|
if end_month > start_month {
|
|
return Ok(false);
|
|
}
|
|
// we are one year later same month
|
|
let start_day = start.day();
|
|
let end_day = end.day();
|
|
Ok(end_day <= start_day)
|
|
}
|
|
|
|
fn is_leap_year(year: i32) -> bool {
|
|
(year % 4 == 0) && (year % 100 != 0 || year % 400 == 0)
|
|
}
|
|
|
|
fn is_last_day_of_february(date: chrono::NaiveDate) -> bool {
|
|
date.month() == 2 && date.day() == if is_leap_year(date.year()) { 29 } else { 28 }
|
|
}
|
|
|
|
fn days360_us(start: chrono::NaiveDate, end: chrono::NaiveDate) -> i32 {
|
|
let mut d1 = start.day() as i32;
|
|
let mut d2 = end.day() as i32;
|
|
let m1 = start.month() as i32;
|
|
let m2 = end.month() as i32;
|
|
let y1 = start.year();
|
|
let y2 = end.year();
|
|
|
|
// US (NASD) 30/360 method - implementing official specification
|
|
|
|
// Rule 1: If both date A and B fall on the last day of February, then date B will be changed to the 30th
|
|
if is_last_day_of_february(start) && is_last_day_of_february(end) {
|
|
d2 = DAYS_IN_MONTH_360;
|
|
}
|
|
|
|
// Rule 2: If date A falls on the 31st of a month or last day of February, then date A will be changed to the 30th
|
|
if d1 == 31 || is_last_day_of_february(start) {
|
|
d1 = DAYS_IN_MONTH_360;
|
|
}
|
|
|
|
// Rule 3: If date A falls on the 30th after applying rule 2 and date B falls on the 31st, then date B will be changed to the 30th
|
|
if d1 == DAYS_IN_MONTH_360 && d2 == 31 {
|
|
d2 = DAYS_IN_MONTH_360;
|
|
}
|
|
|
|
DAYS_IN_YEAR_360 * (y2 - y1) + DAYS_IN_MONTH_360 * (m2 - m1) + (d2 - d1)
|
|
}
|
|
|
|
fn days360_eu(start: chrono::NaiveDate, end: chrono::NaiveDate) -> i32 {
|
|
let mut d1 = start.day() as i32;
|
|
let mut d2 = end.day() as i32;
|
|
let m1 = start.month() as i32;
|
|
let m2 = end.month() as i32;
|
|
let y1 = start.year();
|
|
let y2 = end.year();
|
|
|
|
if d1 == 31 {
|
|
d1 = DAYS_IN_MONTH_360;
|
|
}
|
|
if d2 == 31 {
|
|
d2 = DAYS_IN_MONTH_360;
|
|
}
|
|
|
|
d2 + m2 * DAYS_IN_MONTH_360 + y2 * DAYS_IN_YEAR_360
|
|
- d1
|
|
- m1 * DAYS_IN_MONTH_360
|
|
- y1 * DAYS_IN_YEAR_360
|
|
}
|
|
|
|
fn days_between(start: i64, end: i64, basis: i32) -> Result<i32, String> {
|
|
let start_date = from_excel_date(start)?;
|
|
let end_date = from_excel_date(end)?;
|
|
Ok(match basis {
|
|
0 => days360_us(start_date, end_date),
|
|
1..=3 => (end - start) as i32,
|
|
4 => days360_eu(start_date, end_date),
|
|
_ => return Err("invalid basis".to_string()),
|
|
})
|
|
}
|
|
|
|
fn days_in_year(date: chrono::NaiveDate, basis: i32) -> Result<i32, String> {
|
|
Ok(match basis {
|
|
0 | 2 | 4 => DAYS_IN_YEAR_360,
|
|
1 => {
|
|
if is_leap_year(date.year()) {
|
|
DAYS_LEAP_YEAR
|
|
} else {
|
|
DAYS_ACTUAL
|
|
}
|
|
}
|
|
3 => DAYS_ACTUAL,
|
|
_ => return Err("invalid basis".to_string()),
|
|
})
|
|
}
|
|
|
|
/// Returns days in year for financial calculations (simplified version without leap year checking)
|
|
fn days_in_year_simple(basis: i32) -> f64 {
|
|
match basis {
|
|
0 | 2 | 4 => DAYS_IN_YEAR_360 as f64,
|
|
1 | 3 => DAYS_ACTUAL as f64,
|
|
_ => DAYS_IN_YEAR_360 as f64,
|
|
}
|
|
}
|
|
|
|
/// Validates frequency parameter for bond functions (must be 1, 2, or 4)
|
|
fn validate_frequency(frequency: i32, cell: CellReferenceIndex) -> Result<(), CalcResult> {
|
|
if frequency != 1 && frequency != 2 && frequency != 4 {
|
|
return Err(CalcResult::new_error(
|
|
Error::NUM,
|
|
cell,
|
|
"frequency should be 1, 2 or 4".to_string(),
|
|
));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Macro to reduce duplication in financial functions that follow the pattern:
|
|
/// 1. Parse settlement/maturity and two parameters with validation
|
|
/// 2. Calculate year fraction
|
|
/// 3. Apply formula and return result
|
|
macro_rules! financial_function_with_year_frac {
|
|
(
|
|
$args:ident, $self:ident, $cell:ident,
|
|
param1_name: $param1_name:literal,
|
|
param2_name: $param2_name:literal,
|
|
validator: $validator:expr,
|
|
formula: |$settlement:ident, $maturity:ident, $param1:ident, $param2:ident, $basis:ident, $year_frac:ident| $formula:expr
|
|
) => {{
|
|
// Parse and validate arguments
|
|
let arg_count = $args.len();
|
|
if !(4..=5).contains(&arg_count) {
|
|
return CalcResult::new_args_number_error($cell);
|
|
}
|
|
|
|
let ($settlement, $maturity) =
|
|
match parse_and_validate_settlement_maturity($args, $self, $cell, true) {
|
|
Ok(sm) => sm,
|
|
Err(err) => return err,
|
|
};
|
|
let $param1 = match $self.get_number_no_bools(&$args[2], $cell) {
|
|
Ok(p) => p,
|
|
Err(err) => return err,
|
|
};
|
|
let $param2 = match $self.get_number_no_bools(&$args[3], $cell) {
|
|
Ok(p) => p,
|
|
Err(err) => return err,
|
|
};
|
|
let $basis = match parse_optional_basis($args, 4, arg_count, $self, $cell) {
|
|
Ok(b) => b,
|
|
Err(err) => return err,
|
|
};
|
|
|
|
// Apply custom validation
|
|
if let Err(msg) = ($validator)($param1, $param2) {
|
|
return CalcResult::new_error(
|
|
Error::NUM,
|
|
$cell,
|
|
format!("{} and {}: {}", $param1_name, $param2_name, msg),
|
|
);
|
|
}
|
|
|
|
let $year_frac = match year_frac($settlement as i64, $maturity as i64, $basis) {
|
|
Ok(f) => f,
|
|
Err(_) => return CalcResult::new_error(Error::NUM, $cell, "Invalid date".to_string()),
|
|
};
|
|
|
|
let result = $formula;
|
|
CalcResult::Number(result)
|
|
}};
|
|
}
|
|
|
|
/// Validates that settlement < maturity for financial functions
|
|
fn validate_settlement_maturity(
|
|
settlement: f64,
|
|
maturity: f64,
|
|
cell: CellReferenceIndex,
|
|
) -> Result<(), CalcResult> {
|
|
if settlement >= maturity {
|
|
return Err(CalcResult::new_error(
|
|
Error::NUM,
|
|
cell,
|
|
"settlement should be < maturity".to_string(),
|
|
));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Validates date range for financial calculations
|
|
fn validate_date_range(date: f64, cell: CellReferenceIndex) -> Result<(), CalcResult> {
|
|
if date < MINIMUM_DATE_SERIAL_NUMBER as f64 || date > MAXIMUM_DATE_SERIAL_NUMBER as f64 {
|
|
return Err(CalcResult::new_error(
|
|
Error::NUM,
|
|
cell,
|
|
"Invalid number for date".to_string(),
|
|
));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Helper function to convert date serial number to chrono date with error handling
|
|
fn convert_date_serial(
|
|
date_serial: f64,
|
|
cell: CellReferenceIndex,
|
|
) -> Result<chrono::NaiveDate, CalcResult> {
|
|
match from_excel_date(date_serial as i64) {
|
|
Ok(date) => Ok(date),
|
|
Err(_) => Err(CalcResult::new_error(
|
|
Error::NUM,
|
|
cell,
|
|
"Invalid date".to_string(),
|
|
)),
|
|
}
|
|
}
|
|
|
|
/// Helper function to parse optional basis parameter (defaults to 0)
|
|
fn parse_optional_basis(
|
|
args: &[Node],
|
|
basis_arg_index: usize,
|
|
arg_count: usize,
|
|
model: &mut Model,
|
|
cell: CellReferenceIndex,
|
|
) -> Result<i32, CalcResult> {
|
|
if arg_count > basis_arg_index {
|
|
match model.get_number_no_bools(&args[basis_arg_index], cell) {
|
|
Ok(f) => Ok(f.trunc() as i32),
|
|
Err(s) => Err(s),
|
|
}
|
|
} else {
|
|
Ok(0)
|
|
}
|
|
}
|
|
|
|
/// Validates basis parameter (must be 0-4)
|
|
fn validate_basis(basis: i32, cell: CellReferenceIndex) -> Result<(), CalcResult> {
|
|
if !(0..=4).contains(&basis) {
|
|
return Err(CalcResult::new_error(
|
|
Error::NUM,
|
|
cell,
|
|
"invalid basis".to_string(),
|
|
));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Validates both frequency and basis for coupon functions
|
|
fn validate_frequency_and_basis(
|
|
frequency: i32,
|
|
basis: i32,
|
|
cell: CellReferenceIndex,
|
|
) -> Result<(), CalcResult> {
|
|
if ![1, 2, 4].contains(&frequency) || !(0..=4).contains(&basis) {
|
|
return Err(CalcResult::new_error(
|
|
Error::NUM,
|
|
cell,
|
|
"invalid arguments".to_string(),
|
|
));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Helper function for common negative value validation
|
|
fn validate_non_negative(
|
|
value: f64,
|
|
parameter_name: &str,
|
|
cell: CellReferenceIndex,
|
|
) -> Result<(), CalcResult> {
|
|
if value < 0.0 {
|
|
return Err(CalcResult::new_error(
|
|
Error::NUM,
|
|
cell,
|
|
format!("{} cannot be negative", parameter_name),
|
|
));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Enhanced helper function to parse, validate settlement/maturity with optional date range validation
|
|
fn parse_and_validate_settlement_maturity(
|
|
args: &[Node],
|
|
model: &mut Model,
|
|
cell: CellReferenceIndex,
|
|
check_date_range: bool,
|
|
) -> Result<(f64, f64), CalcResult> {
|
|
// Parse settlement and maturity
|
|
let settlement = model.get_number_no_bools(&args[0], cell)?;
|
|
let maturity = model.get_number_no_bools(&args[1], cell)?;
|
|
|
|
// Validate settlement < maturity
|
|
validate_settlement_maturity(settlement, maturity, cell)?;
|
|
|
|
// Optionally validate date ranges
|
|
if check_date_range {
|
|
validate_date_range(settlement, cell)?;
|
|
validate_date_range(maturity, cell)?;
|
|
}
|
|
|
|
Ok((settlement, maturity))
|
|
}
|
|
|
|
/// Helper function to parse multiple required numeric parameters efficiently
|
|
/// Returns a vector of parsed values in the same order as the indices
|
|
fn parse_required_params(
|
|
args: &[Node],
|
|
indices: &[usize],
|
|
model: &mut Model,
|
|
cell: CellReferenceIndex,
|
|
use_no_bools: bool,
|
|
) -> Result<Vec<f64>, CalcResult> {
|
|
let mut params = Vec::with_capacity(indices.len());
|
|
for &index in indices {
|
|
let value = if use_no_bools {
|
|
model.get_number_no_bools(&args[index], cell)?
|
|
} else {
|
|
model.get_number(&args[index], cell)?
|
|
};
|
|
params.push(value);
|
|
}
|
|
Ok(params)
|
|
}
|
|
|
|
/// Helper function to validate argument count and return early if invalid
|
|
fn validate_arg_count_or_return(
|
|
arg_count: usize,
|
|
min: usize,
|
|
max: usize,
|
|
cell: CellReferenceIndex,
|
|
) -> Option<CalcResult> {
|
|
if !(min..=max).contains(&arg_count) {
|
|
Some(CalcResult::new_args_number_error(cell))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
/// Helper function to convert date to serial number with consistent error handling
|
|
fn date_to_serial_with_validation(date: chrono::NaiveDate, cell: CellReferenceIndex) -> CalcResult {
|
|
match crate::formatter::dates::date_to_serial_number(date.day(), date.month(), date.year()) {
|
|
Ok(n) => {
|
|
if !(MINIMUM_DATE_SERIAL_NUMBER..=MAXIMUM_DATE_SERIAL_NUMBER).contains(&n) {
|
|
CalcResult::new_error(Error::NUM, cell, "date out of range".to_string())
|
|
} else {
|
|
CalcResult::Number(n as f64)
|
|
}
|
|
}
|
|
Err(msg) => CalcResult::new_error(Error::NUM, cell, msg),
|
|
}
|
|
}
|
|
|
|
/// Helper struct for common financial function optional parameters
|
|
struct FinancialOptionalParams {
|
|
pub optional_value: f64,
|
|
pub period_start: bool,
|
|
}
|
|
|
|
/// Helper function to parse common optional financial parameters: [optional_value], [type]
|
|
/// optional_value defaults to 0.0, type defaults to false (end of period)
|
|
fn parse_financial_optional_params(
|
|
args: &[Node],
|
|
arg_count: usize,
|
|
optional_value_index: usize,
|
|
model: &mut Model,
|
|
cell: CellReferenceIndex,
|
|
) -> Result<FinancialOptionalParams, CalcResult> {
|
|
let optional_value = if arg_count > optional_value_index {
|
|
model.get_number(&args[optional_value_index], cell)?
|
|
} else {
|
|
0.0
|
|
};
|
|
|
|
let period_start = if arg_count > optional_value_index + 1 {
|
|
model.get_number(&args[optional_value_index + 1], cell)? != 0.0
|
|
} else {
|
|
false // at the end of the period
|
|
};
|
|
|
|
Ok(FinancialOptionalParams {
|
|
optional_value,
|
|
period_start,
|
|
})
|
|
}
|
|
|
|
/// Helper function to parse coupon function parameters with truncation (for date serial numbers)
|
|
fn parse_coupon_params_truncated(
|
|
args: &[Node],
|
|
arg_count: usize,
|
|
model: &mut Model,
|
|
cell: CellReferenceIndex,
|
|
) -> Result<(i64, i64, i32, i32), CalcResult> {
|
|
let settlement = match model.get_number_no_bools(&args[0], cell) {
|
|
Ok(f) => f.trunc() as i64,
|
|
Err(s) => return Err(s),
|
|
};
|
|
let maturity = match model.get_number_no_bools(&args[1], cell) {
|
|
Ok(f) => f.trunc() as i64,
|
|
Err(s) => return Err(s),
|
|
};
|
|
let frequency = match model.get_number_no_bools(&args[2], cell) {
|
|
Ok(f) => f.trunc() as i32,
|
|
Err(s) => return Err(s),
|
|
};
|
|
let basis = if arg_count > 3 {
|
|
match model.get_number_no_bools(&args[3], cell) {
|
|
Ok(f) => f.trunc() as i32,
|
|
Err(s) => return Err(s),
|
|
}
|
|
} else {
|
|
0
|
|
};
|
|
Ok((settlement, maturity, frequency, basis))
|
|
}
|
|
|
|
/// Helper struct for validated coupon function parameters
|
|
struct ValidatedCouponParams {
|
|
pub settlement_date: chrono::NaiveDate,
|
|
pub maturity_date: chrono::NaiveDate,
|
|
pub frequency: i32,
|
|
pub basis: i32,
|
|
}
|
|
|
|
/// Helper function to validate T-Bill calculation results
|
|
fn validate_tbill_result(result: f64, cell: CellReferenceIndex) -> CalcResult {
|
|
if result.is_infinite() {
|
|
CalcResult::new_error(Error::DIV, cell, "Division by 0".to_string())
|
|
} else if result.is_nan() {
|
|
CalcResult::new_error(
|
|
Error::NUM,
|
|
cell,
|
|
"Invalid data for T-Bill calculation".to_string(),
|
|
)
|
|
} else {
|
|
CalcResult::Number(result)
|
|
}
|
|
}
|
|
|
|
/// Helper function to validate and normalize fraction parameter for DOLLARDE/DOLLARFR functions
|
|
fn validate_and_normalize_fraction(
|
|
fraction: f64,
|
|
cell: CellReferenceIndex,
|
|
) -> Result<f64, CalcResult> {
|
|
if fraction < 0.0 {
|
|
return Err(CalcResult::new_error(
|
|
Error::NUM,
|
|
cell,
|
|
"fraction should be >= 1".to_string(),
|
|
));
|
|
}
|
|
if fraction < 1.0 {
|
|
return Err(CalcResult::new_error(
|
|
Error::DIV,
|
|
cell,
|
|
"fraction should be >= 1".to_string(),
|
|
));
|
|
}
|
|
|
|
let mut normalized_fraction = fraction.trunc();
|
|
while normalized_fraction > 10.0 {
|
|
normalized_fraction /= 10.0;
|
|
}
|
|
|
|
Ok(normalized_fraction)
|
|
}
|
|
|
|
/// Helper function to handle compute function errors consistently
|
|
fn handle_compute_error<T>(
|
|
result: Result<T, (Error, String)>,
|
|
cell: CellReferenceIndex,
|
|
) -> Result<T, CalcResult> {
|
|
match result {
|
|
Ok(value) => Ok(value),
|
|
Err(error) => Err(CalcResult::Error {
|
|
error: error.0,
|
|
origin: cell,
|
|
message: error.1,
|
|
}),
|
|
}
|
|
}
|
|
|
|
/// Helper function to validate values and dates arrays for XNPV/XIRR functions
|
|
fn validate_values_dates_arrays(
|
|
values: &[f64],
|
|
dates: &[f64],
|
|
cell: CellReferenceIndex,
|
|
) -> Result<Vec<f64>, CalcResult> {
|
|
// Decimal points on dates are truncated
|
|
let normalized_dates: Vec<f64> = dates.iter().map(|s| s.floor()).collect();
|
|
let values_count = values.len();
|
|
|
|
// If values and dates contain a different number of values, return error
|
|
if values_count != dates.len() {
|
|
return Err(CalcResult::new_error(
|
|
Error::NUM,
|
|
cell,
|
|
"Values and dates must be the same length".to_string(),
|
|
));
|
|
}
|
|
|
|
if values_count == 0 {
|
|
return Err(CalcResult::new_error(
|
|
Error::NUM,
|
|
cell,
|
|
"Not enough values".to_string(),
|
|
));
|
|
}
|
|
|
|
let first_date = normalized_dates[0];
|
|
for date in &normalized_dates {
|
|
// Validate date range
|
|
if *date < MINIMUM_DATE_SERIAL_NUMBER as f64 || *date > MAXIMUM_DATE_SERIAL_NUMBER as f64 {
|
|
return Err(CalcResult::new_error(
|
|
Error::NUM,
|
|
cell,
|
|
"Invalid number for date".to_string(),
|
|
));
|
|
}
|
|
|
|
// If any date precedes the starting date, return error
|
|
if date < &first_date {
|
|
return Err(CalcResult::new_error(
|
|
Error::NUM,
|
|
cell,
|
|
"Date precedes the starting date".to_string(),
|
|
));
|
|
}
|
|
}
|
|
|
|
Ok(normalized_dates)
|
|
}
|
|
|
|
/// Parse and validate T-Bill parameters, returning (days_to_maturity, param_value)
|
|
fn parse_tbill_params(
|
|
args: &[Node],
|
|
model: &mut Model,
|
|
cell: CellReferenceIndex,
|
|
) -> Result<(f64, f64), CalcResult> {
|
|
// Parse settlement, maturity, and third parameter
|
|
let settlement = model.get_number_no_bools(&args[0], cell)?;
|
|
let maturity = model.get_number_no_bools(&args[1], cell)?;
|
|
let param_value = model.get_number_no_bools(&args[2], cell)?;
|
|
|
|
// Validate settlement <= maturity
|
|
if settlement > maturity {
|
|
return Err(CalcResult::new_error(
|
|
Error::NUM,
|
|
cell,
|
|
"settlement should be <= maturity".to_string(),
|
|
));
|
|
}
|
|
|
|
// Validate less than one year
|
|
let less_than_one_year = match is_less_than_one_year(settlement as i64, maturity as i64) {
|
|
Ok(f) => f,
|
|
Err(_) => {
|
|
return Err(CalcResult::new_error(
|
|
Error::NUM,
|
|
cell,
|
|
"Invalid date".to_string(),
|
|
))
|
|
}
|
|
};
|
|
if !less_than_one_year {
|
|
return Err(CalcResult::new_error(
|
|
Error::NUM,
|
|
cell,
|
|
"maturity <= settlement + year".to_string(),
|
|
));
|
|
}
|
|
|
|
// Validate parameter > 0
|
|
if param_value <= 0.0 {
|
|
return Err(CalcResult::new_error(
|
|
Error::NUM,
|
|
cell,
|
|
"parameter should be >0".to_string(),
|
|
));
|
|
}
|
|
|
|
let days_to_maturity = maturity - settlement;
|
|
Ok((days_to_maturity, param_value))
|
|
}
|
|
|
|
/// Helper struct for validated bond pricing function parameters
|
|
struct BondPricingParams {
|
|
pub third_param: f64, // yld for PRICE, price for YIELD
|
|
pub redemption: f64,
|
|
pub frequency: i32,
|
|
pub periods: f64,
|
|
pub coupon: f64,
|
|
}
|
|
|
|
/// Helper function to parse and validate common bond pricing function parameters
|
|
/// Used by PRICE and YIELD functions
|
|
fn parse_and_validate_bond_pricing_params(
|
|
args: &[Node],
|
|
model: &mut Model,
|
|
cell: CellReferenceIndex,
|
|
) -> Result<BondPricingParams, CalcResult> {
|
|
if !(6..=7).contains(&args.len()) {
|
|
return Err(CalcResult::new_args_number_error(cell));
|
|
}
|
|
|
|
let settlement = model.get_number_no_bools(&args[0], cell)?;
|
|
let maturity = model.get_number_no_bools(&args[1], cell)?;
|
|
let rate = model.get_number_no_bools(&args[2], cell)?;
|
|
let third_param = model.get_number_no_bools(&args[3], cell)?;
|
|
let redemption = model.get_number_no_bools(&args[4], cell)?;
|
|
|
|
let frequency = match model.get_number_no_bools(&args[5], cell) {
|
|
Ok(f) => f.trunc() as i32,
|
|
Err(s) => return Err(s),
|
|
};
|
|
|
|
validate_frequency(frequency, cell)?;
|
|
validate_settlement_maturity(settlement, maturity, cell)?;
|
|
|
|
let basis = if args.len() == 7 {
|
|
match model.get_number_no_bools(&args[6], cell) {
|
|
Ok(f) => f.trunc() as i32,
|
|
Err(s) => return Err(s),
|
|
}
|
|
} else {
|
|
0
|
|
};
|
|
|
|
let days_in_year = days_in_year_simple(basis);
|
|
let days = maturity - settlement;
|
|
let periods = ((days * frequency as f64) / days_in_year).round();
|
|
if periods <= 0.0 {
|
|
return Err(CalcResult::new_error(
|
|
Error::NUM,
|
|
cell,
|
|
"invalid dates".to_string(),
|
|
));
|
|
}
|
|
let coupon = redemption * rate / frequency as f64;
|
|
|
|
Ok(BondPricingParams {
|
|
third_param,
|
|
redemption,
|
|
frequency,
|
|
periods,
|
|
coupon,
|
|
})
|
|
}
|
|
|
|
/// Helper struct for validated cumulative payment function parameters
|
|
struct CumulativePaymentParams {
|
|
pub rate: f64,
|
|
pub nper: f64,
|
|
pub pv: f64,
|
|
pub start_period: i32,
|
|
pub end_period: i32,
|
|
pub period_type: bool,
|
|
}
|
|
|
|
/// Helper function to parse and validate cumulative payment function parameters
|
|
fn parse_and_validate_cumulative_payment_params(
|
|
args: &[Node],
|
|
model: &mut Model,
|
|
cell: CellReferenceIndex,
|
|
) -> Result<CumulativePaymentParams, CalcResult> {
|
|
// Check argument count
|
|
if args.len() != 6 {
|
|
return Err(CalcResult::new_args_number_error(cell));
|
|
}
|
|
|
|
// Parse rate, nper, pv
|
|
let rate = model.get_number_no_bools(&args[0], cell)?;
|
|
let nper = model.get_number_no_bools(&args[1], cell)?;
|
|
let pv = model.get_number_no_bools(&args[2], cell)?;
|
|
|
|
// Parse periods with appropriate rounding
|
|
let start_period = model.get_number_no_bools(&args[3], cell)?.ceil() as i32;
|
|
let end_period = model.get_number_no_bools(&args[4], cell)?.trunc() as i32;
|
|
|
|
// Parse and validate period type (0 = end of period, 1 = beginning of period)
|
|
let period_type = match model.get_number_no_bools(&args[5], cell)? {
|
|
0.0 => false,
|
|
1.0 => true,
|
|
_ => {
|
|
return Err(CalcResult::new_error(
|
|
Error::NUM,
|
|
cell,
|
|
"invalid period type".to_string(),
|
|
))
|
|
}
|
|
};
|
|
|
|
// Validate period order
|
|
if start_period > end_period {
|
|
return Err(CalcResult::new_error(
|
|
Error::NUM,
|
|
cell,
|
|
"start period should come before end period".to_string(),
|
|
));
|
|
}
|
|
|
|
// Validate positive parameters
|
|
if rate <= 0.0 || nper <= 0.0 || pv <= 0.0 || start_period < 1 {
|
|
return Err(CalcResult::new_error(
|
|
Error::NUM,
|
|
cell,
|
|
"invalid parameters".to_string(),
|
|
));
|
|
}
|
|
|
|
Ok(CumulativePaymentParams {
|
|
rate,
|
|
nper,
|
|
pv,
|
|
start_period,
|
|
end_period,
|
|
period_type,
|
|
})
|
|
}
|
|
|
|
/// Helper function to validate and parse common coupon function parameters
|
|
fn parse_and_validate_coupon_params(
|
|
args: &[Node],
|
|
arg_count: usize,
|
|
model: &mut Model,
|
|
cell: CellReferenceIndex,
|
|
) -> Result<ValidatedCouponParams, CalcResult> {
|
|
// Check argument count
|
|
if !(3..=4).contains(&arg_count) {
|
|
return Err(CalcResult::new_args_number_error(cell));
|
|
}
|
|
|
|
// Parse parameters
|
|
let (settlement, maturity, frequency, basis) =
|
|
parse_coupon_params_truncated(args, arg_count, model, cell)?;
|
|
|
|
// Validate frequency and basis
|
|
validate_frequency_and_basis(frequency, basis, cell)?;
|
|
|
|
// Validate settlement < maturity
|
|
validate_settlement_maturity(settlement as f64, maturity as f64, cell)?;
|
|
|
|
// Convert to dates
|
|
let settlement_date = convert_date_serial(settlement as f64, cell)?;
|
|
let maturity_date = convert_date_serial(maturity as f64, cell)?;
|
|
|
|
Ok(ValidatedCouponParams {
|
|
settlement_date,
|
|
maturity_date,
|
|
frequency,
|
|
basis,
|
|
})
|
|
}
|
|
|
|
fn year_frac(start: i64, end: i64, basis: i32) -> Result<f64, String> {
|
|
let start_date = from_excel_date(start)?;
|
|
let days = days_between(start, end, basis)? as f64;
|
|
let year_days = days_in_year(start_date, basis)? as f64;
|
|
Ok(days / year_days)
|
|
}
|
|
|
|
fn year_fraction(
|
|
start: chrono::NaiveDate,
|
|
end: chrono::NaiveDate,
|
|
basis: i32,
|
|
) -> Result<f64, String> {
|
|
let days = match basis {
|
|
0 => days360_us(start, end) as f64 / DAYS_IN_YEAR_360 as f64,
|
|
1 => (end - start).num_days() as f64 / DAYS_ACTUAL as f64,
|
|
2 => (end - start).num_days() as f64 / DAYS_IN_YEAR_360 as f64,
|
|
3 => (end - start).num_days() as f64 / DAYS_ACTUAL as f64,
|
|
4 => days360_eu(start, end) as f64 / DAYS_IN_YEAR_360 as f64,
|
|
_ => return Err("Invalid basis".to_string()),
|
|
};
|
|
Ok(days)
|
|
}
|
|
|
|
fn days_between_dates(start: chrono::NaiveDate, end: chrono::NaiveDate, basis: i32) -> i32 {
|
|
match basis {
|
|
0 => days360_us(start, end),
|
|
1 | 2 => (end - start).num_days() as i32,
|
|
3 => (end - start).num_days() as i32,
|
|
4 => days360_eu(start, end),
|
|
_ => (end - start).num_days() as i32,
|
|
}
|
|
}
|
|
|
|
fn coupon_dates(
|
|
settlement: chrono::NaiveDate,
|
|
maturity: chrono::NaiveDate,
|
|
freq: i32,
|
|
) -> (chrono::NaiveDate, chrono::NaiveDate) {
|
|
let months = 12 / freq;
|
|
let step = chrono::Months::new(months as u32);
|
|
let mut next_coupon_date = maturity;
|
|
while let Some(prev) = next_coupon_date.checked_sub_months(step) {
|
|
if settlement >= prev {
|
|
return (prev, next_coupon_date);
|
|
}
|
|
next_coupon_date = prev;
|
|
}
|
|
// Fallback if we somehow exit the loop (shouldn't happen in practice)
|
|
(settlement, maturity)
|
|
}
|
|
|
|
fn compute_payment(
|
|
rate: f64,
|
|
nper: f64,
|
|
pv: f64,
|
|
fv: f64,
|
|
period_start: bool,
|
|
) -> Result<f64, (Error, String)> {
|
|
if rate == 0.0 {
|
|
if nper == 0.0 {
|
|
return Err((Error::NUM, "Period count must be non zero".to_string()));
|
|
}
|
|
return Ok(-(pv + fv) / nper);
|
|
}
|
|
if rate <= -1.0 {
|
|
return Err((Error::NUM, "Rate must be > -1".to_string()));
|
|
};
|
|
let rate_nper = if nper == 0.0 {
|
|
1.0
|
|
} else {
|
|
(1.0 + rate).powf(nper)
|
|
};
|
|
let result = if period_start {
|
|
// type = 1
|
|
(fv + pv * rate_nper) * rate / ((1.0 + rate) * (1.0 - rate_nper))
|
|
} else {
|
|
(fv * rate + pv * rate * rate_nper) / (1.0 - rate_nper)
|
|
};
|
|
if result.is_nan() || result.is_infinite() {
|
|
return Err((Error::NUM, "Invalid result".to_string()));
|
|
}
|
|
Ok(result)
|
|
}
|
|
|
|
fn compute_future_value(
|
|
rate: f64,
|
|
nper: f64,
|
|
pmt: f64,
|
|
pv: f64,
|
|
period_start: bool,
|
|
) -> Result<f64, (Error, String)> {
|
|
if rate == 0.0 {
|
|
return Ok(-pv - pmt * nper);
|
|
}
|
|
if rate == -1.0 && nper < 0.0 {
|
|
return Err((Error::DIV, "Divide by zero".to_string()));
|
|
}
|
|
|
|
let rate_nper = (1.0 + rate).powf(nper);
|
|
let fv = if period_start {
|
|
// type = 1
|
|
-pv * rate_nper - pmt * (1.0 + rate) * (rate_nper - 1.0) / rate
|
|
} else {
|
|
-pv * rate_nper - pmt * (rate_nper - 1.0) / rate
|
|
};
|
|
if fv.is_nan() {
|
|
return Err((Error::NUM, "Invalid result".to_string()));
|
|
}
|
|
if !fv.is_finite() {
|
|
return Err((Error::DIV, "Divide by zero".to_string()));
|
|
}
|
|
Ok(fv)
|
|
}
|
|
|
|
fn compute_ipmt(
|
|
rate: f64,
|
|
period: f64,
|
|
period_count: f64,
|
|
present_value: f64,
|
|
future_value: f64,
|
|
period_start: bool,
|
|
) -> Result<f64, (Error, String)> {
|
|
// http://www.staff.city.ac.uk/o.s.kerr/CompMaths/WSheet4.pdf
|
|
// https://www.experts-exchange.com/articles/1948/A-Guide-to-the-PMT-FV-IPMT-and-PPMT-Functions.html
|
|
// type = 0 (end of period)
|
|
// impt = -[(1+rate)^(period-1)*(pv*rate+pmt)-pmt]
|
|
// ipmt = FV(rate, period-1, payment, pv, type) * rate
|
|
// type = 1 (beginning of period)
|
|
// ipmt = (FV(rate, period-2, payment, pv, type) - payment) * rate
|
|
let payment = compute_payment(
|
|
rate,
|
|
period_count,
|
|
present_value,
|
|
future_value,
|
|
period_start,
|
|
)?;
|
|
if period < 1.0 || period >= period_count + 1.0 {
|
|
return Err((
|
|
Error::NUM,
|
|
format!("Period must be between 1 and {}", period_count + 1.0),
|
|
));
|
|
}
|
|
if period == 1.0 && period_start {
|
|
Ok(0.0)
|
|
} else {
|
|
let p = if period_start {
|
|
period - 2.0
|
|
} else {
|
|
period - 1.0
|
|
};
|
|
let c = if period_start { -payment } else { 0.0 };
|
|
let fv = compute_future_value(rate, p, payment, present_value, period_start)?;
|
|
Ok((fv + c) * rate)
|
|
}
|
|
}
|
|
|
|
fn compute_ppmt(
|
|
rate: f64,
|
|
period: f64,
|
|
period_count: f64,
|
|
present_value: f64,
|
|
future_value: f64,
|
|
period_start: bool,
|
|
) -> Result<f64, (Error, String)> {
|
|
let payment = compute_payment(
|
|
rate,
|
|
period_count,
|
|
present_value,
|
|
future_value,
|
|
period_start,
|
|
)?;
|
|
// It's a bit unfortunate that the first thing compute_ipmt does is compute_payment again
|
|
let ipmt = compute_ipmt(
|
|
rate,
|
|
period,
|
|
period_count,
|
|
present_value,
|
|
future_value,
|
|
period_start,
|
|
)?;
|
|
Ok(payment - ipmt)
|
|
}
|
|
|
|
// These formulas revolve around compound interest and annuities.
|
|
// The financial functions pv, rate, nper, pmt and fv:
|
|
// rate = interest rate per period
|
|
// nper (number of periods) = loan term
|
|
// pv (present value) = loan amount
|
|
// fv (future value) = cash balance after last payment. Default is 0
|
|
// type = the annuity type indicates when payments are due
|
|
// * 0 (default) Payments are made at the end of the period
|
|
// * 1 Payments are made at the beginning of the period (like a lease or rent)
|
|
// The variable period_start is true if type is 1
|
|
// They are linked by the formulas:
|
|
// If rate != 0
|
|
// $pv*(1+rate)^nper+pmt*(1+rate*type)*((1+rate)^nper-1)/rate+fv=0$
|
|
// If rate = 0
|
|
// $pmt*nper+pv+fv=0$
|
|
// All, except for rate are easily solvable in terms of the others.
|
|
// In these formulas the payment (pmt) is normally negative
|
|
|
|
/// Enum to define different array processing behaviors
|
|
enum ArrayProcessingMode {
|
|
Standard, // Accept single numbers, ignore empty/non-number cells
|
|
StrictWithError(Error), // Accept single numbers, error on empty/non-number with specified error type
|
|
RangeOnlyWithZeros, // Don't accept single numbers, treat empty as 0.0, error on non-number
|
|
}
|
|
|
|
impl Model {
|
|
fn get_array_of_numbers_generic(
|
|
&mut self,
|
|
arg: &Node,
|
|
cell: &CellReferenceIndex,
|
|
accept_number_node: bool,
|
|
handle_empty_cell: impl Fn() -> Result<Option<f64>, CalcResult>,
|
|
handle_non_number_cell: impl Fn() -> Result<Option<f64>, CalcResult>,
|
|
) -> Result<Vec<f64>, CalcResult> {
|
|
let mut values = Vec::new();
|
|
match self.evaluate_node_in_context(arg, *cell) {
|
|
CalcResult::Number(value) if accept_number_node => values.push(value),
|
|
CalcResult::Number(_) => {
|
|
return Err(CalcResult::new_error(
|
|
Error::VALUE,
|
|
*cell,
|
|
"Expected range of numbers".to_string(),
|
|
));
|
|
}
|
|
CalcResult::Range { left, right } => {
|
|
if left.sheet != right.sheet {
|
|
return Err(CalcResult::new_error(
|
|
Error::VALUE,
|
|
*cell,
|
|
"Ranges are in different sheets".to_string(),
|
|
));
|
|
}
|
|
let sheet = left.sheet;
|
|
let row1 = left.row;
|
|
let mut row2 = right.row;
|
|
let column1 = left.column;
|
|
let mut column2 = right.column;
|
|
if row1 == 1 && row2 == LAST_ROW {
|
|
row2 = self
|
|
.workbook
|
|
.worksheet(sheet)
|
|
.map_err(|_| {
|
|
CalcResult::new_error(
|
|
Error::ERROR,
|
|
*cell,
|
|
format!("Invalid worksheet index: '{sheet}'"),
|
|
)
|
|
})?
|
|
.dimension()
|
|
.max_row;
|
|
}
|
|
if column1 == 1 && column2 == LAST_COLUMN {
|
|
column2 = self
|
|
.workbook
|
|
.worksheet(sheet)
|
|
.map_err(|_| {
|
|
CalcResult::new_error(
|
|
Error::ERROR,
|
|
*cell,
|
|
format!("Invalid worksheet index: '{sheet}'"),
|
|
)
|
|
})?
|
|
.dimension()
|
|
.max_column;
|
|
}
|
|
for row in row1..=row2 {
|
|
for column in column1..=column2 {
|
|
let cell_ref = CellReferenceIndex { sheet, row, column };
|
|
match self.evaluate_cell(cell_ref) {
|
|
CalcResult::Number(value) => values.push(value),
|
|
error @ CalcResult::Error { .. } => return Err(error),
|
|
CalcResult::EmptyCell => {
|
|
if let Some(value) = handle_empty_cell()? {
|
|
values.push(value);
|
|
}
|
|
}
|
|
_ => {
|
|
if let Some(value) = handle_non_number_cell()? {
|
|
values.push(value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
error @ CalcResult::Error { .. } => return Err(error),
|
|
_ => {
|
|
handle_non_number_cell()?;
|
|
}
|
|
}
|
|
Ok(values)
|
|
}
|
|
|
|
fn get_array_of_numbers_with_mode(
|
|
&mut self,
|
|
arg: &Node,
|
|
cell: &CellReferenceIndex,
|
|
mode: ArrayProcessingMode,
|
|
) -> Result<Vec<f64>, CalcResult> {
|
|
match mode {
|
|
ArrayProcessingMode::Standard => {
|
|
self.get_array_of_numbers_generic(
|
|
arg,
|
|
cell,
|
|
true, // accept_number_node
|
|
|| Ok(None), // Ignore empty cells
|
|
|| Ok(None), // Ignore non-number cells
|
|
)
|
|
}
|
|
ArrayProcessingMode::StrictWithError(error_type) => {
|
|
self.get_array_of_numbers_generic(
|
|
arg,
|
|
cell,
|
|
true, // accept_number_node
|
|
|| {
|
|
Err(CalcResult::new_error(
|
|
Error::NUM,
|
|
*cell,
|
|
"Expected number".to_string(),
|
|
))
|
|
},
|
|
|| {
|
|
Err(CalcResult::new_error(
|
|
error_type.clone(),
|
|
*cell,
|
|
"Expected number".to_string(),
|
|
))
|
|
},
|
|
)
|
|
}
|
|
ArrayProcessingMode::RangeOnlyWithZeros => {
|
|
self.get_array_of_numbers_generic(
|
|
arg,
|
|
cell,
|
|
false, // Do not accept a single number node
|
|
|| Ok(Some(0.0)), // Treat empty cells as zero
|
|
|| {
|
|
Err(CalcResult::new_error(
|
|
Error::VALUE,
|
|
*cell,
|
|
"Expected number".to_string(),
|
|
))
|
|
},
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn get_array_of_numbers(
|
|
&mut self,
|
|
arg: &Node,
|
|
cell: &CellReferenceIndex,
|
|
) -> Result<Vec<f64>, CalcResult> {
|
|
self.get_array_of_numbers_with_mode(arg, cell, ArrayProcessingMode::Standard)
|
|
}
|
|
|
|
fn get_array_of_numbers_xpnv(
|
|
&mut self,
|
|
arg: &Node,
|
|
cell: &CellReferenceIndex,
|
|
error: Error,
|
|
) -> Result<Vec<f64>, CalcResult> {
|
|
self.get_array_of_numbers_with_mode(arg, cell, ArrayProcessingMode::StrictWithError(error))
|
|
}
|
|
|
|
fn get_array_of_numbers_xirr(
|
|
&mut self,
|
|
arg: &Node,
|
|
cell: &CellReferenceIndex,
|
|
) -> Result<Vec<f64>, CalcResult> {
|
|
self.get_array_of_numbers_with_mode(arg, cell, ArrayProcessingMode::RangeOnlyWithZeros)
|
|
}
|
|
|
|
/// PMT(rate, nper, pv, [fv], [type])
|
|
pub(crate) fn fn_pmt(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
let arg_count = args.len();
|
|
if let Some(err) = validate_arg_count_or_return(arg_count, 3, 5, cell) {
|
|
return err;
|
|
}
|
|
|
|
let params = match parse_required_params(args, &[0, 1, 2], self, cell, false) {
|
|
Ok(p) => p,
|
|
Err(err) => return err,
|
|
};
|
|
let (rate, nper, pv) = (params[0], params[1], params[2]);
|
|
|
|
let optional_params = match parse_financial_optional_params(args, arg_count, 3, self, cell)
|
|
{
|
|
Ok(params) => params,
|
|
Err(err) => return err,
|
|
};
|
|
|
|
match handle_compute_error(
|
|
compute_payment(
|
|
rate,
|
|
nper,
|
|
pv,
|
|
optional_params.optional_value,
|
|
optional_params.period_start,
|
|
),
|
|
cell,
|
|
) {
|
|
Ok(p) => CalcResult::Number(p),
|
|
Err(err) => err,
|
|
}
|
|
}
|
|
|
|
// PV(rate, nper, pmt, [fv], [type])
|
|
pub(crate) fn fn_pv(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
let arg_count = args.len();
|
|
if let Some(err) = validate_arg_count_or_return(arg_count, 3, 5, cell) {
|
|
return err;
|
|
}
|
|
|
|
let params = match parse_required_params(args, &[0, 1, 2], self, cell, false) {
|
|
Ok(p) => p,
|
|
Err(err) => return err,
|
|
};
|
|
let (rate, period_count, payment) = (params[0], params[1], params[2]);
|
|
|
|
let optional_params = match parse_financial_optional_params(args, arg_count, 3, self, cell)
|
|
{
|
|
Ok(params) => params,
|
|
Err(err) => return err,
|
|
};
|
|
if rate == 0.0 {
|
|
return CalcResult::Number(-optional_params.optional_value - payment * period_count);
|
|
}
|
|
if rate == -1.0 {
|
|
return CalcResult::Error {
|
|
error: Error::DIV,
|
|
origin: cell,
|
|
message: "Rate must be != -1".to_string(),
|
|
};
|
|
};
|
|
let rate_nper = (1.0 + rate).powf(period_count);
|
|
let result = if optional_params.period_start {
|
|
// type = 1
|
|
-(optional_params.optional_value * rate + payment * (1.0 + rate) * (rate_nper - 1.0))
|
|
/ (rate * rate_nper)
|
|
} else {
|
|
(-optional_params.optional_value * rate - payment * (rate_nper - 1.0))
|
|
/ (rate * rate_nper)
|
|
};
|
|
if result.is_nan() || result.is_infinite() {
|
|
return CalcResult::Error {
|
|
error: Error::NUM,
|
|
origin: cell,
|
|
message: "Invalid result".to_string(),
|
|
};
|
|
}
|
|
|
|
CalcResult::Number(result)
|
|
}
|
|
|
|
// ACCRINT(issue, first_interest, settlement, rate, par, freq, [basis], [calc])
|
|
pub(crate) fn fn_accrint(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
let arg_count = args.len();
|
|
if let Some(err) = validate_arg_count_or_return(arg_count, 6, 8, cell) {
|
|
return err;
|
|
}
|
|
|
|
let params = match parse_required_params(args, &[0, 1, 2, 3, 4, 5], self, cell, true) {
|
|
Ok(p) => p,
|
|
Err(err) => return err,
|
|
};
|
|
let (issue, first, settlement, rate, par, freq) = (
|
|
params[0],
|
|
params[1],
|
|
params[2],
|
|
params[3],
|
|
params[4],
|
|
params[5] as i32,
|
|
);
|
|
let basis = match parse_optional_basis(args, 6, arg_count, self, cell) {
|
|
Ok(b) => b,
|
|
Err(err) => return err,
|
|
};
|
|
let calc = if arg_count > 7 {
|
|
match self.get_number(&args[7], cell) {
|
|
Ok(f) => f != 0.0,
|
|
Err(s) => return s,
|
|
}
|
|
} else {
|
|
true
|
|
};
|
|
|
|
if !(freq == 1 || freq == 2 || freq == 4) {
|
|
return CalcResult::new_error(Error::NUM, cell, "invalid frequency".to_string());
|
|
}
|
|
if let Err(err) = validate_basis(basis, cell) {
|
|
return err;
|
|
}
|
|
if let Err(err) = validate_non_negative(par, "par", cell) {
|
|
return err;
|
|
}
|
|
if let Err(err) = validate_non_negative(rate, "rate", cell) {
|
|
return err;
|
|
}
|
|
|
|
let issue_d = match convert_date_serial(issue, cell) {
|
|
Ok(d) => d,
|
|
Err(err) => return err,
|
|
};
|
|
let first_d = match from_excel_date(first as i64) {
|
|
Ok(d) => d,
|
|
Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string()),
|
|
};
|
|
let settle_d = match convert_date_serial(settlement, cell) {
|
|
Ok(d) => d,
|
|
Err(err) => return err,
|
|
};
|
|
|
|
if settle_d < issue_d {
|
|
return CalcResult::new_error(Error::NUM, cell, "settlement < issue".to_string());
|
|
}
|
|
if first_d < issue_d {
|
|
return CalcResult::new_error(Error::NUM, cell, "first_interest < issue".to_string());
|
|
}
|
|
if settle_d < first_d {
|
|
return CalcResult::new_error(
|
|
Error::NUM,
|
|
cell,
|
|
"settlement < first_interest".to_string(),
|
|
);
|
|
}
|
|
|
|
let months = 12 / freq;
|
|
let mut prev = first_d;
|
|
if settle_d <= first_d {
|
|
prev = issue_d;
|
|
} else {
|
|
while prev <= settle_d {
|
|
let next = prev + chrono::Months::new(months as u32);
|
|
if next > settle_d {
|
|
break;
|
|
}
|
|
prev = next;
|
|
}
|
|
}
|
|
let next_coupon = prev + chrono::Months::new(months as u32);
|
|
|
|
let mut result = 0.0;
|
|
if calc {
|
|
let mut next = first_d;
|
|
while next < prev {
|
|
result += rate * par / freq as f64;
|
|
next = next + chrono::Months::new(months as u32);
|
|
}
|
|
}
|
|
|
|
let days_in_period = match year_fraction(prev, next_coupon, basis) {
|
|
Ok(f) => f,
|
|
Err(_) => return CalcResult::new_error(Error::NUM, cell, "invalid basis".to_string()),
|
|
};
|
|
let days_elapsed = match year_fraction(prev, settle_d, basis) {
|
|
Ok(f) => f,
|
|
Err(_) => return CalcResult::new_error(Error::NUM, cell, "invalid basis".to_string()),
|
|
};
|
|
|
|
result += rate * par / freq as f64
|
|
* if days_in_period == 0.0 {
|
|
0.0
|
|
} else {
|
|
days_elapsed / days_in_period
|
|
};
|
|
CalcResult::Number(result)
|
|
}
|
|
|
|
// ACCRINTM(issue, settlement, rate, par, [basis])
|
|
pub(crate) fn fn_accrintm(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
let arg_count = args.len();
|
|
if let Some(err) = validate_arg_count_or_return(arg_count, 4, 5, cell) {
|
|
return err;
|
|
}
|
|
|
|
let params = match parse_required_params(args, &[0, 1, 2, 3], self, cell, true) {
|
|
Ok(p) => p,
|
|
Err(err) => return err,
|
|
};
|
|
let (issue, settlement, rate, par) = (params[0], params[1], params[2], params[3]);
|
|
let basis = match parse_optional_basis(args, 4, arg_count, self, cell) {
|
|
Ok(b) => b,
|
|
Err(err) => return err,
|
|
};
|
|
|
|
if let Err(err) = validate_basis(basis, cell) {
|
|
return err;
|
|
}
|
|
if let Err(err) = validate_non_negative(par, "par", cell) {
|
|
return err;
|
|
}
|
|
if let Err(err) = validate_non_negative(rate, "rate", cell) {
|
|
return err;
|
|
}
|
|
|
|
let issue_d = match convert_date_serial(issue, cell) {
|
|
Ok(d) => d,
|
|
Err(err) => return err,
|
|
};
|
|
let settle_d = match convert_date_serial(settlement, cell) {
|
|
Ok(d) => d,
|
|
Err(err) => return err,
|
|
};
|
|
|
|
if settle_d < issue_d {
|
|
return CalcResult::new_error(Error::NUM, cell, "settlement < issue".to_string());
|
|
}
|
|
|
|
let frac = match year_fraction(issue_d, settle_d, basis) {
|
|
Ok(f) => f,
|
|
Err(_) => return CalcResult::new_error(Error::NUM, cell, "invalid basis".to_string()),
|
|
};
|
|
|
|
CalcResult::Number(par * rate * frac)
|
|
}
|
|
|
|
// RATE(nper, pmt, pv, [fv], [type], [guess])
|
|
pub(crate) fn fn_rate(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
let arg_count = args.len();
|
|
if let Some(err) = validate_arg_count_or_return(arg_count, 3, 5, cell) {
|
|
return err;
|
|
}
|
|
|
|
let params = match parse_required_params(args, &[0, 1, 2], self, cell, false) {
|
|
Ok(p) => p,
|
|
Err(err) => return err,
|
|
};
|
|
let (nper, pmt, pv) = (params[0], params[1], params[2]);
|
|
// fv
|
|
let fv = if arg_count > 3 {
|
|
match self.get_number(&args[3], cell) {
|
|
Ok(f) => f,
|
|
Err(s) => return s,
|
|
}
|
|
} else {
|
|
0.0
|
|
};
|
|
let annuity_type = if arg_count > 4 {
|
|
match self.get_number(&args[4], cell) {
|
|
Ok(f) => i32::from(f != 0.0),
|
|
Err(s) => return s,
|
|
}
|
|
} else {
|
|
// at the end of the period
|
|
0
|
|
};
|
|
|
|
let guess = if arg_count > 5 {
|
|
match self.get_number(&args[5], cell) {
|
|
Ok(f) => f,
|
|
Err(s) => return s,
|
|
}
|
|
} else {
|
|
0.1
|
|
};
|
|
|
|
match handle_compute_error(compute_rate(pv, fv, nper, pmt, annuity_type, guess), cell) {
|
|
Ok(f) => CalcResult::Number(f),
|
|
Err(err) => err,
|
|
}
|
|
}
|
|
|
|
// NPER(rate,pmt,pv,[fv],[type])
|
|
pub(crate) fn fn_nper(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
let arg_count = args.len();
|
|
if let Some(err) = validate_arg_count_or_return(arg_count, 3, 5, cell) {
|
|
return err;
|
|
}
|
|
|
|
let params = match parse_required_params(args, &[0, 1, 2], self, cell, false) {
|
|
Ok(p) => p,
|
|
Err(err) => return err,
|
|
};
|
|
let (rate, payment, present_value) = (params[0], params[1], params[2]);
|
|
// fv
|
|
let future_value = if arg_count > 3 {
|
|
match self.get_number(&args[3], cell) {
|
|
Ok(f) => f,
|
|
Err(s) => return s,
|
|
}
|
|
} else {
|
|
0.0
|
|
};
|
|
let period_start = if arg_count > 4 {
|
|
match self.get_number(&args[4], cell) {
|
|
Ok(f) => f != 0.0,
|
|
Err(s) => return s,
|
|
}
|
|
} else {
|
|
// at the end of the period
|
|
false
|
|
};
|
|
if rate == 0.0 {
|
|
if payment == 0.0 {
|
|
return CalcResult::Error {
|
|
error: Error::DIV,
|
|
origin: cell,
|
|
message: "Divide by zero".to_string(),
|
|
};
|
|
}
|
|
return CalcResult::Number(-(future_value + present_value) / payment);
|
|
}
|
|
if rate < -1.0 {
|
|
return CalcResult::Error {
|
|
error: Error::NUM,
|
|
origin: cell,
|
|
message: "Rate must be > -1".to_string(),
|
|
};
|
|
};
|
|
let rate_nper = if period_start {
|
|
// type = 1
|
|
if payment != 0.0 {
|
|
let term = payment * (1.0 + rate) / rate;
|
|
(1.0 - future_value / term) / (1.0 + present_value / term)
|
|
} else {
|
|
-future_value / present_value
|
|
}
|
|
} else {
|
|
// type = 0
|
|
if payment != 0.0 {
|
|
let term = payment / rate;
|
|
(1.0 - future_value / term) / (1.0 + present_value / term)
|
|
} else {
|
|
-future_value / present_value
|
|
}
|
|
};
|
|
if rate_nper <= 0.0 {
|
|
return CalcResult::Error {
|
|
error: Error::NUM,
|
|
origin: cell,
|
|
message: "Cannot compute.".to_string(),
|
|
};
|
|
}
|
|
let result = rate_nper.ln() / (1.0 + rate).ln();
|
|
CalcResult::Number(result)
|
|
}
|
|
|
|
// FV(rate, nper, pmt, [pv], [type])
|
|
pub(crate) fn fn_fv(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
let arg_count = args.len();
|
|
if let Some(err) = validate_arg_count_or_return(arg_count, 3, 5, cell) {
|
|
return err;
|
|
}
|
|
|
|
let params = match parse_required_params(args, &[0, 1, 2], self, cell, false) {
|
|
Ok(p) => p,
|
|
Err(err) => return err,
|
|
};
|
|
let (rate, nper, pmt) = (params[0], params[1], params[2]);
|
|
|
|
let optional_params = match parse_financial_optional_params(args, arg_count, 3, self, cell)
|
|
{
|
|
Ok(params) => params,
|
|
Err(err) => return err,
|
|
};
|
|
|
|
match handle_compute_error(
|
|
compute_future_value(
|
|
rate,
|
|
nper,
|
|
pmt,
|
|
optional_params.optional_value,
|
|
optional_params.period_start,
|
|
),
|
|
cell,
|
|
) {
|
|
Ok(f) => CalcResult::Number(f),
|
|
Err(err) => err,
|
|
}
|
|
}
|
|
|
|
// 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();
|
|
if let Some(err) = validate_arg_count_or_return(arg_count, 4, 6, cell) {
|
|
return err;
|
|
}
|
|
|
|
let params = match parse_required_params(args, &[0, 1, 2, 3], self, cell, false) {
|
|
Ok(p) => p,
|
|
Err(err) => return err,
|
|
};
|
|
let (rate, period, period_count, present_value) =
|
|
(params[0], params[1], params[2], params[3]);
|
|
// fv
|
|
let future_value = if arg_count > 4 {
|
|
match self.get_number(&args[4], cell) {
|
|
Ok(f) => f,
|
|
Err(s) => return s,
|
|
}
|
|
} else {
|
|
0.0
|
|
};
|
|
let period_start = if arg_count > 5 {
|
|
match self.get_number(&args[5], cell) {
|
|
Ok(f) => f != 0.0,
|
|
Err(s) => return s,
|
|
}
|
|
} else {
|
|
// at the end of the period
|
|
false
|
|
};
|
|
let ipmt = match handle_compute_error(
|
|
compute_ipmt(
|
|
rate,
|
|
period,
|
|
period_count,
|
|
present_value,
|
|
future_value,
|
|
period_start,
|
|
),
|
|
cell,
|
|
) {
|
|
Ok(f) => f,
|
|
Err(err) => return err,
|
|
};
|
|
CalcResult::Number(ipmt)
|
|
}
|
|
|
|
// PPMT(rate, per, nper, pv, [fv], [type])
|
|
pub(crate) fn fn_ppmt(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
let arg_count = args.len();
|
|
if let Some(err) = validate_arg_count_or_return(arg_count, 4, 6, cell) {
|
|
return err;
|
|
}
|
|
|
|
let params = match parse_required_params(args, &[0, 1, 2, 3], self, cell, false) {
|
|
Ok(p) => p,
|
|
Err(err) => return err,
|
|
};
|
|
let (rate, period, period_count, present_value) =
|
|
(params[0], params[1], params[2], params[3]);
|
|
// fv
|
|
let future_value = if arg_count > 4 {
|
|
match self.get_number(&args[4], cell) {
|
|
Ok(f) => f,
|
|
Err(s) => return s,
|
|
}
|
|
} else {
|
|
0.0
|
|
};
|
|
let period_start = if arg_count > 5 {
|
|
match self.get_number(&args[5], cell) {
|
|
Ok(f) => f != 0.0,
|
|
Err(s) => return s,
|
|
}
|
|
} else {
|
|
// at the end of the period
|
|
false
|
|
};
|
|
|
|
let ppmt = match handle_compute_error(
|
|
compute_ppmt(
|
|
rate,
|
|
period,
|
|
period_count,
|
|
present_value,
|
|
future_value,
|
|
period_start,
|
|
),
|
|
cell,
|
|
) {
|
|
Ok(f) => f,
|
|
Err(err) => return err,
|
|
};
|
|
CalcResult::Number(ppmt)
|
|
}
|
|
|
|
// NPV(rate, value1, [value2],...)
|
|
// npv = Sum[value[i]/(1+rate)^i, {i, 1, n}]
|
|
pub(crate) fn fn_npv(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
let arg_count = args.len();
|
|
if arg_count < 2 {
|
|
return CalcResult::new_args_number_error(cell);
|
|
}
|
|
let rate = match self.get_number(&args[0], cell) {
|
|
Ok(f) => f,
|
|
Err(s) => return s,
|
|
};
|
|
let mut values = Vec::new();
|
|
for arg in &args[1..] {
|
|
match self.get_array_of_numbers(arg, &cell) {
|
|
Ok(mut arg_values) => values.append(&mut arg_values),
|
|
Err(err) => return err,
|
|
}
|
|
}
|
|
match handle_compute_error(compute_npv(rate, &values), cell) {
|
|
Ok(f) => CalcResult::Number(f),
|
|
Err(err) => err,
|
|
}
|
|
}
|
|
|
|
// Returns the internal rate of return for a series of cash flows represented by the numbers
|
|
// in values.
|
|
// These cash flows do not have to be even, as they would be for an annuity.
|
|
// However, the cash flows must occur at regular intervals, such as monthly or annually.
|
|
// The internal rate of return is the interest rate received for an investment consisting
|
|
// of payments (negative values) and income (positive values) that occur at regular periods
|
|
|
|
// IRR(values, [guess])
|
|
pub(crate) fn fn_irr(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
let arg_count = args.len();
|
|
if arg_count > 2 || arg_count == 0 {
|
|
return CalcResult::new_args_number_error(cell);
|
|
}
|
|
let values = match self.get_array_of_numbers(&args[0], &cell) {
|
|
Ok(s) => s,
|
|
Err(error) => return error,
|
|
};
|
|
let guess = if arg_count == 2 {
|
|
match self.get_number(&args[1], cell) {
|
|
Ok(f) => f,
|
|
Err(s) => return s,
|
|
}
|
|
} else {
|
|
0.1
|
|
};
|
|
match handle_compute_error(compute_irr(&values, guess), cell) {
|
|
Ok(f) => CalcResult::Number(f),
|
|
Err(err) => err,
|
|
}
|
|
}
|
|
|
|
// XNPV(rate, values, dates)
|
|
pub(crate) fn fn_xnpv(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
let arg_count = args.len();
|
|
if !(2..=3).contains(&arg_count) {
|
|
return CalcResult::new_args_number_error(cell);
|
|
}
|
|
let rate = match self.get_number(&args[0], cell) {
|
|
Ok(f) => f,
|
|
Err(s) => return s,
|
|
};
|
|
let values = match self.get_array_of_numbers_xpnv(&args[1], &cell, Error::NUM) {
|
|
Ok(s) => s,
|
|
Err(error) => return error,
|
|
};
|
|
let dates = match self.get_array_of_numbers_xpnv(&args[2], &cell, Error::VALUE) {
|
|
Ok(s) => s,
|
|
Err(error) => return error,
|
|
};
|
|
let dates = match validate_values_dates_arrays(&values, &dates, cell) {
|
|
Ok(d) => d,
|
|
Err(err) => return err,
|
|
};
|
|
// It seems Excel returns #NUM! if rate < 0, this is only necessary if r <= -1
|
|
if rate <= 0.0 {
|
|
return CalcResult::new_error(Error::NUM, cell, "rate needs to be > 0".to_string());
|
|
}
|
|
match handle_compute_error(compute_xnpv(rate, &values, &dates), cell) {
|
|
Ok(f) => CalcResult::Number(f),
|
|
Err(err) => err,
|
|
}
|
|
}
|
|
|
|
// XIRR(values, dates, [guess])
|
|
pub(crate) fn fn_xirr(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
let arg_count = args.len();
|
|
if !(2..=3).contains(&arg_count) {
|
|
return CalcResult::new_args_number_error(cell);
|
|
}
|
|
let values = match self.get_array_of_numbers_xirr(&args[0], &cell) {
|
|
Ok(s) => s,
|
|
Err(error) => return error,
|
|
};
|
|
let dates = match self.get_array_of_numbers_xirr(&args[1], &cell) {
|
|
Ok(s) => s,
|
|
Err(error) => return error,
|
|
};
|
|
let guess = if arg_count == 3 {
|
|
match self.get_number(&args[2], cell) {
|
|
Ok(f) => f,
|
|
Err(s) => return s,
|
|
}
|
|
} else {
|
|
0.1
|
|
};
|
|
let dates = match validate_values_dates_arrays(&values, &dates, cell) {
|
|
Ok(d) => d,
|
|
Err(err) => return err,
|
|
};
|
|
match handle_compute_error(compute_xirr(&values, &dates, guess), cell) {
|
|
Ok(f) => CalcResult::Number(f),
|
|
Err(err) => err,
|
|
}
|
|
}
|
|
|
|
// MIRR(values, finance_rate, reinvest_rate)
|
|
// The formula is:
|
|
// $$ (-NPV(r1, v_p) * (1+r1)^y)/(NPV(r2, v_n)*(1+r2))^(1/y)-1$$
|
|
// where:
|
|
// $r1$ is the reinvest_rate, $r2$ the finance_rate
|
|
// $v_p$ the vector of positive values
|
|
// $v_n$ the vector of negative values
|
|
// and $y$ is dimension of $v$ - 1 (number of years)
|
|
pub(crate) fn fn_mirr(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
if let Some(err) = validate_arg_count_or_return(args.len(), 3, 3, cell) {
|
|
return err;
|
|
}
|
|
|
|
let values = match self.get_array_of_numbers(&args[0], &cell) {
|
|
Ok(s) => s,
|
|
Err(error) => return error,
|
|
};
|
|
let params = match parse_required_params(args, &[1, 2], self, cell, false) {
|
|
Ok(p) => p,
|
|
Err(err) => return err,
|
|
};
|
|
let (finance_rate, reinvest_rate) = (params[0], params[1]);
|
|
let mut positive_values = Vec::new();
|
|
let mut negative_values = Vec::new();
|
|
let mut last_negative_index = -1;
|
|
for (index, &value) in values.iter().enumerate() {
|
|
let (p, n) = if value >= 0.0 {
|
|
(value, 0.0)
|
|
} else {
|
|
last_negative_index = index as i32;
|
|
(0.0, value)
|
|
};
|
|
positive_values.push(p);
|
|
negative_values.push(n);
|
|
}
|
|
if last_negative_index == -1 {
|
|
return CalcResult::new_error(
|
|
Error::DIV,
|
|
cell,
|
|
"Invalid data for MIRR function".to_string(),
|
|
);
|
|
}
|
|
// We do a bit of analysis if the rates are -1 as there are some cancellations
|
|
// It is probably not important.
|
|
let years = values.len() as f64;
|
|
let top = if reinvest_rate == -1.0 {
|
|
// This is finite
|
|
match positive_values.last() {
|
|
Some(f) => *f,
|
|
None => 0.0,
|
|
}
|
|
} else {
|
|
match handle_compute_error(compute_npv(reinvest_rate, &positive_values), cell) {
|
|
Ok(npv) => -npv * ((1.0 + reinvest_rate).powf(years)),
|
|
Err(err) => return err,
|
|
}
|
|
};
|
|
let bottom = if finance_rate == -1.0 {
|
|
if last_negative_index == 0 {
|
|
// This is still finite
|
|
negative_values[last_negative_index as usize]
|
|
} else {
|
|
// or -Infinity depending of the sign in the last_negative_index coef.
|
|
// But it is irrelevant for the calculation
|
|
f64::INFINITY
|
|
}
|
|
} else {
|
|
match handle_compute_error(compute_npv(finance_rate, &negative_values), cell) {
|
|
Ok(npv) => npv * (1.0 + finance_rate),
|
|
Err(err) => return err,
|
|
}
|
|
};
|
|
|
|
let result = (top / bottom).powf(1.0 / (years - 1.0)) - 1.0;
|
|
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 data for MIRR".to_string());
|
|
}
|
|
CalcResult::Number(result)
|
|
}
|
|
|
|
// ISPMT(rate, per, nper, pv)
|
|
// Formula is:
|
|
// $$pv*rate*\left(\frac{per}{nper}-1\right)$$
|
|
pub(crate) fn fn_ispmt(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
if let Some(err) = validate_arg_count_or_return(args.len(), 4, 4, cell) {
|
|
return err;
|
|
}
|
|
|
|
let params = match parse_required_params(args, &[0, 1, 2, 3], self, cell, false) {
|
|
Ok(p) => p,
|
|
Err(err) => return err,
|
|
};
|
|
let (rate, per, nper, pv) = (params[0], params[1], params[2], params[3]);
|
|
if nper == 0.0 {
|
|
return CalcResult::new_error(Error::DIV, cell, "Division by 0".to_string());
|
|
}
|
|
CalcResult::Number(pv * rate * (per / nper - 1.0))
|
|
}
|
|
|
|
// RRI(nper, pv, fv)
|
|
// Formula is
|
|
// $$ \left(\frac{fv}{pv}\right)^{\frac{1}{nper}}-1 $$
|
|
pub(crate) fn fn_rri(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
if let Some(err) = validate_arg_count_or_return(args.len(), 3, 3, cell) {
|
|
return err;
|
|
}
|
|
|
|
let params = match parse_required_params(args, &[0, 1, 2], self, cell, false) {
|
|
Ok(p) => p,
|
|
Err(err) => return err,
|
|
};
|
|
let (nper, pv, fv) = (params[0], params[1], params[2]);
|
|
if nper <= 0.0 {
|
|
return CalcResult::new_error(Error::NUM, cell, "nper should be >0".to_string());
|
|
}
|
|
if pv == 0.0 {
|
|
// Note error is NUM not DIV/0 also bellow
|
|
return CalcResult::new_error(Error::NUM, cell, "Division by 0".to_string());
|
|
}
|
|
let result = (fv / pv).powf(1.0 / nper) - 1.0;
|
|
if result.is_infinite() {
|
|
return CalcResult::new_error(Error::NUM, cell, "Division by 0".to_string());
|
|
}
|
|
if result.is_nan() {
|
|
return CalcResult::new_error(Error::NUM, cell, "Invalid data for RRI".to_string());
|
|
}
|
|
|
|
CalcResult::Number(result)
|
|
}
|
|
|
|
// SLN(cost, salvage, life)
|
|
// Formula is:
|
|
// $$ \frac{cost-salvage}{life} $$
|
|
pub(crate) fn fn_sln(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
if let Some(err) = validate_arg_count_or_return(args.len(), 3, 3, cell) {
|
|
return err;
|
|
}
|
|
|
|
let params = match parse_required_params(args, &[0, 1, 2], self, cell, false) {
|
|
Ok(p) => p,
|
|
Err(err) => return err,
|
|
};
|
|
let (cost, salvage, life) = (params[0], params[1], params[2]);
|
|
if life == 0.0 {
|
|
return CalcResult::new_error(Error::DIV, cell, "Division by 0".to_string());
|
|
}
|
|
let result = (cost - salvage) / life;
|
|
|
|
CalcResult::Number(result)
|
|
}
|
|
|
|
// SYD(cost, salvage, life, per)
|
|
// Formula is:
|
|
// $$ \frac{(cost-salvage)*(life-per+1)*2}{life*(life+1)} $$
|
|
pub(crate) fn fn_syd(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
if let Some(err) = validate_arg_count_or_return(args.len(), 4, 4, cell) {
|
|
return err;
|
|
}
|
|
|
|
let params = match parse_required_params(args, &[0, 1, 2, 3], self, cell, false) {
|
|
Ok(p) => p,
|
|
Err(err) => return err,
|
|
};
|
|
let (cost, salvage, life, per) = (params[0], params[1], params[2], params[3]);
|
|
if life == 0.0 {
|
|
return CalcResult::new_error(Error::NUM, cell, "Division by 0".to_string());
|
|
}
|
|
if per > life || per <= 0.0 {
|
|
return CalcResult::new_error(Error::NUM, cell, "per should be <= life".to_string());
|
|
}
|
|
let result = ((cost - salvage) * (life - per + 1.0) * 2.0) / (life * (life + 1.0));
|
|
|
|
CalcResult::Number(result)
|
|
}
|
|
|
|
// NOMINAL(effective_rate, npery)
|
|
// Formula is:
|
|
// $$ n\times\left(\left(1+r\right)^{\frac{1}{n}}-1\right) $$
|
|
// where:
|
|
// $r$ is the effective interest rate
|
|
// $n$ is the number of periods per year
|
|
pub(crate) fn fn_nominal(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
if let Some(err) = validate_arg_count_or_return(args.len(), 2, 2, cell) {
|
|
return err;
|
|
}
|
|
|
|
let params = match parse_required_params(args, &[0, 1], self, cell, true) {
|
|
Ok(p) => p,
|
|
Err(err) => return err,
|
|
};
|
|
let (effect_rate, npery) = (params[0], params[1].floor());
|
|
if effect_rate <= 0.0 || npery < 1.0 {
|
|
return CalcResult::new_error(Error::NUM, cell, "Invalid arguments".to_string());
|
|
}
|
|
let result = ((1.0 + effect_rate).powf(1.0 / npery) - 1.0) * npery;
|
|
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 data for RRI".to_string());
|
|
}
|
|
|
|
CalcResult::Number(result)
|
|
}
|
|
|
|
// EFFECT(nominal_rate, npery)
|
|
// Formula is:
|
|
// $$ \left(1+\frac{r}{n}\right)^n-1 $$
|
|
// where:
|
|
// $r$ is the nominal interest rate
|
|
// $n$ is the number of periods per year
|
|
pub(crate) fn fn_effect(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
if let Some(err) = validate_arg_count_or_return(args.len(), 2, 2, cell) {
|
|
return err;
|
|
}
|
|
|
|
let params = match parse_required_params(args, &[0, 1], self, cell, true) {
|
|
Ok(p) => p,
|
|
Err(err) => return err,
|
|
};
|
|
let (nominal_rate, npery) = (params[0], params[1].floor());
|
|
if nominal_rate <= 0.0 || npery < 1.0 {
|
|
return CalcResult::new_error(Error::NUM, cell, "Invalid arguments".to_string());
|
|
}
|
|
let result = (1.0 + nominal_rate / npery).powf(npery) - 1.0;
|
|
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 data for RRI".to_string());
|
|
}
|
|
|
|
CalcResult::Number(result)
|
|
}
|
|
|
|
// PDURATION(rate, pv, fv)
|
|
// Formula is:
|
|
// $$ \frac{log(fv) - log(pv)}{log(1+r)} $$
|
|
// where:
|
|
// * $r$ is the interest rate per period
|
|
// * $pv$ is the present value of the investment
|
|
// * $fv$ is the desired future value of the investment
|
|
pub(crate) fn fn_pduration(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
if let Some(err) = validate_arg_count_or_return(args.len(), 3, 3, cell) {
|
|
return err;
|
|
}
|
|
|
|
let params = match parse_required_params(args, &[0, 1, 2], self, cell, false) {
|
|
Ok(p) => p,
|
|
Err(err) => return err,
|
|
};
|
|
let (rate, pv, fv) = (params[0], params[1], params[2]);
|
|
if fv <= 0.0 || pv <= 0.0 || rate <= 0.0 {
|
|
return CalcResult::new_error(Error::NUM, cell, "Invalid arguments".to_string());
|
|
}
|
|
let result = (fv.ln() - pv.ln()) / ((1.0 + rate).ln());
|
|
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 data for RRI".to_string());
|
|
}
|
|
|
|
CalcResult::Number(result)
|
|
}
|
|
|
|
// DURATION(settlement, maturity, coupon, yld, freq, [basis])
|
|
pub(crate) fn fn_duration(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
let arg_count = args.len();
|
|
if let Some(err) = validate_arg_count_or_return(arg_count, 5, 6, cell) {
|
|
return err;
|
|
}
|
|
|
|
let params = match parse_required_params(args, &[0, 1, 2, 3, 4], self, cell, true) {
|
|
Ok(p) => p,
|
|
Err(err) => return err,
|
|
};
|
|
let (settlement, maturity, coupon, yld, freq) = (
|
|
params[0],
|
|
params[1],
|
|
params[2],
|
|
params[3],
|
|
params[4].trunc() as i32,
|
|
);
|
|
let basis = match parse_optional_basis(args, 5, arg_count, self, cell) {
|
|
Ok(b) => b,
|
|
Err(err) => return err,
|
|
};
|
|
if settlement >= maturity || coupon < 0.0 || yld < 0.0 || !matches!(freq, 1 | 2 | 4) {
|
|
return CalcResult::new_error(Error::NUM, cell, "Invalid arguments".to_string());
|
|
}
|
|
|
|
let days_in_year = days_in_year_simple(basis);
|
|
let diff_days = maturity - settlement;
|
|
if diff_days <= 0.0 {
|
|
return CalcResult::new_error(Error::NUM, cell, "Invalid arguments".to_string());
|
|
}
|
|
let yearfrac = diff_days / days_in_year;
|
|
let mut num_coupons = (yearfrac * freq as f64).ceil();
|
|
if num_coupons < 1.0 {
|
|
num_coupons = 1.0;
|
|
}
|
|
|
|
let cf = coupon * 100.0 / freq as f64;
|
|
let y = 1.0 + yld / freq as f64;
|
|
let ndiff = yearfrac * freq as f64 - num_coupons;
|
|
let mut dur = 0.0;
|
|
for t in 1..(num_coupons as i32) {
|
|
let tt = t as f64 + ndiff;
|
|
dur += tt * cf / y.powf(tt);
|
|
}
|
|
let last_t = num_coupons + ndiff;
|
|
dur += last_t * (cf + 100.0) / y.powf(last_t);
|
|
|
|
let mut price = 0.0;
|
|
for t in 1..(num_coupons as i32) {
|
|
let tt = t as f64 + ndiff;
|
|
price += cf / y.powf(tt);
|
|
}
|
|
price += (cf + 100.0) / y.powf(last_t);
|
|
|
|
if price == 0.0 {
|
|
return CalcResult::new_error(Error::DIV, cell, "Division by 0".to_string());
|
|
}
|
|
|
|
let result = (dur / price) / freq as f64;
|
|
CalcResult::Number(result)
|
|
}
|
|
|
|
// MDURATION(settlement, maturity, coupon, yld, freq, [basis])
|
|
pub(crate) fn fn_mduration(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
let mut res = self.fn_duration(args, cell);
|
|
if let CalcResult::Number(ref mut d) = res {
|
|
let yld = match self.get_number_no_bools(&args[3], cell) {
|
|
Ok(f) => f,
|
|
Err(_) => {
|
|
return CalcResult::new_error(
|
|
Error::VALUE,
|
|
cell,
|
|
"Invalid arguments".to_string(),
|
|
)
|
|
}
|
|
};
|
|
let freq = match self.get_number_no_bools(&args[4], cell) {
|
|
Ok(f) => f.trunc(),
|
|
Err(_) => {
|
|
return CalcResult::new_error(
|
|
Error::VALUE,
|
|
cell,
|
|
"Invalid arguments".to_string(),
|
|
)
|
|
}
|
|
};
|
|
*d /= 1.0 + yld / freq;
|
|
}
|
|
res
|
|
}
|
|
|
|
// This next three functions deal with Treasure Bills or T-Bills for short
|
|
// They are zero-coupon that mature in one year or less.
|
|
// Definitions:
|
|
// $r$ be the discount rate
|
|
// $v$ the face value of the Bill
|
|
// $p$ the price of the Bill
|
|
// $d_m$ is the number of days from the settlement to maturity
|
|
// Then:
|
|
// $$ p = v \times\left(1-\frac{d_m}{r}\right) $$
|
|
// If d_m is less than 183 days the he Bond Equivalent Yield (BEY, here $y$) is given by:
|
|
// $$ y = \frac{F - B}{M}\times \frac{365}{d_m} = \frac{365\times r}{360-r\times d_m}
|
|
// If d_m>= 183 days things are a bit more complicated.
|
|
// Let $d_e = d_m - 365/2$ if $d_m <= 365$ or $d_e = 183$ if $d_m = 366$.
|
|
// $$ v = p\times \left(1+\frac{y}{2}\right)\left(1+d_e\times\frac{y}{365}\right) $$
|
|
// Together with the previous relation of $p$ and $v$ gives us a quadratic equation for $y$.
|
|
|
|
// TBILLEQ(settlement, maturity, discount)
|
|
pub(crate) fn fn_tbilleq(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
if args.len() != 3 {
|
|
return CalcResult::new_args_number_error(cell);
|
|
}
|
|
|
|
let (days_to_maturity, discount) = match parse_tbill_params(args, self, cell) {
|
|
Ok(params) => params,
|
|
Err(err) => return err,
|
|
};
|
|
|
|
let result = if days_to_maturity < TBILL_MATURITY_THRESHOLD {
|
|
DAYS_ACTUAL as f64 * discount / (DAYS_IN_YEAR_360 as f64 - discount * days_to_maturity)
|
|
} else {
|
|
// Equation here is:
|
|
// (1-days*rate/360)*(1+y/2)*(1+d_extra*y/year)=1
|
|
let year = if days_to_maturity == DAYS_LEAP_YEAR as f64 {
|
|
DAYS_LEAP_YEAR as f64
|
|
} else {
|
|
DAYS_ACTUAL as f64
|
|
};
|
|
let d_extra = days_to_maturity - year / 2.0;
|
|
let alpha = 1.0 - days_to_maturity * discount / DAYS_IN_YEAR_360 as f64;
|
|
let beta = 0.5 + d_extra / year;
|
|
// ay^2+by+c=0
|
|
let a = d_extra * alpha / (year * 2.0);
|
|
let b = alpha * beta;
|
|
let c = alpha - 1.0;
|
|
(-b + (b * b - 4.0 * a * c).sqrt()) / (2.0 * a)
|
|
};
|
|
|
|
validate_tbill_result(result, cell)
|
|
}
|
|
|
|
// TBILLPRICE(settlement, maturity, discount)
|
|
pub(crate) fn fn_tbillprice(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
if args.len() != 3 {
|
|
return CalcResult::new_args_number_error(cell);
|
|
}
|
|
|
|
let (days_to_maturity, discount) = match parse_tbill_params(args, self, cell) {
|
|
Ok(params) => params,
|
|
Err(err) => return err,
|
|
};
|
|
|
|
let result = 100.0 * (1.0 - discount * days_to_maturity / DAYS_IN_YEAR_360 as f64);
|
|
|
|
// TBILLPRICE specifically checks for negative results (prices can't be negative)
|
|
if result < 0.0 {
|
|
return CalcResult::new_error(Error::NUM, cell, "Invalid data for RRI".to_string());
|
|
}
|
|
|
|
validate_tbill_result(result, cell)
|
|
}
|
|
|
|
// TBILLYIELD(settlement, maturity, pr)
|
|
pub(crate) fn fn_tbillyield(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
if args.len() != 3 {
|
|
return CalcResult::new_args_number_error(cell);
|
|
}
|
|
|
|
let (days, price) = match parse_tbill_params(args, self, cell) {
|
|
Ok(params) => params,
|
|
Err(err) => return err,
|
|
};
|
|
|
|
let result = (100.0 - price) * DAYS_IN_YEAR_360 as f64 / (price * days);
|
|
|
|
validate_tbill_result(result, cell)
|
|
}
|
|
|
|
pub(crate) fn fn_price(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
let params = match parse_and_validate_bond_pricing_params(args, self, cell) {
|
|
Ok(p) => p,
|
|
Err(err) => return err,
|
|
};
|
|
|
|
let r = params.third_param / params.frequency as f64; // yld / frequency
|
|
let mut price = 0.0;
|
|
for i in 1..=(params.periods as i32) {
|
|
price += params.coupon / (1.0 + r).powf(i as f64);
|
|
}
|
|
price += params.redemption / (1.0 + r).powf(params.periods);
|
|
if price.is_nan() || price.is_infinite() {
|
|
return CalcResult::new_error(Error::NUM, cell, "Invalid data".to_string());
|
|
}
|
|
CalcResult::Number(price)
|
|
}
|
|
|
|
pub(crate) fn fn_pricedisc(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
financial_function_with_year_frac!(
|
|
args, self, cell,
|
|
param1_name: "discount rate",
|
|
param2_name: "redemption value",
|
|
validator: |discount_rate, redemption_value| {
|
|
if discount_rate <= 0.0 || redemption_value <= 0.0 {
|
|
Err("values must be positive".to_string())
|
|
} else {
|
|
Ok(())
|
|
}
|
|
},
|
|
formula: |_settlement, _maturity, discount_rate, redemption_value, _basis, year_frac| {
|
|
redemption_value * (1.0 - discount_rate * year_frac)
|
|
}
|
|
)
|
|
}
|
|
|
|
pub(crate) fn fn_pricemat(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
let arg_count = args.len();
|
|
if let Some(err) = validate_arg_count_or_return(arg_count, 5, 6, cell) {
|
|
return err;
|
|
}
|
|
|
|
let params = match parse_required_params(args, &[0, 1, 2, 3, 4], self, cell, true) {
|
|
Ok(p) => p,
|
|
Err(err) => return err,
|
|
};
|
|
let (settlement, maturity, issue, rate, yld) =
|
|
(params[0], params[1], params[2], params[3], params[4]);
|
|
let basis = if arg_count == 6 {
|
|
match self.get_number_no_bools(&args[5], cell) {
|
|
Ok(f) => f,
|
|
Err(s) => return s,
|
|
}
|
|
} else {
|
|
0.0
|
|
};
|
|
if rate < 0.0 || yld < 0.0 || settlement >= maturity {
|
|
return CalcResult::new_error(Error::NUM, cell, "invalid parameters".to_string());
|
|
}
|
|
if settlement < MINIMUM_DATE_SERIAL_NUMBER as f64
|
|
|| maturity > MAXIMUM_DATE_SERIAL_NUMBER as f64
|
|
|| settlement > MAXIMUM_DATE_SERIAL_NUMBER as f64
|
|
|| maturity < MINIMUM_DATE_SERIAL_NUMBER as f64
|
|
|| issue < MINIMUM_DATE_SERIAL_NUMBER as f64
|
|
|| issue > MAXIMUM_DATE_SERIAL_NUMBER as f64
|
|
{
|
|
return CalcResult::new_error(Error::NUM, cell, "Invalid number for date".to_string());
|
|
}
|
|
let issue_to_maturity_frac = match year_frac(issue as i64, maturity as i64, basis as i32) {
|
|
Ok(f) => f,
|
|
Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string()),
|
|
};
|
|
let issue_to_settlement_frac =
|
|
match year_frac(issue as i64, settlement as i64, basis as i32) {
|
|
Ok(f) => f,
|
|
Err(_) => {
|
|
return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string())
|
|
}
|
|
};
|
|
let settlement_to_maturity_frac =
|
|
match year_frac(settlement as i64, maturity as i64, basis as i32) {
|
|
Ok(f) => f,
|
|
Err(_) => {
|
|
return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string())
|
|
}
|
|
};
|
|
let mut result = 1.0 + issue_to_maturity_frac * rate;
|
|
result /= 1.0 + settlement_to_maturity_frac * yld;
|
|
result -= issue_to_settlement_frac * rate;
|
|
result *= 100.0;
|
|
CalcResult::Number(result)
|
|
}
|
|
|
|
pub(crate) fn fn_yield(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
let params = match parse_and_validate_bond_pricing_params(args, self, cell) {
|
|
Ok(p) => p,
|
|
Err(err) => return err,
|
|
};
|
|
|
|
match handle_compute_error(
|
|
compute_rate(
|
|
-params.third_param,
|
|
params.redemption,
|
|
params.periods,
|
|
params.coupon,
|
|
0,
|
|
0.1,
|
|
),
|
|
cell,
|
|
) {
|
|
Ok(r) => CalcResult::Number(r * params.frequency as f64),
|
|
Err(err) => err,
|
|
}
|
|
}
|
|
|
|
pub(crate) fn fn_yielddisc(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
financial_function_with_year_frac!(
|
|
args, self, cell,
|
|
param1_name: "price",
|
|
param2_name: "redemption value",
|
|
validator: |price, redemption_value| {
|
|
if price <= 0.0 || redemption_value <= 0.0 {
|
|
Err("values must be positive".to_string())
|
|
} else {
|
|
Ok(())
|
|
}
|
|
},
|
|
formula: |_settlement, _maturity, price, redemption_value, _basis, year_frac| {
|
|
(redemption_value / price - 1.0) / year_frac
|
|
}
|
|
)
|
|
}
|
|
|
|
pub(crate) fn fn_yieldmat(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
let arg_count = args.len();
|
|
if let Some(err) = validate_arg_count_or_return(arg_count, 5, 6, cell) {
|
|
return err;
|
|
}
|
|
|
|
let params = match parse_required_params(args, &[0, 1, 2, 3, 4], self, cell, true) {
|
|
Ok(p) => p,
|
|
Err(err) => return err,
|
|
};
|
|
let (settlement, maturity, issue, rate, price) =
|
|
(params[0], params[1], params[2], params[3], params[4]);
|
|
let basis = if arg_count == 6 {
|
|
match self.get_number_no_bools(&args[5], cell) {
|
|
Ok(f) => f,
|
|
Err(s) => return s,
|
|
}
|
|
} else {
|
|
0.0
|
|
};
|
|
if price <= 0.0 || rate < 0.0 || settlement >= maturity || settlement < issue {
|
|
return CalcResult::new_error(Error::NUM, cell, "invalid parameters".to_string());
|
|
}
|
|
if settlement < MINIMUM_DATE_SERIAL_NUMBER as f64
|
|
|| maturity > MAXIMUM_DATE_SERIAL_NUMBER as f64
|
|
|| settlement > MAXIMUM_DATE_SERIAL_NUMBER as f64
|
|
|| maturity < MINIMUM_DATE_SERIAL_NUMBER as f64
|
|
|| issue < MINIMUM_DATE_SERIAL_NUMBER as f64
|
|
|| issue > MAXIMUM_DATE_SERIAL_NUMBER as f64
|
|
{
|
|
return CalcResult::new_error(Error::NUM, cell, "Invalid number for date".to_string());
|
|
}
|
|
let issue_to_maturity_frac = match year_frac(issue as i64, maturity as i64, basis as i32) {
|
|
Ok(f) => f,
|
|
Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string()),
|
|
};
|
|
let issue_to_settlement_frac =
|
|
match year_frac(issue as i64, settlement as i64, basis as i32) {
|
|
Ok(f) => f,
|
|
Err(_) => {
|
|
return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string())
|
|
}
|
|
};
|
|
let settlement_to_maturity_frac =
|
|
match year_frac(settlement as i64, maturity as i64, basis as i32) {
|
|
Ok(f) => f,
|
|
Err(_) => {
|
|
return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string())
|
|
}
|
|
};
|
|
let mut y = 1.0 + issue_to_maturity_frac * rate;
|
|
y /= price / 100.0 + issue_to_settlement_frac * rate;
|
|
y -= 1.0;
|
|
y /= settlement_to_maturity_frac;
|
|
CalcResult::Number(y)
|
|
}
|
|
|
|
// DISC(settlement, maturity, pr, redemption, [basis])
|
|
pub(crate) fn fn_disc(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
financial_function_with_year_frac!(
|
|
args, self, cell,
|
|
param1_name: "price",
|
|
param2_name: "redemption value",
|
|
validator: |price, redemption_value| {
|
|
if price <= 0.0 || redemption_value <= 0.0 {
|
|
Err("values must be positive".to_string())
|
|
} else {
|
|
Ok(())
|
|
}
|
|
},
|
|
formula: |_settlement, _maturity, price, redemption_value, _basis, year_frac| {
|
|
(1.0 - price / redemption_value) / year_frac
|
|
}
|
|
)
|
|
}
|
|
|
|
// RECEIVED(settlement, maturity, investment, discount, [basis])
|
|
pub(crate) fn fn_received(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
financial_function_with_year_frac!(
|
|
args, self, cell,
|
|
param1_name: "investment",
|
|
param2_name: "discount rate",
|
|
validator: |investment, discount_rate| {
|
|
if investment <= 0.0 || discount_rate <= 0.0 {
|
|
Err("values must be positive".to_string())
|
|
} else {
|
|
Ok(())
|
|
}
|
|
},
|
|
formula: |_settlement, _maturity, investment, discount_rate, _basis, year_frac| {
|
|
investment / (1.0 - discount_rate * year_frac)
|
|
}
|
|
)
|
|
}
|
|
|
|
// INTRATE(settlement, maturity, investment, redemption, [basis])
|
|
pub(crate) fn fn_intrate(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
financial_function_with_year_frac!(
|
|
args, self, cell,
|
|
param1_name: "investment",
|
|
param2_name: "redemption value",
|
|
validator: |investment, redemption_value| {
|
|
if investment <= 0.0 || redemption_value <= 0.0 {
|
|
Err("values must be positive".to_string())
|
|
} else {
|
|
Ok(())
|
|
}
|
|
},
|
|
formula: |_settlement, _maturity, investment, redemption_value, _basis, year_frac| {
|
|
((redemption_value / investment) - 1.0) / year_frac
|
|
}
|
|
)
|
|
}
|
|
|
|
// COUPDAYBS(settlement, maturity, frequency, [basis])
|
|
pub(crate) fn fn_coupdaybs(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
let params = match parse_and_validate_coupon_params(args, args.len(), self, cell) {
|
|
Ok(p) => p,
|
|
Err(err) => return err,
|
|
};
|
|
|
|
let (prev_coupon_date, _) = coupon_dates(
|
|
params.settlement_date,
|
|
params.maturity_date,
|
|
params.frequency,
|
|
);
|
|
let days = days_between_dates(prev_coupon_date, params.settlement_date, params.basis);
|
|
CalcResult::Number(days as f64)
|
|
}
|
|
|
|
// COUPDAYS(settlement, maturity, frequency, [basis])
|
|
pub(crate) fn fn_coupdays(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
let params = match parse_and_validate_coupon_params(args, args.len(), self, cell) {
|
|
Ok(p) => p,
|
|
Err(err) => return err,
|
|
};
|
|
|
|
let (prev_coupon_date, next_coupon_date) = coupon_dates(
|
|
params.settlement_date,
|
|
params.maturity_date,
|
|
params.frequency,
|
|
);
|
|
let days = match params.basis {
|
|
0 | 4 => DAYS_IN_YEAR_360 / params.frequency, // 30/360 conventions
|
|
_ => days_between_dates(prev_coupon_date, next_coupon_date, params.basis), // Actual day counts
|
|
};
|
|
CalcResult::Number(days as f64)
|
|
}
|
|
|
|
// COUPDAYSNC(settlement, maturity, frequency, [basis])
|
|
pub(crate) fn fn_coupdaysnc(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
let params = match parse_and_validate_coupon_params(args, args.len(), self, cell) {
|
|
Ok(p) => p,
|
|
Err(err) => return err,
|
|
};
|
|
|
|
let (_, next_coupon_date) = coupon_dates(
|
|
params.settlement_date,
|
|
params.maturity_date,
|
|
params.frequency,
|
|
);
|
|
let days = days_between_dates(params.settlement_date, next_coupon_date, params.basis);
|
|
CalcResult::Number(days as f64)
|
|
}
|
|
|
|
// COUPNCD(settlement, maturity, frequency, [basis])
|
|
pub(crate) fn fn_coupncd(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
let params = match parse_and_validate_coupon_params(args, args.len(), self, cell) {
|
|
Ok(p) => p,
|
|
Err(err) => return err,
|
|
};
|
|
|
|
let (_, next_coupon_date) = coupon_dates(
|
|
params.settlement_date,
|
|
params.maturity_date,
|
|
params.frequency,
|
|
);
|
|
date_to_serial_with_validation(next_coupon_date, cell)
|
|
}
|
|
|
|
// COUPNUM(settlement, maturity, frequency, [basis])
|
|
pub(crate) fn fn_coupnum(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
let params = match parse_and_validate_coupon_params(args, args.len(), self, cell) {
|
|
Ok(p) => p,
|
|
Err(err) => return err,
|
|
};
|
|
|
|
let months = 12 / params.frequency;
|
|
let step = chrono::Months::new(months as u32);
|
|
let mut date = params.maturity_date;
|
|
let mut count = 0;
|
|
while params.settlement_date < date {
|
|
count += 1;
|
|
date = match date.checked_sub_months(step) {
|
|
Some(new_date) => new_date,
|
|
None => break, // Safety check to avoid infinite loop
|
|
};
|
|
}
|
|
CalcResult::Number(count as f64)
|
|
}
|
|
|
|
// COUPPCD(settlement, maturity, frequency, [basis])
|
|
pub(crate) fn fn_couppcd(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
let params = match parse_and_validate_coupon_params(args, args.len(), self, cell) {
|
|
Ok(p) => p,
|
|
Err(err) => return err,
|
|
};
|
|
|
|
let (prev_coupon_date, _) = coupon_dates(
|
|
params.settlement_date,
|
|
params.maturity_date,
|
|
params.frequency,
|
|
);
|
|
date_to_serial_with_validation(prev_coupon_date, cell)
|
|
}
|
|
|
|
// DOLLARDE(fractional_dollar, fraction)
|
|
pub(crate) fn fn_dollarde(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
if let Some(err) = validate_arg_count_or_return(args.len(), 2, 2, cell) {
|
|
return err;
|
|
}
|
|
|
|
let params = match parse_required_params(args, &[0, 1], self, cell, true) {
|
|
Ok(p) => p,
|
|
Err(err) => return err,
|
|
};
|
|
let (fractional_dollar, raw_fraction) = (params[0], params[1]);
|
|
let fraction = match validate_and_normalize_fraction(raw_fraction, cell) {
|
|
Ok(f) => f,
|
|
Err(err) => return err,
|
|
};
|
|
|
|
let t = fractional_dollar.trunc();
|
|
let result = t + (fractional_dollar - t) * 10.0 / fraction;
|
|
CalcResult::Number(result)
|
|
}
|
|
|
|
// DOLLARFR(decimal_dollar, fraction)
|
|
pub(crate) fn fn_dollarfr(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
if let Some(err) = validate_arg_count_or_return(args.len(), 2, 2, cell) {
|
|
return err;
|
|
}
|
|
|
|
let params = match parse_required_params(args, &[0, 1], self, cell, true) {
|
|
Ok(p) => p,
|
|
Err(err) => return err,
|
|
};
|
|
let (decimal_dollar, raw_fraction) = (params[0], params[1]);
|
|
let fraction = match validate_and_normalize_fraction(raw_fraction, cell) {
|
|
Ok(f) => f,
|
|
Err(err) => return err,
|
|
};
|
|
|
|
let t = decimal_dollar.trunc();
|
|
let result = t + (decimal_dollar - t) * fraction / 10.0;
|
|
CalcResult::Number(result)
|
|
}
|
|
|
|
// CUMIPMT(rate, nper, pv, start_period, end_period, type)
|
|
pub(crate) fn fn_cumipmt(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
let params = match parse_and_validate_cumulative_payment_params(args, self, cell) {
|
|
Ok(p) => p,
|
|
Err(err) => return err,
|
|
};
|
|
let mut result = 0.0;
|
|
for period in params.start_period..=params.end_period {
|
|
result += match handle_compute_error(
|
|
compute_ipmt(
|
|
params.rate,
|
|
period as f64,
|
|
params.nper,
|
|
params.pv,
|
|
0.0,
|
|
params.period_type,
|
|
),
|
|
cell,
|
|
) {
|
|
Ok(f) => f,
|
|
Err(err) => return err,
|
|
}
|
|
}
|
|
CalcResult::Number(result)
|
|
}
|
|
|
|
// CUMPRINC(rate, nper, pv, start_period, end_period, type)
|
|
pub(crate) fn fn_cumprinc(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
let params = match parse_and_validate_cumulative_payment_params(args, self, cell) {
|
|
Ok(p) => p,
|
|
Err(err) => return err,
|
|
};
|
|
let mut result = 0.0;
|
|
for period in params.start_period..=params.end_period {
|
|
result += match handle_compute_error(
|
|
compute_ppmt(
|
|
params.rate,
|
|
period as f64,
|
|
params.nper,
|
|
params.pv,
|
|
0.0,
|
|
params.period_type,
|
|
),
|
|
cell,
|
|
) {
|
|
Ok(f) => f,
|
|
Err(err) => return err,
|
|
}
|
|
}
|
|
CalcResult::Number(result)
|
|
}
|
|
|
|
// DDB(cost, salvage, life, period, [factor])
|
|
pub(crate) fn fn_ddb(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
let arg_count = args.len();
|
|
if let Some(err) = validate_arg_count_or_return(arg_count, 4, 5, cell) {
|
|
return err;
|
|
}
|
|
|
|
let params = match parse_required_params(args, &[0, 1, 2, 3], self, cell, false) {
|
|
Ok(p) => p,
|
|
Err(err) => return err,
|
|
};
|
|
let (cost, salvage, life, period) = (params[0], params[1], params[2], params[3]);
|
|
// The rate at which the balance declines.
|
|
let factor = if arg_count > 4 {
|
|
match self.get_number_no_bools(&args[4], cell) {
|
|
Ok(f) => f,
|
|
Err(s) => return s,
|
|
}
|
|
} else {
|
|
// If factor is omitted, it is assumed to be 2 (the double-declining balance method).
|
|
2.0
|
|
};
|
|
if period > life || cost < 0.0 || salvage < 0.0 || period <= 0.0 || factor <= 0.0 {
|
|
return CalcResult::new_error(Error::NUM, cell, "invalid parameters".to_string());
|
|
};
|
|
// let period_trunc = period.floor() as i32;
|
|
let mut rate = factor / life;
|
|
if rate > 1.0 {
|
|
rate = 1.0
|
|
};
|
|
let value = if rate == 1.0 {
|
|
if period == 1.0 {
|
|
cost
|
|
} else {
|
|
0.0
|
|
}
|
|
} else {
|
|
cost * (1.0 - rate).powf(period - 1.0)
|
|
};
|
|
let new_value = cost * (1.0 - rate).powf(period);
|
|
let result = f64::max(value - f64::max(salvage, new_value), 0.0);
|
|
CalcResult::Number(result)
|
|
}
|
|
|
|
// DB(cost, salvage, life, period, [month])
|
|
pub(crate) fn fn_db(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
let arg_count = args.len();
|
|
if let Some(err) = validate_arg_count_or_return(arg_count, 4, 5, cell) {
|
|
return err;
|
|
}
|
|
|
|
let params = match parse_required_params(args, &[0, 1, 2, 3], self, cell, false) {
|
|
Ok(p) => p,
|
|
Err(err) => return err,
|
|
};
|
|
let (cost, salvage, life, period) = (params[0], params[1], params[2], params[3]);
|
|
let month = if arg_count > 4 {
|
|
match self.get_number_no_bools(&args[4], cell) {
|
|
Ok(f) => f.trunc(),
|
|
Err(s) => return s,
|
|
}
|
|
} else {
|
|
12.0
|
|
};
|
|
if month == 12.0 && period > life
|
|
|| (period > life + 1.0)
|
|
|| month <= 0.0
|
|
|| month > 12.0
|
|
|| period <= 0.0
|
|
|| cost < 0.0
|
|
{
|
|
return CalcResult::new_error(Error::NUM, cell, "invalid parameters".to_string());
|
|
};
|
|
if cost == 0.0 {
|
|
return CalcResult::Number(0.0);
|
|
}
|
|
// rounded to three decimal places
|
|
// FIXME: We should have utilities for this (see to_precision)
|
|
let rate = f64::round((1.0 - f64::powf(salvage / cost, 1.0 / life)) * 1000.0) / 1000.0;
|
|
|
|
let mut result = cost * rate * month / 12.0;
|
|
|
|
let period = period.floor() as i32;
|
|
let life = life.floor() as i32;
|
|
|
|
// Depreciation for the first and last periods is a special case.
|
|
if period == 1 {
|
|
return CalcResult::Number(result);
|
|
};
|
|
|
|
for _ in 0..period - 2 {
|
|
result += (cost - result) * rate;
|
|
}
|
|
|
|
if period == life + 1 {
|
|
// last period
|
|
return CalcResult::Number((cost - result) * rate * (12.0 - month) / 12.0);
|
|
}
|
|
|
|
CalcResult::Number(rate * (cost - result))
|
|
}
|
|
}
|