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

@@ -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 {