diff --git a/base/src/functions/financial.rs b/base/src/functions/financial.rs index 727df0c..a0cb03c 100644 --- a/base/src/functions/financial.rs +++ b/base/src/functions/financial.rs @@ -11,11 +11,11 @@ use crate::{ use super::financial_util::{compute_irr, compute_npv, compute_rate, compute_xirr, compute_xnpv}; // Financial calculation constants -const DAYS_30_360: i32 = 360; +const DAYS_IN_YEAR_360: i32 = 360; const DAYS_ACTUAL: i32 = 365; const DAYS_LEAP_YEAR: i32 = 366; -const DAYS_PER_MONTH_360: i32 = 30; -const TBILL_THRESHOLD_DAYS: f64 = 183.0; +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 @@ -52,7 +52,7 @@ fn is_leap_year(year: i32) -> bool { (year % 4 == 0) && (year % 100 != 0 || year % 400 == 0) } -fn is_last_day_of_feb(date: chrono::NaiveDate) -> bool { +fn is_last_day_of_february(date: chrono::NaiveDate) -> bool { date.month() == 2 && date.day() == if is_leap_year(date.year()) { 29 } else { 28 } } @@ -67,21 +67,21 @@ fn days360_us(start: chrono::NaiveDate, end: chrono::NaiveDate) -> i32 { // 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_feb(start) && is_last_day_of_feb(end) { - d2 = DAYS_PER_MONTH_360; + 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_feb(start) { - d1 = DAYS_PER_MONTH_360; + 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_PER_MONTH_360 && d2 == 31 { - d2 = DAYS_PER_MONTH_360; + if d1 == DAYS_IN_MONTH_360 && d2 == 31 { + d2 = DAYS_IN_MONTH_360; } - DAYS_30_360 * (y2 - y1) + DAYS_PER_MONTH_360 * (m2 - m1) + (d2 - d1) + DAYS_IN_YEAR_360 * (y2 - y1) + DAYS_IN_MONTH_360 * (m2 - m1) + (d2 - d1) } fn days360_eu(start: chrono::NaiveDate, end: chrono::NaiveDate) -> i32 { @@ -93,60 +93,16 @@ fn days360_eu(start: chrono::NaiveDate, end: chrono::NaiveDate) -> i32 { let y2 = end.year(); if d1 == 31 { - d1 = DAYS_PER_MONTH_360; + d1 = DAYS_IN_MONTH_360; } if d2 == 31 { - d2 = DAYS_PER_MONTH_360; + d2 = DAYS_IN_MONTH_360; } - d2 + m2 * DAYS_PER_MONTH_360 + y2 * DAYS_30_360 + d2 + m2 * DAYS_IN_MONTH_360 + y2 * DAYS_IN_YEAR_360 - d1 - - m1 * DAYS_PER_MONTH_360 - - y1 * DAYS_30_360 -} - -fn days_30us_360(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 - same as days360_us, 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_feb(start) && is_last_day_of_feb(end) { - d2 = DAYS_PER_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_feb(start) { - d1 = DAYS_PER_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_PER_MONTH_360 && d2 == 31 { - d2 = DAYS_PER_MONTH_360; - } - - (y2 - y1) * DAYS_30_360 + (m2 - m1) * DAYS_PER_MONTH_360 + (d2 - d1) -} - -fn days_30e_360(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_PER_MONTH_360; - } - if d2 == 31 { - d2 = DAYS_PER_MONTH_360; - } - (y2 - y1) * DAYS_30_360 + (m2 - m1) * DAYS_PER_MONTH_360 + (d2 - d1) + - m1 * DAYS_IN_MONTH_360 + - y1 * DAYS_IN_YEAR_360 } fn days_between(start: i64, end: i64, basis: i32) -> Result { @@ -162,7 +118,7 @@ fn days_between(start: i64, end: i64, basis: i32) -> Result { fn days_in_year(date: chrono::NaiveDate, basis: i32) -> Result { Ok(match basis { - 0 | 2 | 4 => DAYS_30_360, + 0 | 2 | 4 => DAYS_IN_YEAR_360, 1 => { if is_leap_year(date.year()) { DAYS_LEAP_YEAR @@ -175,6 +131,660 @@ fn days_in_year(date: chrono::NaiveDate, basis: i32) -> Result { }) } +/// 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 { + 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 { + 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, 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 { + 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 { + 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 { + 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( + result: Result, + cell: CellReferenceIndex, +) -> Result { + 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, CalcResult> { + // Decimal points on dates are truncated + let normalized_dates: Vec = 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 { + 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 { + // 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 { + // 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 { let start_date = from_excel_date(start)?; let days = days_between(start, end, basis)? as f64; @@ -182,21 +792,17 @@ fn year_frac(start: i64, end: i64, basis: i32) -> Result { Ok(days / year_days) } -fn year_diff(start: i64, end: i64, basis: i32) -> Result { - year_frac(start, end, basis) -} - fn year_fraction( start: chrono::NaiveDate, end: chrono::NaiveDate, basis: i32, ) -> Result { let days = match basis { - 0 => days_30us_360(start, end) as f64 / DAYS_30_360 as f64, + 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_30_360 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 => days_30e_360(start, end) as f64 / DAYS_30_360 as f64, + 4 => days360_eu(start, end) as f64 / DAYS_IN_YEAR_360 as f64, _ => return Err("Invalid basis".to_string()), }; Ok(days) @@ -219,12 +825,12 @@ fn coupon_dates( ) -> (chrono::NaiveDate, chrono::NaiveDate) { let months = 12 / freq; let step = chrono::Months::new(months as u32); - let mut ncd = maturity; - while let Some(prev) = ncd.checked_sub_months(step) { + let mut next_coupon_date = maturity; + while let Some(prev) = next_coupon_date.checked_sub_months(step) { if settlement >= prev { - return (prev, ncd); + return (prev, next_coupon_date); } - ncd = prev; + next_coupon_date = prev; } // Fallback if we somehow exit the loop (shouldn't happen in practice) (settlement, maturity) @@ -380,6 +986,13 @@ fn compute_ppmt( // 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, @@ -468,18 +1081,67 @@ impl Model { Ok(values) } + fn get_array_of_numbers_with_mode( + &mut self, + arg: &Node, + cell: &CellReferenceIndex, + mode: ArrayProcessingMode, + ) -> Result, 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, CalcResult> { - self.get_array_of_numbers_generic( - arg, - cell, - true, // accept_number_node - || Ok(None), // Ignore empty cells - || Ok(None), // Ignore non-number cells - ) + self.get_array_of_numbers_with_mode(arg, cell, ArrayProcessingMode::Standard) } fn get_array_of_numbers_xpnv( @@ -488,25 +1150,7 @@ impl Model { cell: &CellReferenceIndex, error: Error, ) -> Result, CalcResult> { - 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.clone(), - *cell, - "Expected number".to_string(), - )) - }, - ) + self.get_array_of_numbers_with_mode(arg, cell, ArrayProcessingMode::StrictWithError(error)) } fn get_array_of_numbers_xirr( @@ -514,109 +1158,63 @@ impl Model { arg: &Node, cell: &CellReferenceIndex, ) -> Result, CalcResult> { - 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(), - )) - }, - ) + 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 !(3..=5).contains(&arg_count) { - return CalcResult::new_args_number_error(cell); + if let Some(err) = validate_arg_count_or_return(arg_count, 3, 5, cell) { + return err; } - let rate = match self.get_number(&args[0], cell) { - Ok(f) => f, - Err(s) => return s, + + let params = match parse_required_params(args, &[0, 1, 2], self, cell, false) { + Ok(p) => p, + Err(err) => return err, }; - // number of periods - let nper = match self.get_number(&args[1], cell) { - Ok(f) => f, - Err(s) => return s, + 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, }; - // present value - let pv = match self.get_number(&args[2], cell) { - Ok(f) => f, - Err(s) => return s, - }; - // future_value - let fv = 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 - }; - match compute_payment(rate, nper, pv, fv, period_start) { + + match handle_compute_error( + compute_payment( + rate, + nper, + pv, + optional_params.optional_value, + optional_params.period_start, + ), + cell, + ) { Ok(p) => CalcResult::Number(p), - Err(error) => CalcResult::Error { - error: error.0, - origin: cell, - message: error.1, - }, + 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 !(3..=5).contains(&arg_count) { - return CalcResult::new_args_number_error(cell); + if let Some(err) = validate_arg_count_or_return(arg_count, 3, 5, cell) { + return err; } - let rate = match self.get_number(&args[0], cell) { - Ok(f) => f, - Err(s) => return s, + + let params = match parse_required_params(args, &[0, 1, 2], self, cell, false) { + Ok(p) => p, + Err(err) => return err, }; - // nper - let period_count = match self.get_number(&args[1], cell) { - Ok(f) => f, - Err(s) => return s, - }; - // pmt - let payment = match self.get_number(&args[2], cell) { - Ok(f) => f, - Err(s) => return s, - }; - // 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 + 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(-future_value - payment * period_count); + return CalcResult::Number(-optional_params.optional_value - payment * period_count); } if rate == -1.0 { return CalcResult::Error { @@ -626,11 +1224,13 @@ impl Model { }; }; let rate_nper = (1.0 + rate).powf(period_count); - let result = if period_start { + let result = if optional_params.period_start { // type = 1 - -(future_value * rate + payment * (1.0 + rate) * (rate_nper - 1.0)) / (rate * rate_nper) + -(optional_params.optional_value * rate + payment * (1.0 + rate) * (rate_nper - 1.0)) + / (rate * rate_nper) } else { - (-future_value * rate - payment * (rate_nper - 1.0)) / (rate * rate_nper) + (-optional_params.optional_value * rate - payment * (rate_nper - 1.0)) + / (rate * rate_nper) }; if result.is_nan() || result.is_infinite() { return CalcResult::Error { @@ -646,40 +1246,25 @@ impl Model { // 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 !(6..=8).contains(&arg_count) { - return CalcResult::new_args_number_error(cell); + if let Some(err) = validate_arg_count_or_return(arg_count, 6, 8, cell) { + return err; } - let issue = match self.get_number_no_bools(&args[0], cell) { - Ok(f) => f, - Err(s) => return s, + + let params = match parse_required_params(args, &[0, 1, 2, 3, 4, 5], self, cell, true) { + Ok(p) => p, + Err(err) => return err, }; - let first = match self.get_number_no_bools(&args[1], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let settlement = match self.get_number_no_bools(&args[2], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let rate = match self.get_number_no_bools(&args[3], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let par = match self.get_number_no_bools(&args[4], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let freq = match self.get_number_no_bools(&args[5], cell) { - Ok(f) => f as i32, - Err(s) => return s, - }; - let basis = if arg_count > 6 { - match self.get_number_no_bools(&args[6], cell) { - Ok(f) => f as i32, - Err(s) => return s, - } - } else { - 0 + 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) { @@ -693,27 +1278,27 @@ impl Model { if !(freq == 1 || freq == 2 || freq == 4) { return CalcResult::new_error(Error::NUM, cell, "invalid frequency".to_string()); } - if !(0..=4).contains(&basis) { - return CalcResult::new_error(Error::NUM, cell, "invalid basis".to_string()); + if let Err(err) = validate_basis(basis, cell) { + return err; } - if par < 0.0 { - return CalcResult::new_error(Error::NUM, cell, "par cannot be negative".to_string()); + if let Err(err) = validate_non_negative(par, "par", cell) { + return err; } - if rate < 0.0 { - return CalcResult::new_error(Error::NUM, cell, "rate cannot be negative".to_string()); + if let Err(err) = validate_non_negative(rate, "rate", cell) { + return err; } - let issue_d = match from_excel_date(issue as i64) { + let issue_d = match convert_date_serial(issue, cell) { Ok(d) => d, - Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string()), + 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 from_excel_date(settlement as i64) { + let settle_d = match convert_date_serial(settlement, cell) { Ok(d) => d, - Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string()), + Err(err) => return err, }; if settle_d < issue_d { @@ -775,51 +1360,37 @@ impl Model { // ACCRINTM(issue, settlement, rate, par, [basis]) pub(crate) fn fn_accrintm(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { let arg_count = args.len(); - if !(4..=5).contains(&arg_count) { - return CalcResult::new_args_number_error(cell); - } - let issue = match self.get_number_no_bools(&args[0], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let settlement = match self.get_number_no_bools(&args[1], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let rate = match self.get_number_no_bools(&args[2], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let par = match self.get_number_no_bools(&args[3], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let basis = if arg_count > 4 { - match self.get_number_no_bools(&args[4], cell) { - Ok(f) => f as i32, - Err(s) => return s, - } - } else { - 0 - }; - - if !(0..=4).contains(&basis) { - return CalcResult::new_error(Error::NUM, cell, "invalid basis".to_string()); - } - if par < 0.0 { - return CalcResult::new_error(Error::NUM, cell, "par cannot be negative".to_string()); - } - if rate < 0.0 { - return CalcResult::new_error(Error::NUM, cell, "rate cannot be negative".to_string()); + if let Some(err) = validate_arg_count_or_return(arg_count, 4, 5, cell) { + return err; } - let issue_d = match from_excel_date(issue as i64) { - Ok(d) => d, - Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string()), + let params = match parse_required_params(args, &[0, 1, 2, 3], self, cell, true) { + Ok(p) => p, + Err(err) => return err, }; - let settle_d = match from_excel_date(settlement as i64) { + 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(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string()), + 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 { @@ -837,21 +1408,15 @@ impl Model { // 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 !(3..=5).contains(&arg_count) { - return CalcResult::new_args_number_error(cell); + if let Some(err) = validate_arg_count_or_return(arg_count, 3, 5, cell) { + return err; } - let nper = match self.get_number(&args[0], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let pmt = match self.get_number(&args[1], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let pv = match self.get_number(&args[2], cell) { - Ok(f) => f, - Err(s) => return s, + + 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) { @@ -880,36 +1445,24 @@ impl Model { 0.1 }; - match compute_rate(pv, fv, nper, pmt, annuity_type, guess) { + match handle_compute_error(compute_rate(pv, fv, nper, pmt, annuity_type, guess), cell) { Ok(f) => CalcResult::Number(f), - Err(error) => CalcResult::Error { - error: error.0, - origin: cell, - message: error.1, - }, + 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 !(3..=5).contains(&arg_count) { - return CalcResult::new_args_number_error(cell); + if let Some(err) = validate_arg_count_or_return(arg_count, 3, 5, cell) { + return err; } - let rate = match self.get_number(&args[0], cell) { - Ok(f) => f, - Err(s) => return s, - }; - // pmt - let payment = match self.get_number(&args[1], cell) { - Ok(f) => f, - Err(s) => return s, - }; - // pv - let present_value = match self.get_number(&args[2], cell) { - Ok(f) => f, - Err(s) => return s, + + 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) { @@ -976,48 +1529,34 @@ impl Model { // FV(rate, nper, pmt, [pv], [type]) pub(crate) fn fn_fv(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { let arg_count = args.len(); - if !(3..=5).contains(&arg_count) { - return CalcResult::new_args_number_error(cell); + if let Some(err) = validate_arg_count_or_return(arg_count, 3, 5, cell) { + return err; } - let rate = match self.get_number(&args[0], cell) { - Ok(f) => f, - Err(s) => return s, + + let params = match parse_required_params(args, &[0, 1, 2], self, cell, false) { + Ok(p) => p, + Err(err) => return err, }; - // number of periods - let nper = match self.get_number(&args[1], cell) { - Ok(f) => f, - Err(s) => return s, + 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, }; - // payment - let pmt = match self.get_number(&args[2], cell) { - Ok(f) => f, - Err(s) => return s, - }; - // present value - let pv = 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 - }; - match compute_future_value(rate, nper, pmt, pv, period_start) { + + 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(error) => CalcResult::Error { - error: error.0, - origin: cell, - message: error.1, - }, + Err(err) => err, } } @@ -1053,28 +1592,16 @@ impl Model { // 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 !(4..=6).contains(&arg_count) { - return CalcResult::new_args_number_error(cell); + if let Some(err) = validate_arg_count_or_return(arg_count, 4, 6, cell) { + return err; } - let rate = match self.get_number(&args[0], cell) { - Ok(f) => f, - Err(s) => return s, - }; - // per - let period = match self.get_number(&args[1], cell) { - Ok(f) => f, - Err(s) => return s, - }; - // nper - let period_count = match self.get_number(&args[2], cell) { - Ok(f) => f, - Err(s) => return s, - }; - // pv - let present_value = match self.get_number(&args[3], cell) { - Ok(f) => f, - Err(s) => return s, + + 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) { @@ -1093,22 +1620,19 @@ impl Model { // at the end of the period false }; - let ipmt = match compute_ipmt( - rate, - period, - period_count, - present_value, - future_value, - period_start, + let ipmt = match handle_compute_error( + compute_ipmt( + rate, + period, + period_count, + present_value, + future_value, + period_start, + ), + cell, ) { Ok(f) => f, - Err(error) => { - return CalcResult::Error { - error: error.0, - origin: cell, - message: error.1, - } - } + Err(err) => return err, }; CalcResult::Number(ipmt) } @@ -1116,28 +1640,16 @@ impl Model { // 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 !(4..=6).contains(&arg_count) { - return CalcResult::new_args_number_error(cell); + if let Some(err) = validate_arg_count_or_return(arg_count, 4, 6, cell) { + return err; } - let rate = match self.get_number(&args[0], cell) { - Ok(f) => f, - Err(s) => return s, - }; - // per - let period = match self.get_number(&args[1], cell) { - Ok(f) => f, - Err(s) => return s, - }; - // nper - let period_count = match self.get_number(&args[2], cell) { - Ok(f) => f, - Err(s) => return s, - }; - // pv - let present_value = match self.get_number(&args[3], cell) { - Ok(f) => f, - Err(s) => return s, + + 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) { @@ -1157,22 +1669,19 @@ impl Model { false }; - let ppmt = match compute_ppmt( - rate, - period, - period_count, - present_value, - future_value, - period_start, + let ppmt = match handle_compute_error( + compute_ppmt( + rate, + period, + period_count, + present_value, + future_value, + period_start, + ), + cell, ) { Ok(f) => f, - Err(error) => { - return CalcResult::Error { - error: error.0, - origin: cell, - message: error.1, - } - } + Err(err) => return err, }; CalcResult::Number(ppmt) } @@ -1190,71 +1699,14 @@ impl Model { }; let mut values = Vec::new(); for arg in &args[1..] { - match self.evaluate_node_in_context(arg, cell) { - CalcResult::Number(value) => values.push(value), - CalcResult::Range { left, right } => { - if left.sheet != right.sheet { - return CalcResult::new_error( - Error::VALUE, - cell, - "Ranges are in different sheets".to_string(), - ); - } - 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 = match self.workbook.worksheet(left.sheet) { - Ok(s) => s.dimension().max_row, - Err(_) => { - return CalcResult::new_error( - Error::ERROR, - cell, - format!("Invalid worksheet index: '{}'", left.sheet), - ); - } - }; - } - if column1 == 1 && column2 == LAST_COLUMN { - column2 = match self.workbook.worksheet(left.sheet) { - Ok(s) => s.dimension().max_column, - Err(_) => { - return CalcResult::new_error( - Error::ERROR, - cell, - format!("Invalid worksheet index: '{}'", left.sheet), - ); - } - }; - } - for row in row1..row2 + 1 { - for column in column1..(column2 + 1) { - match self.evaluate_cell(CellReferenceIndex { - sheet: left.sheet, - row, - column, - }) { - CalcResult::Number(value) => { - values.push(value); - } - error @ CalcResult::Error { .. } => return error, - _ => { - // We ignore booleans and strings - } - } - } - } - } - error @ CalcResult::Error { .. } => return error, - _ => { - // We ignore booleans and strings - } - }; + match self.get_array_of_numbers(arg, &cell) { + Ok(mut arg_values) => values.append(&mut arg_values), + Err(err) => return err, + } } - match compute_npv(rate, &values) { + match handle_compute_error(compute_npv(rate, &values), cell) { Ok(f) => CalcResult::Number(f), - Err(error) => CalcResult::new_error(error.0, cell, error.1), + Err(err) => err, } } @@ -1283,13 +1735,9 @@ impl Model { } else { 0.1 }; - match compute_irr(&values, guess) { + match handle_compute_error(compute_irr(&values, guess), cell) { Ok(f) => CalcResult::Number(f), - Err(error) => CalcResult::Error { - error: error.0, - origin: cell, - message: error.1, - }, + Err(err) => err, } } @@ -1311,49 +1759,17 @@ impl Model { Ok(s) => s, Err(error) => return error, }; - // Decimal points on dates are truncated - let dates: Vec = dates.iter().map(|s| s.floor()).collect(); - let values_count = values.len(); - // If values and dates contain a different number of values, XNPV returns the #NUM! error value. - if values_count != dates.len() { - return CalcResult::new_error( - Error::NUM, - cell, - "Values and dates must be the same length".to_string(), - ); - } - if values_count == 0 { - return CalcResult::new_error(Error::NUM, cell, "Not enough values".to_string()); - } - let first_date = dates[0]; - for date in &dates { - if *date < MINIMUM_DATE_SERIAL_NUMBER as f64 - || *date > MAXIMUM_DATE_SERIAL_NUMBER as f64 - { - // Excel docs claim that if any number in dates is not a valid date, - // XNPV returns the #VALUE! error value, but it seems to return #VALUE! - return CalcResult::new_error( - Error::NUM, - cell, - "Invalid number for date".to_string(), - ); - } - // If any number in dates precedes the starting date, XNPV returns the #NUM! error value. - if date < &first_date { - return CalcResult::new_error( - Error::NUM, - cell, - "Date precedes the starting date".to_string(), - ); - } - } + 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 compute_xnpv(rate, &values, &dates) { + match handle_compute_error(compute_xnpv(rate, &values, &dates), cell) { Ok(f) => CalcResult::Number(f), - Err((error, message)) => CalcResult::new_error(error, cell, message), + Err(err) => err, } } @@ -1379,47 +1795,13 @@ impl Model { } else { 0.1 }; - // Decimal points on dates are truncated - let dates: Vec = dates.iter().map(|s| s.floor()).collect(); - let values_count = values.len(); - // If values and dates contain a different number of values, XNPV returns the #NUM! error value. - if values_count != dates.len() { - return CalcResult::new_error( - Error::NUM, - cell, - "Values and dates must be the same length".to_string(), - ); - } - if values_count == 0 { - return CalcResult::new_error(Error::NUM, cell, "Not enough values".to_string()); - } - let first_date = dates[0]; - for date in &dates { - if *date < MINIMUM_DATE_SERIAL_NUMBER as f64 - || *date > MAXIMUM_DATE_SERIAL_NUMBER as f64 - { - return CalcResult::new_error( - Error::NUM, - cell, - "Invalid number for date".to_string(), - ); - } - // If any number in dates precedes the starting date, XIRR returns the #NUM! error value. - if date < &first_date { - return CalcResult::new_error( - Error::NUM, - cell, - "Date precedes the starting date".to_string(), - ); - } - } - match compute_xirr(&values, &dates, guess) { + 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((error, message)) => CalcResult::Error { - error, - origin: cell, - message, - }, + Err(err) => err, } } @@ -1432,21 +1814,19 @@ impl Model { // $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 args.len() != 3 { - return CalcResult::new_args_number_error(cell); + 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 finance_rate = match self.get_number(&args[1], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let reinvest_rate = match self.get_number(&args[2], cell) { - Ok(f) => f, - Err(s) => return s, + 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; @@ -1477,15 +1857,9 @@ impl Model { None => 0.0, } } else { - match compute_npv(reinvest_rate, &positive_values) { + match handle_compute_error(compute_npv(reinvest_rate, &positive_values), cell) { Ok(npv) => -npv * ((1.0 + reinvest_rate).powf(years)), - Err((error, message)) => { - return CalcResult::Error { - error, - origin: cell, - message, - } - } + Err(err) => return err, } }; let bottom = if finance_rate == -1.0 { @@ -1498,15 +1872,9 @@ impl Model { f64::INFINITY } } else { - match compute_npv(finance_rate, &negative_values) { + match handle_compute_error(compute_npv(finance_rate, &negative_values), cell) { Ok(npv) => npv * (1.0 + finance_rate), - Err((error, message)) => { - return CalcResult::Error { - error, - origin: cell, - message, - } - } + Err(err) => return err, } }; @@ -1524,25 +1892,15 @@ impl Model { // Formula is: // $$pv*rate*\left(\frac{per}{nper}-1\right)$$ pub(crate) fn fn_ispmt(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { - if args.len() != 4 { - return CalcResult::new_args_number_error(cell); + if let Some(err) = validate_arg_count_or_return(args.len(), 4, 4, cell) { + return err; } - let rate = match self.get_number(&args[0], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let per = match self.get_number(&args[1], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let nper = match self.get_number(&args[2], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let pv = match self.get_number(&args[3], cell) { - Ok(f) => f, - Err(s) => return s, + + 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()); } @@ -1553,21 +1911,15 @@ impl Model { // Formula is // $$ \left(\frac{fv}{pv}\right)^{\frac{1}{nper}}-1 $$ pub(crate) fn fn_rri(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { - if args.len() != 3 { - return CalcResult::new_args_number_error(cell); + if let Some(err) = validate_arg_count_or_return(args.len(), 3, 3, cell) { + return err; } - let nper = match self.get_number(&args[0], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let pv = match self.get_number(&args[1], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let fv = match self.get_number(&args[2], cell) { - Ok(f) => f, - Err(s) => return s, + + 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()); } @@ -1590,21 +1942,15 @@ impl Model { // Formula is: // $$ \frac{cost-salvage}{life} $$ pub(crate) fn fn_sln(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { - if args.len() != 3 { - return CalcResult::new_args_number_error(cell); + if let Some(err) = validate_arg_count_or_return(args.len(), 3, 3, cell) { + return err; } - let cost = match self.get_number(&args[0], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let salvage = match self.get_number(&args[1], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let life = match self.get_number(&args[2], cell) { - Ok(f) => f, - Err(s) => return s, + + 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()); } @@ -1617,25 +1963,15 @@ impl Model { // 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 args.len() != 4 { - return CalcResult::new_args_number_error(cell); + if let Some(err) = validate_arg_count_or_return(args.len(), 4, 4, cell) { + return err; } - let cost = match self.get_number(&args[0], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let salvage = match self.get_number(&args[1], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let life = match self.get_number(&args[2], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let per = match self.get_number(&args[3], cell) { - Ok(f) => f, - Err(s) => return s, + + 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()); } @@ -1654,17 +1990,15 @@ impl Model { // $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 args.len() != 2 { - return CalcResult::new_args_number_error(cell); + if let Some(err) = validate_arg_count_or_return(args.len(), 2, 2, cell) { + return err; } - let effect_rate = match self.get_number_no_bools(&args[0], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let npery = match self.get_number_no_bools(&args[1], cell) { - Ok(f) => f.floor(), - Err(s) => return s, + + 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()); } @@ -1686,17 +2020,15 @@ impl Model { // $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 args.len() != 2 { - return CalcResult::new_args_number_error(cell); + if let Some(err) = validate_arg_count_or_return(args.len(), 2, 2, cell) { + return err; } - let nominal_rate = match self.get_number_no_bools(&args[0], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let npery = match self.get_number_no_bools(&args[1], cell) { - Ok(f) => f.floor(), - Err(s) => return s, + + 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()); } @@ -1719,21 +2051,15 @@ impl Model { // * $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 args.len() != 3 { - return CalcResult::new_args_number_error(cell); + if let Some(err) = validate_arg_count_or_return(args.len(), 3, 3, cell) { + return err; } - let rate = match self.get_number(&args[0], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let pv = match self.get_number(&args[1], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let fv = match self.get_number(&args[2], cell) { - Ok(f) => f, - Err(s) => return s, + + 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()); } @@ -1751,46 +2077,30 @@ impl Model { // 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 !(5..=6).contains(&arg_count) { - return CalcResult::new_args_number_error(cell); + if let Some(err) = validate_arg_count_or_return(arg_count, 5, 6, cell) { + return err; } - let settlement = match self.get_number_no_bools(&args[0], cell) { - Ok(f) => f, - Err(s) => return s, + + let params = match parse_required_params(args, &[0, 1, 2, 3, 4], self, cell, true) { + Ok(p) => p, + Err(err) => return err, }; - let maturity = match self.get_number_no_bools(&args[1], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let coupon = match self.get_number_no_bools(&args[2], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let yld = match self.get_number_no_bools(&args[3], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let freq = match self.get_number_no_bools(&args[4], cell) { - Ok(f) => f.trunc() as i32, - Err(s) => return s, - }; - let basis = if arg_count > 5 { - match self.get_number_no_bools(&args[5], cell) { - Ok(f) => f.trunc() as i32, - Err(s) => return s, - } - } else { - 0 + 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 = match basis { - 0 | 2 | 4 => DAYS_30_360 as f64, - 1 | 3 => DAYS_ACTUAL as f64, - _ => DAYS_30_360 as f64, - }; + 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()); @@ -1877,53 +2187,24 @@ impl Model { if args.len() != 3 { return CalcResult::new_args_number_error(cell); } - let settlement = match self.get_number_no_bools(&args[0], cell) { - Ok(f) => f, - Err(s) => return s, + + let (days_to_maturity, discount) = match parse_tbill_params(args, self, cell) { + Ok(params) => params, + Err(err) => return err, }; - let maturity = match self.get_number_no_bools(&args[1], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let discount = match self.get_number_no_bools(&args[2], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let less_than_one_year = match is_less_than_one_year(settlement as i64, maturity as i64) { - Ok(f) => f, - Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string()), - }; - if settlement > maturity { - return CalcResult::new_error( - Error::NUM, - cell, - "settlement should be <= maturity".to_string(), - ); - } - if !less_than_one_year { - return CalcResult::new_error( - Error::NUM, - cell, - "maturity <= settlement + year".to_string(), - ); - } - if discount <= 0.0 { - return CalcResult::new_error(Error::NUM, cell, "discount should be >0".to_string()); - } - // days to maturity - let d_m = maturity - settlement; - let result = if d_m < TBILL_THRESHOLD_DAYS { - DAYS_ACTUAL as f64 * discount / (DAYS_30_360 as f64 - discount * d_m) + + 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 d_m == DAYS_LEAP_YEAR as f64 { + let year = if days_to_maturity == DAYS_LEAP_YEAR as f64 { DAYS_LEAP_YEAR as f64 } else { DAYS_ACTUAL as f64 }; - let d_extra = d_m - year / 2.0; - let alpha = 1.0 - d_m * discount / DAYS_30_360 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); @@ -1931,14 +2212,8 @@ impl Model { let c = alpha - 1.0; (-b + (b * b - 4.0 * a * c).sqrt()) / (2.0 * a) }; - 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) + validate_tbill_result(result, cell) } // TBILLPRICE(settlement, maturity, discount) @@ -1946,50 +2221,20 @@ impl Model { if args.len() != 3 { return CalcResult::new_args_number_error(cell); } - let settlement = match self.get_number_no_bools(&args[0], cell) { - Ok(f) => f, - Err(s) => return s, + + let (days_to_maturity, discount) = match parse_tbill_params(args, self, cell) { + Ok(params) => params, + Err(err) => return err, }; - let maturity = match self.get_number_no_bools(&args[1], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let discount = match self.get_number_no_bools(&args[2], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let less_than_one_year = match is_less_than_one_year(settlement as i64, maturity as i64) { - Ok(f) => f, - Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string()), - }; - if settlement > maturity { - return CalcResult::new_error( - Error::NUM, - cell, - "settlement should be <= maturity".to_string(), - ); - } - if !less_than_one_year { - return CalcResult::new_error( - Error::NUM, - cell, - "maturity <= settlement + year".to_string(), - ); - } - if discount <= 0.0 { - return CalcResult::new_error(Error::NUM, cell, "discount should be >0".to_string()); - } - // days to maturity - let d_m = maturity - settlement; - let result = 100.0 * (1.0 - discount * d_m / DAYS_30_360 as f64); - if result.is_infinite() { - return CalcResult::new_error(Error::DIV, cell, "Division by 0".to_string()); - } - if result.is_nan() || result < 0.0 { + + 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()); } - CalcResult::Number(result) + validate_tbill_result(result, cell) } // TBILLYIELD(settlement, maturity, pr) @@ -1997,112 +2242,29 @@ impl Model { if args.len() != 3 { return CalcResult::new_args_number_error(cell); } - let settlement = match self.get_number_no_bools(&args[0], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let maturity = match self.get_number_no_bools(&args[1], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let pr = match self.get_number_no_bools(&args[2], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let less_than_one_year = match is_less_than_one_year(settlement as i64, maturity as i64) { - Ok(f) => f, - Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string()), - }; - if settlement > maturity { - return CalcResult::new_error( - Error::NUM, - cell, - "settlement should be <= maturity".to_string(), - ); - } - if !less_than_one_year { - return CalcResult::new_error( - Error::NUM, - cell, - "maturity <= settlement + year".to_string(), - ); - } - if pr <= 0.0 { - return CalcResult::new_error(Error::NUM, cell, "discount should be >0".to_string()); - } - let days = maturity - settlement; - let result = (100.0 - pr) * DAYS_30_360 as f64 / (pr * days); - CalcResult::Number(result) + 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 { - if !(6..=7).contains(&args.len()) { - return CalcResult::new_args_number_error(cell); - } - let settlement = match self.get_number_no_bools(&args[0], cell) { - Ok(f) => f, - Err(s) => return s, + let params = match parse_and_validate_bond_pricing_params(args, self, cell) { + Ok(p) => p, + Err(err) => return err, }; - let maturity = match self.get_number_no_bools(&args[1], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let rate = match self.get_number_no_bools(&args[2], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let yld = match self.get_number_no_bools(&args[3], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let redemption = match self.get_number_no_bools(&args[4], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let frequency = match self.get_number_no_bools(&args[5], cell) { - Ok(f) => f.trunc() as i32, - Err(s) => return s, - }; - if frequency != 1 && frequency != 2 && frequency != 4 { - return CalcResult::new_error( - Error::NUM, - cell, - "frequency should be 1, 2 or 4".to_string(), - ); - } - if settlement >= maturity { - return CalcResult::new_error( - Error::NUM, - cell, - "settlement should be < maturity".to_string(), - ); - } - let basis = if args.len() == 7 { - match self.get_number_no_bools(&args[6], cell) { - Ok(f) => f.trunc() as i32, - Err(s) => return s, - } - } else { - 0 - }; - let days_in_year = match basis { - 0 | 2 | 4 => DAYS_30_360 as f64, - 1 | 3 => DAYS_ACTUAL as f64, - _ => DAYS_30_360 as f64, - }; - let days = maturity - settlement; - let periods = ((days * frequency as f64) / days_in_year).round(); - if periods <= 0.0 { - return CalcResult::new_error(Error::NUM, cell, "invalid dates".to_string()); - } - let coupon = redemption * rate / frequency as f64; - let r = yld / frequency as f64; + + let r = params.third_param / params.frequency as f64; // yld / frequency let mut price = 0.0; - for i in 1..=(periods as i32) { - price += coupon / (1.0 + r).powf(i as f64); + for i in 1..=(params.periods as i32) { + price += params.coupon / (1.0 + r).powf(i as f64); } - price += redemption / (1.0 + r).powf(periods); + 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()); } @@ -2110,77 +2272,35 @@ impl Model { } pub(crate) fn fn_pricedisc(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { - let arg_count = args.len(); - if !(4..=5).contains(&arg_count) { - return CalcResult::new_args_number_error(cell); - } - let settlement = match self.get_number_no_bools(&args[0], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let maturity = match self.get_number_no_bools(&args[1], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let discount = match self.get_number_no_bools(&args[2], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let redemption = match self.get_number_no_bools(&args[3], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let basis = if arg_count == 5 { - match self.get_number_no_bools(&args[4], cell) { - Ok(f) => f, - Err(s) => return s, + 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) } - } else { - 0.0 - }; - if settlement >= maturity || discount <= 0.0 || redemption <= 0.0 { - 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 - { - return CalcResult::new_error(Error::NUM, cell, "Invalid number for date".to_string()); - } - let yd = match year_diff(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 result = redemption * (1.0 - discount * yd); - CalcResult::Number(result) + ) } pub(crate) fn fn_pricemat(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { let arg_count = args.len(); - if !(5..=6).contains(&arg_count) { - return CalcResult::new_args_number_error(cell); + if let Some(err) = validate_arg_count_or_return(arg_count, 5, 6, cell) { + return err; } - let settlement = match self.get_number_no_bools(&args[0], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let maturity = match self.get_number_no_bools(&args[1], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let issue = match self.get_number_no_bools(&args[2], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let rate = match self.get_number_no_bools(&args[3], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let yld = match self.get_number_no_bools(&args[4], cell) { - Ok(f) => f, - Err(s) => return s, + + 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, @@ -2201,168 +2321,83 @@ impl Model { { return CalcResult::new_error(Error::NUM, cell, "Invalid number for date".to_string()); } - let f_iss_mat = match year_frac(issue as i64, maturity as i64, basis as i32) { + 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 f_iss_set = 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 f_set_mat = 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 + f_iss_mat * rate; - result /= 1.0 + f_set_mat * yld; - result -= f_iss_set * rate; + 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 { - if !(6..=7).contains(&args.len()) { - return CalcResult::new_args_number_error(cell); - } - let settlement = match self.get_number_no_bools(&args[0], cell) { - Ok(f) => f, - Err(s) => return s, + let params = match parse_and_validate_bond_pricing_params(args, self, cell) { + Ok(p) => p, + Err(err) => return err, }; - let maturity = match self.get_number_no_bools(&args[1], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let rate = match self.get_number_no_bools(&args[2], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let price = match self.get_number_no_bools(&args[3], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let redemption = match self.get_number_no_bools(&args[4], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let frequency = match self.get_number_no_bools(&args[5], cell) { - Ok(f) => f.trunc() as i32, - Err(s) => return s, - }; - if frequency != 1 && frequency != 2 && frequency != 4 { - return CalcResult::new_error( - Error::NUM, - cell, - "frequency should be 1, 2 or 4".to_string(), - ); - } - if settlement >= maturity { - return CalcResult::new_error( - Error::NUM, - cell, - "settlement should be < maturity".to_string(), - ); - } - let basis = if args.len() == 7 { - match self.get_number_no_bools(&args[6], cell) { - Ok(f) => f.trunc() as i32, - Err(s) => return s, - } - } else { - 0 - }; - let days_in_year = match basis { - 0 | 2 | 4 => DAYS_30_360 as f64, - 1 | 3 => DAYS_ACTUAL as f64, - _ => DAYS_30_360 as f64, - }; - let days = maturity - settlement; - let periods = ((days * frequency as f64) / days_in_year).round(); - if periods <= 0.0 { - return CalcResult::new_error(Error::NUM, cell, "invalid dates".to_string()); - } - let coupon = redemption * rate / frequency as f64; - match compute_rate(-price, redemption, periods, coupon, 0, 0.1) { - Ok(r) => CalcResult::Number(r * frequency as f64), - Err(err) => CalcResult::Error { - error: err.0, - origin: cell, - message: err.1, - }, + + 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 { - let arg_count = args.len(); - if !(4..=5).contains(&arg_count) { - return CalcResult::new_args_number_error(cell); - } - let settlement = match self.get_number_no_bools(&args[0], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let maturity = match self.get_number_no_bools(&args[1], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let pr = match self.get_number_no_bools(&args[2], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let redemption = match self.get_number_no_bools(&args[3], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let basis = if arg_count == 5 { - match self.get_number_no_bools(&args[4], cell) { - Ok(f) => f, - Err(s) => return s, + 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 } - } else { - 0.0 - }; - if settlement >= maturity || pr <= 0.0 || redemption <= 0.0 { - 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 - { - return CalcResult::new_error(Error::NUM, cell, "Invalid number for date".to_string()); - } - let yf = 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 result = (redemption / pr - 1.0) / yf; - CalcResult::Number(result) + ) } pub(crate) fn fn_yieldmat(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { let arg_count = args.len(); - if !(5..=6).contains(&arg_count) { - return CalcResult::new_args_number_error(cell); + if let Some(err) = validate_arg_count_or_return(arg_count, 5, 6, cell) { + return err; } - let settlement = match self.get_number_no_bools(&args[0], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let maturity = match self.get_number_no_bools(&args[1], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let issue = match self.get_number_no_bools(&args[2], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let rate = match self.get_number_no_bools(&args[3], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let price = match self.get_number_no_bools(&args[4], cell) { - Ok(f) => f, - Err(s) => return s, + + 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, @@ -2383,415 +2418,166 @@ impl Model { { return CalcResult::new_error(Error::NUM, cell, "Invalid number for date".to_string()); } - let f_iss_mat = match year_frac(issue as i64, maturity as i64, basis as i32) { + 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 f_iss_set = 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 f_set_mat = 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 + f_iss_mat * rate; - y /= price / 100.0 + f_iss_set * rate; + 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 /= f_set_mat; + 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 { - let arg_count = args.len(); - if !(4..=5).contains(&arg_count) { - return CalcResult::new_args_number_error(cell); - } - let settlement = match self.get_number_no_bools(&args[0], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let maturity = match self.get_number_no_bools(&args[1], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let pr = match self.get_number_no_bools(&args[2], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let redemption = match self.get_number_no_bools(&args[3], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let basis = if arg_count == 5 { - match self.get_number_no_bools(&args[4], cell) { - Ok(f) => f, - Err(s) => return s, + 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 } - } else { - 0.0 - }; - if pr <= 0.0 || redemption <= 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 - { - return CalcResult::new_error(Error::NUM, cell, "Invalid number for date".to_string()); - } - let yf = 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 result = (1.0 - pr / redemption) / yf; - CalcResult::Number(result) + ) } // RECEIVED(settlement, maturity, investment, discount, [basis]) pub(crate) fn fn_received(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { - let arg_count = args.len(); - if !(4..=5).contains(&arg_count) { - return CalcResult::new_args_number_error(cell); - } - let settlement = match self.get_number_no_bools(&args[0], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let maturity = match self.get_number_no_bools(&args[1], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let investment = match self.get_number_no_bools(&args[2], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let discount = match self.get_number_no_bools(&args[3], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let basis = if arg_count == 5 { - match self.get_number_no_bools(&args[4], cell) { - Ok(f) => f, - Err(s) => return s, + 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) } - } else { - 0.0 - }; - if investment <= 0.0 || discount <= 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 - { - return CalcResult::new_error(Error::NUM, cell, "Invalid number for date".to_string()); - } - let yd = match year_diff(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 result = investment / (1.0 - discount * yd); - CalcResult::Number(result) + ) } // INTRATE(settlement, maturity, investment, redemption, [basis]) pub(crate) fn fn_intrate(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { - let arg_count = args.len(); - if !(4..=5).contains(&arg_count) { - return CalcResult::new_args_number_error(cell); - } - let settlement = match self.get_number_no_bools(&args[0], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let maturity = match self.get_number_no_bools(&args[1], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let investment = match self.get_number_no_bools(&args[2], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let redemption = match self.get_number_no_bools(&args[3], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let basis = if arg_count == 5 { - match self.get_number_no_bools(&args[4], cell) { - Ok(f) => f, - Err(s) => return s, + 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 } - } else { - 0.0 - }; - if investment <= 0.0 || redemption <= 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 - { - return CalcResult::new_error(Error::NUM, cell, "Invalid number for date".to_string()); - } - let yd = match year_diff(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 result = ((redemption / investment) - 1.0) / yd; - CalcResult::Number(result) + ) } // COUPDAYBS(settlement, maturity, frequency, [basis]) pub(crate) fn fn_coupdaybs(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { - if args.len() < 3 || args.len() > 4 { - return CalcResult::new_args_number_error(cell); - } - let settlement = match self.get_number_no_bools(&args[0], cell) { - Ok(f) => f.trunc() as i64, - Err(s) => return s, - }; - let maturity = match self.get_number_no_bools(&args[1], cell) { - Ok(f) => f.trunc() as i64, - Err(s) => return s, - }; - let frequency = match self.get_number_no_bools(&args[2], cell) { - Ok(f) => f.trunc() as i32, - Err(s) => return s, - }; - let basis = if args.len() > 3 { - match self.get_number_no_bools(&args[3], cell) { - Ok(f) => f.trunc() as i32, - Err(s) => return s, - } - } else { - 0 + let params = match parse_and_validate_coupon_params(args, args.len(), self, cell) { + Ok(p) => p, + Err(err) => return err, }; - if ![1, 2, 4].contains(&frequency) || !(0..=4).contains(&basis) { - return CalcResult::new_error(Error::NUM, cell, "invalid arguments".to_string()); - } - if settlement >= maturity { - return CalcResult::new_error(Error::NUM, cell, "settlement < maturity".to_string()); - } - - let settlement_date = match from_excel_date(settlement) { - Ok(d) => d, - Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string()), - }; - let maturity_date = match from_excel_date(maturity) { - Ok(d) => d, - Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string()), - }; - - let (pcd, _) = coupon_dates(settlement_date, maturity_date, frequency); - let days = days_between_dates(pcd, settlement_date, basis); + 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 { - if args.len() < 3 || args.len() > 4 { - return CalcResult::new_args_number_error(cell); - } - let settlement = match self.get_number_no_bools(&args[0], cell) { - Ok(f) => f.trunc() as i64, - Err(s) => return s, - }; - let maturity = match self.get_number_no_bools(&args[1], cell) { - Ok(f) => f.trunc() as i64, - Err(s) => return s, - }; - let frequency = match self.get_number_no_bools(&args[2], cell) { - Ok(f) => f.trunc() as i32, - Err(s) => return s, - }; - let basis = if args.len() > 3 { - match self.get_number_no_bools(&args[3], cell) { - Ok(f) => f.trunc() as i32, - Err(s) => return s, - } - } else { - 0 + let params = match parse_and_validate_coupon_params(args, args.len(), self, cell) { + Ok(p) => p, + Err(err) => return err, }; - if ![1, 2, 4].contains(&frequency) || !(0..=4).contains(&basis) { - return CalcResult::new_error(Error::NUM, cell, "invalid arguments".to_string()); - } - if settlement >= maturity { - return CalcResult::new_error(Error::NUM, cell, "settlement < maturity".to_string()); - } - - let settlement_date = match from_excel_date(settlement) { - Ok(d) => d, - Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string()), - }; - let maturity_date = match from_excel_date(maturity) { - Ok(d) => d, - Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string()), - }; - - let (pcd, ncd) = coupon_dates(settlement_date, maturity_date, frequency); - let days = match basis { - 0 | 4 => DAYS_30_360 / frequency, // 30/360 conventions - _ => days_between_dates(pcd, ncd, basis), // Actual day counts + 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 { - if args.len() < 3 || args.len() > 4 { - return CalcResult::new_args_number_error(cell); - } - let settlement = match self.get_number_no_bools(&args[0], cell) { - Ok(f) => f.trunc() as i64, - Err(s) => return s, - }; - let maturity = match self.get_number_no_bools(&args[1], cell) { - Ok(f) => f.trunc() as i64, - Err(s) => return s, - }; - let frequency = match self.get_number_no_bools(&args[2], cell) { - Ok(f) => f.trunc() as i32, - Err(s) => return s, - }; - let basis = if args.len() > 3 { - match self.get_number_no_bools(&args[3], cell) { - Ok(f) => f.trunc() as i32, - Err(s) => return s, - } - } else { - 0 + let params = match parse_and_validate_coupon_params(args, args.len(), self, cell) { + Ok(p) => p, + Err(err) => return err, }; - if ![1, 2, 4].contains(&frequency) || !(0..=4).contains(&basis) { - return CalcResult::new_error(Error::NUM, cell, "invalid arguments".to_string()); - } - if settlement >= maturity { - return CalcResult::new_error(Error::NUM, cell, "settlement < maturity".to_string()); - } - - let settlement_date = match from_excel_date(settlement) { - Ok(d) => d, - Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string()), - }; - let maturity_date = match from_excel_date(maturity) { - Ok(d) => d, - Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string()), - }; - - let (_, ncd) = coupon_dates(settlement_date, maturity_date, frequency); - let days = days_between_dates(settlement_date, ncd, basis); + 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 { - if args.len() < 3 || args.len() > 4 { - return CalcResult::new_args_number_error(cell); - } - let settlement = match self.get_number_no_bools(&args[0], cell) { - Ok(f) => f.trunc() as i64, - Err(s) => return s, - }; - let maturity = match self.get_number_no_bools(&args[1], cell) { - Ok(f) => f.trunc() as i64, - Err(s) => return s, - }; - let frequency = match self.get_number_no_bools(&args[2], cell) { - Ok(f) => f.trunc() as i32, - Err(s) => return s, - }; - let basis = if args.len() > 3 { - match self.get_number_no_bools(&args[3], cell) { - Ok(f) => f.trunc() as i32, - Err(s) => return s, - } - } else { - 0 + let params = match parse_and_validate_coupon_params(args, args.len(), self, cell) { + Ok(p) => p, + Err(err) => return err, }; - if ![1, 2, 4].contains(&frequency) || !(0..=4).contains(&basis) { - return CalcResult::new_error(Error::NUM, cell, "invalid arguments".to_string()); - } - if settlement >= maturity { - return CalcResult::new_error(Error::NUM, cell, "settlement < maturity".to_string()); - } - - let settlement_date = match from_excel_date(settlement) { - Ok(d) => d, - Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string()), - }; - let maturity_date = match from_excel_date(maturity) { - Ok(d) => d, - Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string()), - }; - - let (_, ncd) = coupon_dates(settlement_date, maturity_date, frequency); - match crate::formatter::dates::date_to_serial_number(ncd.day(), ncd.month(), ncd.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), - } + 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 { - if args.len() < 3 || args.len() > 4 { - return CalcResult::new_args_number_error(cell); - } - let settlement = match self.get_number_no_bools(&args[0], cell) { - Ok(f) => f.trunc() as i64, - Err(s) => return s, - }; - let maturity = match self.get_number_no_bools(&args[1], cell) { - Ok(f) => f.trunc() as i64, - Err(s) => return s, - }; - let frequency = match self.get_number_no_bools(&args[2], cell) { - Ok(f) => f.trunc() as i32, - Err(s) => return s, - }; - let basis = if args.len() > 3 { - match self.get_number_no_bools(&args[3], cell) { - Ok(f) => f.trunc() as i32, - Err(s) => return s, - } - } else { - 0 + let params = match parse_and_validate_coupon_params(args, args.len(), self, cell) { + Ok(p) => p, + Err(err) => return err, }; - if ![1, 2, 4].contains(&frequency) || !(0..=4).contains(&basis) { - return CalcResult::new_error(Error::NUM, cell, "invalid arguments".to_string()); - } - if settlement >= maturity { - return CalcResult::new_error(Error::NUM, cell, "settlement < maturity".to_string()); - } - - let settlement_date = match from_excel_date(settlement) { - Ok(d) => d, - Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string()), - }; - let maturity_date = match from_excel_date(maturity) { - Ok(d) => d, - Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string()), - }; - - let months = 12 / frequency; + let months = 12 / params.frequency; let step = chrono::Months::new(months as u32); - let mut date = maturity_date; + let mut date = params.maturity_date; let mut count = 0; - while settlement_date < date { + while params.settlement_date < date { count += 1; date = match date.checked_sub_months(step) { Some(new_date) => new_date, @@ -2803,83 +2589,35 @@ impl Model { // COUPPCD(settlement, maturity, frequency, [basis]) pub(crate) fn fn_couppcd(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { - if args.len() < 3 || args.len() > 4 { - return CalcResult::new_args_number_error(cell); - } - let settlement = match self.get_number_no_bools(&args[0], cell) { - Ok(f) => f.trunc() as i64, - Err(s) => return s, - }; - let maturity = match self.get_number_no_bools(&args[1], cell) { - Ok(f) => f.trunc() as i64, - Err(s) => return s, - }; - let frequency = match self.get_number_no_bools(&args[2], cell) { - Ok(f) => f.trunc() as i32, - Err(s) => return s, - }; - let basis = if args.len() > 3 { - match self.get_number_no_bools(&args[3], cell) { - Ok(f) => f.trunc() as i32, - Err(s) => return s, - } - } else { - 0 + let params = match parse_and_validate_coupon_params(args, args.len(), self, cell) { + Ok(p) => p, + Err(err) => return err, }; - if ![1, 2, 4].contains(&frequency) || !(0..=4).contains(&basis) { - return CalcResult::new_error(Error::NUM, cell, "invalid arguments".to_string()); - } - if settlement >= maturity { - return CalcResult::new_error(Error::NUM, cell, "settlement < maturity".to_string()); - } - - let settlement_date = match from_excel_date(settlement) { - Ok(d) => d, - Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string()), - }; - let maturity_date = match from_excel_date(maturity) { - Ok(d) => d, - Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string()), - }; - - let (pcd, _) = coupon_dates(settlement_date, maturity_date, frequency); - match crate::formatter::dates::date_to_serial_number(pcd.day(), pcd.month(), pcd.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), - } + 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 args.len() != 2 { - return CalcResult::new_args_number_error(cell); + if let Some(err) = validate_arg_count_or_return(args.len(), 2, 2, cell) { + return err; } - let fractional_dollar = match self.get_number_no_bools(&args[0], cell) { - Ok(f) => f, - Err(s) => return s, + + let params = match parse_required_params(args, &[0, 1], self, cell, true) { + Ok(p) => p, + Err(err) => return err, }; - let mut fraction = match self.get_number_no_bools(&args[1], cell) { + let (fractional_dollar, raw_fraction) = (params[0], params[1]); + let fraction = match validate_and_normalize_fraction(raw_fraction, cell) { Ok(f) => f, - Err(s) => return s, + Err(err) => return err, }; - if fraction < 0.0 { - return CalcResult::new_error(Error::NUM, cell, "fraction should be >= 1".to_string()); - } - if fraction < 1.0 { - // this is not necessarily DIV/0 - return CalcResult::new_error(Error::DIV, cell, "fraction should be >= 1".to_string()); - } - fraction = fraction.trunc(); - while fraction > 10.0 { - fraction /= 10.0; - } + let t = fractional_dollar.trunc(); let result = t + (fractional_dollar - t) * 10.0 / fraction; CalcResult::Number(result) @@ -2887,28 +2625,20 @@ impl Model { // DOLLARFR(decimal_dollar, fraction) pub(crate) fn fn_dollarfr(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { - if args.len() != 2 { - return CalcResult::new_args_number_error(cell); + if let Some(err) = validate_arg_count_or_return(args.len(), 2, 2, cell) { + return err; } - let decimal_dollar = match self.get_number_no_bools(&args[0], cell) { - Ok(f) => f, - Err(s) => return s, + + let params = match parse_required_params(args, &[0, 1], self, cell, true) { + Ok(p) => p, + Err(err) => return err, }; - let mut fraction = match self.get_number_no_bools(&args[1], cell) { + let (decimal_dollar, raw_fraction) = (params[0], params[1]); + let fraction = match validate_and_normalize_fraction(raw_fraction, cell) { Ok(f) => f, - Err(s) => return s, + Err(err) => return err, }; - if fraction < 0.0 { - return CalcResult::new_error(Error::NUM, cell, "fraction should be >= 1".to_string()); - } - if fraction < 1.0 { - // this is not necessarily DIV/0 - return CalcResult::new_error(Error::DIV, cell, "fraction should be >= 1".to_string()); - } - fraction = fraction.trunc(); - while fraction > 10.0 { - fraction /= 10.0; - } + let t = decimal_dollar.trunc(); let result = t + (decimal_dollar - t) * fraction / 10.0; CalcResult::Number(result) @@ -2916,67 +2646,25 @@ impl Model { // CUMIPMT(rate, nper, pv, start_period, end_period, type) pub(crate) fn fn_cumipmt(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { - if args.len() != 6 { - return CalcResult::new_args_number_error(cell); - } - let rate = match self.get_number_no_bools(&args[0], cell) { - Ok(f) => f, - Err(s) => return s, + let params = match parse_and_validate_cumulative_payment_params(args, self, cell) { + Ok(p) => p, + Err(err) => return err, }; - let nper = match self.get_number_no_bools(&args[1], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let pv = match self.get_number_no_bools(&args[2], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let start_period = match self.get_number_no_bools(&args[3], cell) { - Ok(f) => f.ceil() as i32, - Err(s) => return s, - }; - let end_period = match self.get_number_no_bools(&args[4], cell) { - Ok(f) => f.trunc() as i32, - Err(s) => return s, - }; - // 0 at the end of the period, 1 at the beginning of the period - let period_type = match self.get_number_no_bools(&args[5], cell) { - Ok(f) => { - if f == 0.0 { - false - } else if f == 1.0 { - true - } else { - return CalcResult::new_error( - Error::NUM, - cell, - "invalid period type".to_string(), - ); - } - } - Err(s) => return s, - }; - if start_period > end_period { - return CalcResult::new_error( - Error::NUM, - cell, - "start period should come before end period".to_string(), - ); - } - if rate <= 0.0 || nper <= 0.0 || pv <= 0.0 || start_period < 1 { - return CalcResult::new_error(Error::NUM, cell, "invalid parameters".to_string()); - } let mut result = 0.0; - for period in start_period..=end_period { - result += match compute_ipmt(rate, period as f64, nper, pv, 0.0, period_type) { + 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(error) => { - return CalcResult::Error { - error: error.0, - origin: cell, - message: error.1, - } - } + Err(err) => return err, } } CalcResult::Number(result) @@ -2984,67 +2672,25 @@ impl Model { // CUMPRINC(rate, nper, pv, start_period, end_period, type) pub(crate) fn fn_cumprinc(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { - if args.len() != 6 { - return CalcResult::new_args_number_error(cell); - } - let rate = match self.get_number_no_bools(&args[0], cell) { - Ok(f) => f, - Err(s) => return s, + let params = match parse_and_validate_cumulative_payment_params(args, self, cell) { + Ok(p) => p, + Err(err) => return err, }; - let nper = match self.get_number_no_bools(&args[1], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let pv = match self.get_number_no_bools(&args[2], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let start_period = match self.get_number_no_bools(&args[3], cell) { - Ok(f) => f.ceil() as i32, - Err(s) => return s, - }; - let end_period = match self.get_number_no_bools(&args[4], cell) { - Ok(f) => f.trunc() as i32, - Err(s) => return s, - }; - // 0 at the end of the period, 1 at the beginning of the period - let period_type = match self.get_number_no_bools(&args[5], cell) { - Ok(f) => { - if f == 0.0 { - false - } else if f == 1.0 { - true - } else { - return CalcResult::new_error( - Error::NUM, - cell, - "invalid period type".to_string(), - ); - } - } - Err(s) => return s, - }; - if start_period > end_period { - return CalcResult::new_error( - Error::NUM, - cell, - "start period should come before end period".to_string(), - ); - } - if rate <= 0.0 || nper <= 0.0 || pv <= 0.0 || start_period < 1 { - return CalcResult::new_error(Error::NUM, cell, "invalid parameters".to_string()); - } let mut result = 0.0; - for period in start_period..=end_period { - result += match compute_ppmt(rate, period as f64, nper, pv, 0.0, period_type) { + 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(error) => { - return CalcResult::Error { - error: error.0, - origin: cell, - message: error.1, - } - } + Err(err) => return err, } } CalcResult::Number(result) @@ -3053,25 +2699,15 @@ impl Model { // DDB(cost, salvage, life, period, [factor]) pub(crate) fn fn_ddb(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { let arg_count = args.len(); - if !(4..=5).contains(&arg_count) { - return CalcResult::new_args_number_error(cell); + if let Some(err) = validate_arg_count_or_return(arg_count, 4, 5, cell) { + return err; } - let cost = match self.get_number(&args[0], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let salvage = match self.get_number(&args[1], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let life = match self.get_number(&args[2], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let period = match self.get_number(&args[3], cell) { - Ok(f) => f, - Err(s) => return s, + + 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) { @@ -3107,25 +2743,15 @@ impl Model { // DB(cost, salvage, life, period, [month]) pub(crate) fn fn_db(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { let arg_count = args.len(); - if !(4..=5).contains(&arg_count) { - return CalcResult::new_args_number_error(cell); + if let Some(err) = validate_arg_count_or_return(arg_count, 4, 5, cell) { + return err; } - let cost = match self.get_number(&args[0], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let salvage = match self.get_number(&args[1], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let life = match self.get_number(&args[2], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let period = match self.get_number(&args[3], cell) { - Ok(f) => f, - Err(s) => return s, + + 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(),