merge pricedisc, pricemat, yielddisc, yieldmat, disc, received, intrate #57

This commit is contained in:
Brian Hung
2025-07-31 15:19:51 -07:00
committed by Nicolás Hatcher
parent 04e012b518
commit 050677f905
12 changed files with 896 additions and 23 deletions

View File

@@ -783,6 +783,13 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
Function::Tbillprice => args_signature_scalars(arg_count, 3, 0),
Function::Tbillyield => args_signature_scalars(arg_count, 3, 0),
Function::Yield => args_signature_scalars(arg_count, 6, 1),
Function::Pricedisc => args_signature_scalars(arg_count, 4, 1),
Function::Pricemat => args_signature_scalars(arg_count, 5, 1),
Function::Yielddisc => args_signature_scalars(arg_count, 4, 1),
Function::Yieldmat => args_signature_scalars(arg_count, 5, 1),
Function::Disc => args_signature_scalars(arg_count, 4, 1),
Function::Received => args_signature_scalars(arg_count, 4, 1),
Function::Intrate => args_signature_scalars(arg_count, 4, 1),
Function::Xirr => args_signature_xirr(arg_count),
Function::Xnpv => args_signature_xnpv(arg_count),
Function::Besseli => args_signature_scalars(arg_count, 2, 0),
@@ -1052,6 +1059,13 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
Function::Tbillprice => not_implemented(args),
Function::Tbillyield => not_implemented(args),
Function::Yield => not_implemented(args),
Function::Pricedisc => not_implemented(args),
Function::Pricemat => not_implemented(args),
Function::Yielddisc => not_implemented(args),
Function::Yieldmat => not_implemented(args),
Function::Disc => not_implemented(args),
Function::Received => not_implemented(args),
Function::Intrate => not_implemented(args),
Function::Xirr => not_implemented(args),
Function::Xnpv => not_implemented(args),
Function::Besseli => scalar_arguments(args),

View File

@@ -41,6 +41,94 @@ fn is_less_than_one_year(start_date: i64, end_date: i64) -> Result<bool, String>
Ok(end_day <= start_day)
}
fn is_leap_year(year: i32) -> bool {
(year % 4 == 0 && year % 100 != 0) || year % 400 == 0
}
fn days360_us(start: chrono::NaiveDate, end: chrono::NaiveDate) -> i32 {
let mut d1 = start.day() as i32;
let m1 = start.month() as i32;
let y1 = start.year();
let mut d2 = end.day() as i32;
let mut m2 = end.month() as i32;
let mut y2 = end.year();
if d1 == 31 || (m1 == 2 && (d1 == 29 || (d1 == 28 && !is_leap_year(y1)))) {
d1 = 30;
}
if d2 == 31 {
if d1 != 30 {
d2 = 1;
if m2 == 12 {
y2 += 1;
m2 = 1;
} else {
m2 += 1;
}
} else {
d2 = 30;
}
}
d2 + m2 * 30 + y2 * 360 - d1 - m1 * 30 - y1 * 360
}
fn days360_eu(start: chrono::NaiveDate, end: chrono::NaiveDate) -> i32 {
let mut d1 = start.day() as i32;
let mut d2 = end.day() as i32;
let m1 = start.month() as i32;
let m2 = end.month() as i32;
let y1 = start.year();
let y2 = end.year();
if d1 == 31 {
d1 = 30;
}
if d2 == 31 {
d2 = 30;
}
d2 + m2 * 30 + y2 * 360 - d1 - m1 * 30 - y1 * 360
}
fn days_between(start: i64, end: i64, basis: i32) -> Result<i32, String> {
let start_date = from_excel_date(start)?;
let end_date = from_excel_date(end)?;
Ok(match basis {
0 => days360_us(start_date, end_date),
1..=3 => (end - start) as i32,
4 => days360_eu(start_date, end_date),
_ => return Err("invalid basis".to_string()),
})
}
fn days_in_year(date: chrono::NaiveDate, basis: i32) -> Result<i32, String> {
Ok(match basis {
0 | 2 | 4 => 360,
1 => {
if is_leap_year(date.year()) {
366
} else {
365
}
}
3 => 365,
_ => return Err("invalid basis".to_string()),
})
}
fn year_frac(start: i64, end: i64, basis: i32) -> Result<f64, String> {
let start_date = from_excel_date(start)?;
let days = days_between(start, end, basis)? as f64;
let year_days = days_in_year(start_date, basis)? as f64;
Ok(days / year_days)
}
fn year_diff(start: i64, end: i64, basis: i32) -> Result<f64, String> {
year_frac(start, end, basis)
}
fn compute_payment(
rate: f64,
nper: f64,
@@ -1652,7 +1740,6 @@ impl Model {
CalcResult::Number(result)
}
// PRICE(settlement, maturity, rate, yld, redemption, frequency, [basis])
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);
@@ -1720,7 +1807,117 @@ impl Model {
CalcResult::Number(price)
}
// YIELD(settlement, maturity, rate, pr, redemption, frequency, [basis])
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,
}
} 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);
}
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 basis = if arg_count == 6 {
match self.get_number_no_bools(&args[5], cell) {
Ok(f) => f,
Err(s) => return s,
}
} else {
0.0
};
if rate < 0.0 || yld < 0.0 || settlement >= maturity {
return CalcResult::new_error(Error::NUM, cell, "invalid parameters".to_string());
}
if settlement < MINIMUM_DATE_SERIAL_NUMBER as f64
|| maturity > MAXIMUM_DATE_SERIAL_NUMBER as f64
|| settlement > MAXIMUM_DATE_SERIAL_NUMBER as f64
|| maturity < MINIMUM_DATE_SERIAL_NUMBER as f64
|| issue < MINIMUM_DATE_SERIAL_NUMBER as f64
|| issue > MAXIMUM_DATE_SERIAL_NUMBER as f64
{
return CalcResult::new_error(Error::NUM, cell, "Invalid number for date".to_string());
}
let f_iss_mat = 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;
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);
@@ -1786,6 +1983,261 @@ impl Model {
}
}
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,
}
} 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);
}
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 basis = if arg_count == 6 {
match self.get_number_no_bools(&args[5], cell) {
Ok(f) => f,
Err(s) => return s,
}
} else {
0.0
};
if price <= 0.0 || rate < 0.0 || settlement >= maturity || settlement < issue {
return CalcResult::new_error(Error::NUM, cell, "invalid parameters".to_string());
}
if settlement < MINIMUM_DATE_SERIAL_NUMBER as f64
|| maturity > MAXIMUM_DATE_SERIAL_NUMBER as f64
|| settlement > MAXIMUM_DATE_SERIAL_NUMBER as f64
|| maturity < MINIMUM_DATE_SERIAL_NUMBER as f64
|| issue < MINIMUM_DATE_SERIAL_NUMBER as f64
|| issue > MAXIMUM_DATE_SERIAL_NUMBER as f64
{
return CalcResult::new_error(Error::NUM, cell, "Invalid number for date".to_string());
}
let f_iss_mat = 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;
y -= 1.0;
y /= f_set_mat;
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,
}
} 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,
}
} 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,
}
} 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)
}
// DOLLARDE(fractional_dollar, fraction)
pub(crate) fn fn_dollarde(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 2 {

View File

@@ -247,6 +247,13 @@ pub enum Function {
Tbilleq,
Tbillprice,
Tbillyield,
Pricedisc,
Pricemat,
Yielddisc,
Yieldmat,
Disc,
Received,
Intrate,
Xirr,
Xnpv,
Yield,
@@ -318,7 +325,7 @@ pub enum Function {
}
impl Function {
pub fn into_iter() -> IntoIter<Function, 261> {
pub fn into_iter() -> IntoIter<Function, 268> {
[
Function::And,
Function::False,
@@ -511,6 +518,13 @@ impl Function {
Function::Tbillyield,
Function::Tbillprice,
Function::Tbilleq,
Function::Pricedisc,
Function::Pricemat,
Function::Yielddisc,
Function::Yieldmat,
Function::Disc,
Function::Received,
Function::Intrate,
Function::Dollarde,
Function::Dollarfr,
Function::Ddb,
@@ -852,6 +866,13 @@ impl Function {
"TBILLYIELD" => Some(Function::Tbillyield),
"TBILLPRICE" => Some(Function::Tbillprice),
"TBILLEQ" => Some(Function::Tbilleq),
"PRICEDISC" => Some(Function::Pricedisc),
"PRICEMAT" => Some(Function::Pricemat),
"YIELDDISC" => Some(Function::Yielddisc),
"YIELDMAT" => Some(Function::Yieldmat),
"DISC" => Some(Function::Disc),
"RECEIVED" => Some(Function::Received),
"INTRATE" => Some(Function::Intrate),
"DOLLARDE" => Some(Function::Dollarde),
"DOLLARFR" => Some(Function::Dollarfr),
@@ -1099,6 +1120,13 @@ impl fmt::Display for Function {
Function::Tbillyield => write!(f, "TBILLYIELD"),
Function::Tbillprice => write!(f, "TBILLPRICE"),
Function::Tbilleq => write!(f, "TBILLEQ"),
Function::Pricedisc => write!(f, "PRICEDISC"),
Function::Pricemat => write!(f, "PRICEMAT"),
Function::Yielddisc => write!(f, "YIELDDISC"),
Function::Yieldmat => write!(f, "YIELDMAT"),
Function::Disc => write!(f, "DISC"),
Function::Received => write!(f, "RECEIVED"),
Function::Intrate => write!(f, "INTRATE"),
Function::Dollarde => write!(f, "DOLLARDE"),
Function::Dollarfr => write!(f, "DOLLARFR"),
Function::Ddb => write!(f, "DDB"),
@@ -1381,6 +1409,13 @@ impl Model {
Function::Tbillyield => self.fn_tbillyield(args, cell),
Function::Tbillprice => self.fn_tbillprice(args, cell),
Function::Tbilleq => self.fn_tbilleq(args, cell),
Function::Pricedisc => self.fn_pricedisc(args, cell),
Function::Pricemat => self.fn_pricemat(args, cell),
Function::Yielddisc => self.fn_yielddisc(args, cell),
Function::Yieldmat => self.fn_yieldmat(args, cell),
Function::Disc => self.fn_disc(args, cell),
Function::Received => self.fn_received(args, cell),
Function::Intrate => self.fn_intrate(args, cell),
Function::Dollarde => self.fn_dollarde(args, cell),
Function::Dollarfr => self.fn_dollarfr(args, cell),
Function::Ddb => self.fn_ddb(args, cell),

View File

@@ -1,6 +1,6 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
use crate::{cell::CellValue, test::util::new_empty_model};
#[test]
fn fn_price_yield() {
@@ -108,6 +108,226 @@ fn fn_price_invalid_frequency() {
assert_eq!(model._get_text("B3"), *"#NUM!");
}
#[test]
fn fn_pricedisc() {
let mut model = new_empty_model();
model._set("A2", "=DATE(2022,1,25)");
model._set("A3", "=DATE(2022,11,15)");
model._set("A4", "3.75%");
model._set("A5", "100");
model._set("B1", "=PRICEDISC(A2,A3,A4,A5)");
model._set("C1", "=PRICEDISC(A2,A3)");
model.evaluate();
assert_eq!(model._get_text("B1"), "96.979166667");
assert_eq!(model._get_text("C1"), *"#ERROR!");
}
#[test]
fn fn_pricemat() {
let mut model = new_empty_model();
model._set("A1", "=DATE(2019,2,15)");
model._set("A2", "=DATE(2025,4,13)");
model._set("A3", "=DATE(2018,11,11)");
model._set("A4", "5.75%");
model._set("A5", "6.5%");
model._set("B1", "=PRICEMAT(A1,A2,A3,A4,A5)");
model.evaluate();
assert_eq!(model._get_text("B1"), "96.271187821");
}
#[test]
fn fn_yielddisc() {
let mut model = new_empty_model();
model._set("A1", "=DATE(2022,1,25)");
model._set("A2", "=DATE(2022,11,15)");
model._set("A3", "97");
model._set("A4", "100");
model._set("B1", "=YIELDDISC(A1,A2,A3,A4)");
model.evaluate();
assert_eq!(model._get_text("B1"), "0.038393175");
}
#[test]
fn fn_yieldmat() {
let mut model = new_empty_model();
model._set("A1", "=DATE(2019,2,15)");
model._set("A2", "=DATE(2025,4,13)");
model._set("A3", "=DATE(2018,11,11)");
model._set("A4", "5.75%");
model._set("A5", "96.27");
model._set("B1", "=YIELDMAT(A1,A2,A3,A4,A5)");
model.evaluate();
assert_eq!(model._get_text("B1"), "0.065002762");
}
#[test]
fn fn_disc() {
let mut model = new_empty_model();
model._set("A1", "=DATE(2022,1,25)");
model._set("A2", "=DATE(2022,11,15)");
model._set("A3", "97");
model._set("A4", "100");
model._set("B1", "=DISC(A1,A2,A3,A4)");
model.evaluate();
assert_eq!(model._get_text("B1"), "0.037241379");
}
#[test]
fn fn_received() {
let mut model = new_empty_model();
model._set("A1", "=DATE(2020,1,1)");
model._set("A2", "=DATE(2023,6,30)");
model._set("A3", "20000");
model._set("A4", "5%");
model._set("A5", "3");
model._set("B1", "=RECEIVED(A1,A2,A3,A4,A5)");
model.evaluate();
assert_eq!(model._get_text("B1"), "24236.387782205");
}
#[test]
fn fn_intrate() {
let mut model = new_empty_model();
model._set("A1", "=DATE(2020,1,1)");
model._set("A2", "=DATE(2023,6,30)");
model._set("A3", "10000");
model._set("A4", "12000");
model._set("A5", "3");
model._set("B1", "=INTRATE(A1,A2,A3,A4,A5)");
model.evaluate();
assert_eq!(model._get_text("B1"), "0.057210031");
}
#[test]
fn fn_bond_functions_arguments() {
let mut model = new_empty_model();
// PRICEDISC: 4-5 args
model._set("A1", "=PRICEDISC()");
model._set("A2", "=PRICEDISC(1,2,3)");
model._set("A3", "=PRICEDISC(1,2,3,4,5,6)");
// PRICEMAT: 5-6 args
model._set("B1", "=PRICEMAT()");
model._set("B2", "=PRICEMAT(1,2,3,4)");
model._set("B3", "=PRICEMAT(1,2,3,4,5,6,7)");
// YIELDDISC: 4-5 args
model._set("C1", "=YIELDDISC()");
model._set("C2", "=YIELDDISC(1,2,3)");
model._set("C3", "=YIELDDISC(1,2,3,4,5,6)");
// YIELDMAT: 5-6 args
model._set("D1", "=YIELDMAT()");
model._set("D2", "=YIELDMAT(1,2,3,4)");
model._set("D3", "=YIELDMAT(1,2,3,4,5,6,7)");
// DISC: 4-5 args
model._set("E1", "=DISC()");
model._set("E2", "=DISC(1,2,3)");
model._set("E3", "=DISC(1,2,3,4,5,6)");
// RECEIVED: 4-5 args
model._set("F1", "=RECEIVED()");
model._set("F2", "=RECEIVED(1,2,3)");
model._set("F3", "=RECEIVED(1,2,3,4,5,6)");
// INTRATE: 4-5 args
model._set("G1", "=INTRATE()");
model._set("G2", "=INTRATE(1,2,3)");
model._set("G3", "=INTRATE(1,2,3,4,5,6)");
model.evaluate();
assert_eq!(model._get_text("A1"), *"#ERROR!");
assert_eq!(model._get_text("A2"), *"#ERROR!");
assert_eq!(model._get_text("A3"), *"#ERROR!");
assert_eq!(model._get_text("B1"), *"#ERROR!");
assert_eq!(model._get_text("B2"), *"#ERROR!");
assert_eq!(model._get_text("B3"), *"#ERROR!");
assert_eq!(model._get_text("C1"), *"#ERROR!");
assert_eq!(model._get_text("C2"), *"#ERROR!");
assert_eq!(model._get_text("C3"), *"#ERROR!");
assert_eq!(model._get_text("D1"), *"#ERROR!");
assert_eq!(model._get_text("D2"), *"#ERROR!");
assert_eq!(model._get_text("D3"), *"#ERROR!");
assert_eq!(model._get_text("E1"), *"#ERROR!");
assert_eq!(model._get_text("E2"), *"#ERROR!");
assert_eq!(model._get_text("E3"), *"#ERROR!");
assert_eq!(model._get_text("F1"), *"#ERROR!");
assert_eq!(model._get_text("F2"), *"#ERROR!");
assert_eq!(model._get_text("F3"), *"#ERROR!");
assert_eq!(model._get_text("G1"), *"#ERROR!");
assert_eq!(model._get_text("G2"), *"#ERROR!");
assert_eq!(model._get_text("G3"), *"#ERROR!");
}
#[test]
fn fn_bond_functions_date_boundaries() {
let mut model = new_empty_model();
// Date boundary values
model._set("A1", "0"); // Below MINIMUM_DATE_SERIAL_NUMBER
model._set("A2", "1"); // MINIMUM_DATE_SERIAL_NUMBER
model._set("A3", "2958465"); // MAXIMUM_DATE_SERIAL_NUMBER
model._set("A4", "2958466"); // Above MAXIMUM_DATE_SERIAL_NUMBER
// Test settlement < minimum
model._set("B1", "=PRICEDISC(A1,A2,0.05,100)");
model._set("B2", "=YIELDDISC(A1,A2,95,100)");
model._set("B3", "=DISC(A1,A2,95,100)");
model._set("B4", "=RECEIVED(A1,A2,1000,0.05)");
model._set("B5", "=INTRATE(A1,A2,1000,1050)");
// Test maturity > maximum
model._set("C1", "=PRICEDISC(A2,A4,0.05,100)");
model._set("C2", "=YIELDDISC(A2,A4,95,100)");
model._set("C3", "=DISC(A2,A4,95,100)");
model._set("C4", "=RECEIVED(A2,A4,1000,0.05)");
model._set("C5", "=INTRATE(A2,A4,1000,1050)");
// Test PRICEMAT/YIELDMAT with issue < minimum
model._set("D1", "=PRICEMAT(A2,A3,A1,0.06,0.05)");
model._set("D2", "=YIELDMAT(A2,A3,A1,0.06,99)");
// Test PRICEMAT/YIELDMAT with issue > maximum
model._set("E1", "=PRICEMAT(A2,A3,A4,0.06,0.05)");
model._set("E2", "=YIELDMAT(A2,A3,A4,0.06,99)");
model.evaluate();
assert_eq!(model._get_text("B1"), *"#NUM!");
assert_eq!(model._get_text("B2"), *"#NUM!");
assert_eq!(model._get_text("B3"), *"#NUM!");
}
#[test]
fn fn_yield_invalid_frequency() {
let mut model = new_empty_model();
@@ -125,6 +345,35 @@ fn fn_yield_invalid_frequency() {
assert_eq!(model._get_text("B3"), *"#NUM!");
}
#[test]
fn fn_bond_functions_date_ordering() {
let mut model = new_empty_model();
model._set("A1", "=DATE(2022,1,1)"); // settlement
model._set("A2", "=DATE(2021,12,31)"); // maturity (before settlement)
model._set("A3", "=DATE(2020,1,1)"); // issue
// Test settlement >= maturity
model._set("B1", "=PRICEDISC(A1,A2,0.05,100)");
model._set("B2", "=YIELDDISC(A1,A2,95,100)");
model._set("B3", "=DISC(A1,A2,95,100)");
model._set("B4", "=RECEIVED(A1,A2,1000,0.05)");
model._set("B5", "=INTRATE(A1,A2,1000,1050)");
model._set("B6", "=PRICEMAT(A1,A2,A3,0.06,0.05)");
model._set("B7", "=YIELDMAT(A1,A2,A3,0.06,99)");
// Test settlement < issue for YIELDMAT/PRICEMAT
model._set("A4", "=DATE(2023,1,1)"); // later issue date
model._set("C1", "=PRICEMAT(A1,A2,A4,0.06,0.05)"); // settlement < issue
model._set("C2", "=YIELDMAT(A1,A2,A4,0.06,99)"); // settlement < issue
model.evaluate();
assert_eq!(model._get_text("B1"), *"#NUM!");
assert_eq!(model._get_text("B2"), *"#NUM!");
assert_eq!(model._get_text("B3"), *"#NUM!");
}
#[test]
fn fn_price_invalid_dates() {
let mut model = new_empty_model();
@@ -140,6 +389,47 @@ fn fn_price_invalid_dates() {
assert_eq!(model._get_text("B2"), *"#NUM!");
}
#[test]
fn fn_bond_functions_parameter_validation() {
let mut model = new_empty_model();
model._set("A1", "=DATE(2022,1,1)");
model._set("A2", "=DATE(2022,12,31)");
model._set("A3", "=DATE(2021,1,1)");
// Test negative/zero prices and redemptions
model._set("B1", "=PRICEDISC(A1,A2,0.05,0)"); // zero redemption
model._set("B2", "=PRICEDISC(A1,A2,0,100)"); // zero discount
model._set("B3", "=PRICEDISC(A1,A2,-0.05,100)"); // negative discount
model._set("C1", "=YIELDDISC(A1,A2,0,100)"); // zero price
model._set("C2", "=YIELDDISC(A1,A2,95,0)"); // zero redemption
model._set("C3", "=YIELDDISC(A1,A2,-95,100)"); // negative price
model._set("D1", "=DISC(A1,A2,0,100)"); // zero price
model._set("D2", "=DISC(A1,A2,95,0)"); // zero redemption
model._set("D3", "=DISC(A1,A2,-95,100)"); // negative price
model._set("E1", "=RECEIVED(A1,A2,0,0.05)"); // zero investment
model._set("E2", "=RECEIVED(A1,A2,1000,0)"); // zero discount
model._set("E3", "=RECEIVED(A1,A2,-1000,0.05)"); // negative investment
model._set("F1", "=INTRATE(A1,A2,0,1050)"); // zero investment
model._set("F2", "=INTRATE(A1,A2,1000,0)"); // zero redemption
model._set("F3", "=INTRATE(A1,A2,-1000,1050)"); // negative investment
model._set("G1", "=PRICEMAT(A1,A2,A3,-0.06,0.05)"); // negative rate
model._set("G2", "=PRICEMAT(A1,A2,A3,0.06,-0.05)"); // negative yield
model._set("H1", "=YIELDMAT(A1,A2,A3,0.06,0)"); // zero price
model._set("H2", "=YIELDMAT(A1,A2,A3,-0.06,99)"); // negative rate
model.evaluate();
assert_eq!(model._get_text("B1"), *"#NUM!");
assert_eq!(model._get_text("B2"), *"#NUM!");
}
#[test]
fn fn_yield_invalid_dates() {
let mut model = new_empty_model();
@@ -237,3 +527,89 @@ fn fn_price_yield_round_trip_stability() {
"Round-trip should be stable: {price1} vs {price2}"
);
}
#[test]
fn fn_bond_functions_basis_validation() {
let mut model = new_empty_model();
model._set("A1", "=DATE(2022,1,1)");
model._set("A2", "=DATE(2022,12,31)");
model._set("A3", "=DATE(2021,1,1)");
// Test valid basis values (0-4)
model._set("B1", "=PRICEDISC(A1,A2,0.05,100,0)");
model._set("B2", "=PRICEDISC(A1,A2,0.05,100,1)");
model._set("B3", "=PRICEDISC(A1,A2,0.05,100,2)");
model._set("B4", "=PRICEDISC(A1,A2,0.05,100,3)");
model._set("B5", "=PRICEDISC(A1,A2,0.05,100,4)");
// Test invalid basis values
model._set("C1", "=PRICEDISC(A1,A2,0.05,100,-1)");
model._set("C2", "=PRICEDISC(A1,A2,0.05,100,5)");
model._set("C3", "=YIELDDISC(A1,A2,95,100,10)");
model._set("C4", "=DISC(A1,A2,95,100,-5)");
model._set("C5", "=RECEIVED(A1,A2,1000,0.05,99)");
model._set("C6", "=INTRATE(A1,A2,1000,1050,-2)");
model._set("C7", "=PRICEMAT(A1,A2,A3,0.06,0.05,7)");
model._set("C8", "=YIELDMAT(A1,A2,A3,0.06,99,-3)");
model.evaluate();
// Valid basis should work
assert_ne!(model._get_text("B1"), *"#ERROR!");
assert_ne!(model._get_text("B2"), *"#ERROR!");
assert_ne!(model._get_text("B3"), *"#ERROR!");
assert_ne!(model._get_text("B4"), *"#ERROR!");
assert_ne!(model._get_text("B5"), *"#ERROR!");
// Invalid basis should error
assert_eq!(model._get_text("C1"), *"#NUM!");
assert_eq!(model._get_text("C2"), *"#NUM!");
assert_eq!(model._get_text("C3"), *"#NUM!");
assert_eq!(model._get_text("C4"), *"#NUM!");
assert_eq!(model._get_text("C5"), *"#NUM!");
assert_eq!(model._get_text("C6"), *"#NUM!");
assert_eq!(model._get_text("C7"), *"#NUM!");
assert_eq!(model._get_text("C8"), *"#NUM!");
}
#[test]
fn fn_bond_functions_relationships() {
// Test mathematical relationships between functions
let mut model = new_empty_model();
model._set("A1", "=DATE(2021,1,1)");
model._set("A2", "=DATE(2021,7,1)");
model._set("B1", "=PRICEDISC(A1,A2,5%,100)");
model._set("B2", "=YIELDDISC(A1,A2,B1,100)");
model._set("B3", "=DISC(A1,A2,B1,100)");
model._set("B4", "=RECEIVED(A1,A2,1000,5%)");
model._set("B5", "=INTRATE(A1,A2,1000,1050)");
model._set("B6", "=PRICEMAT(A1,A2,DATE(2020,7,1),6%,5%)");
model._set("B7", "=YIELDMAT(A1,A2,DATE(2020,7,1),6%,99)");
model.evaluate();
assert_eq!(
model.get_cell_value_by_ref("Sheet1!B1"),
Ok(CellValue::Number(97.5))
);
if let Ok(CellValue::Number(v)) = model.get_cell_value_by_ref("Sheet1!B2") {
assert!((v - 0.051282051).abs() < 1e-6);
}
if let Ok(CellValue::Number(v)) = model.get_cell_value_by_ref("Sheet1!B3") {
assert!((v - 0.05).abs() < 1e-6);
}
if let Ok(CellValue::Number(v)) = model.get_cell_value_by_ref("Sheet1!B4") {
assert!((v - 1025.641025).abs() < 1e-6);
}
if let Ok(CellValue::Number(v)) = model.get_cell_value_by_ref("Sheet1!B5") {
assert!((v - 0.10).abs() < 1e-6);
}
if let Ok(CellValue::Number(v)) = model.get_cell_value_by_ref("Sheet1!B6") {
assert!((v - 100.414634).abs() < 1e-6);
}
if let Ok(CellValue::Number(v)) = model.get_cell_value_by_ref("Sheet1!B7") {
assert!((v - 0.078431372).abs() < 1e-6);
}
}