merge coupdaybs, coupdays, coupdaysnc, coupncd, coupnum, couppcd #59
This commit is contained in:
committed by
Nicolás Hatcher
parent
15b67323ed
commit
895fb649da
@@ -42,36 +42,39 @@ fn is_less_than_one_year(start_date: i64, end_date: i64) -> Result<bool, String>
|
||||
}
|
||||
|
||||
fn is_leap_year(year: i32) -> bool {
|
||||
(year % 4 == 0 && year % 100 != 0) || year % 400 == 0
|
||||
(year % 4 == 0) && (year % 100 != 0 || year % 400 == 0)
|
||||
}
|
||||
|
||||
fn is_last_day_of_feb(date: chrono::NaiveDate) -> bool {
|
||||
date.month() == 2 && date.day() == if is_leap_year(date.year()) { 29 } else { 28 }
|
||||
}
|
||||
|
||||
fn days360_us(start: chrono::NaiveDate, end: chrono::NaiveDate) -> i32 {
|
||||
let mut d1 = start.day() as i32;
|
||||
let 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();
|
||||
let m1 = start.month() as i32;
|
||||
let m2 = end.month() as i32;
|
||||
let y1 = start.year();
|
||||
let y2 = end.year();
|
||||
|
||||
if d1 == 31 || (m1 == 2 && (d1 == 29 || (d1 == 28 && !is_leap_year(y1)))) {
|
||||
// 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 = 30;
|
||||
}
|
||||
|
||||
// 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 = 30;
|
||||
}
|
||||
|
||||
if d2 == 31 {
|
||||
if d1 != 30 {
|
||||
d2 = 1;
|
||||
if m2 == 12 {
|
||||
y2 += 1;
|
||||
m2 = 1;
|
||||
} else {
|
||||
m2 += 1;
|
||||
}
|
||||
} else {
|
||||
d2 = 30;
|
||||
}
|
||||
// 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 == 30 && d2 == 31 {
|
||||
d2 = 30;
|
||||
}
|
||||
|
||||
d2 + m2 * 30 + y2 * 360 - d1 - m1 * 30 - y1 * 360
|
||||
360 * (y2 - y1) + 30 * (m2 - m1) + (d2 - d1)
|
||||
}
|
||||
|
||||
fn days360_eu(start: chrono::NaiveDate, end: chrono::NaiveDate) -> i32 {
|
||||
@@ -99,12 +102,24 @@ fn days_30us_360(start: chrono::NaiveDate, end: chrono::NaiveDate) -> i32 {
|
||||
let m2 = end.month() as i32;
|
||||
let y1 = start.year();
|
||||
let y2 = end.year();
|
||||
if d1 == 31 {
|
||||
d1 = 30;
|
||||
}
|
||||
if d2 == 31 && (d1 == 30 || d1 == 31) {
|
||||
|
||||
// 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 = 30;
|
||||
}
|
||||
|
||||
// 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 = 30;
|
||||
}
|
||||
|
||||
// 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 == 30 && d2 == 31 {
|
||||
d2 = 30;
|
||||
}
|
||||
|
||||
(y2 - y1) * 360 + (m2 - m1) * 30 + (d2 - d1)
|
||||
}
|
||||
|
||||
@@ -177,6 +192,34 @@ fn year_fraction(
|
||||
Ok(days)
|
||||
}
|
||||
|
||||
fn days_between_dates(start: chrono::NaiveDate, end: chrono::NaiveDate, basis: i32) -> i32 {
|
||||
match basis {
|
||||
0 => days360_us(start, end),
|
||||
1 | 2 => (end - start).num_days() as i32,
|
||||
3 => (end - start).num_days() as i32,
|
||||
4 => days360_eu(start, end),
|
||||
_ => (end - start).num_days() as i32,
|
||||
}
|
||||
}
|
||||
|
||||
fn coupon_dates(
|
||||
settlement: chrono::NaiveDate,
|
||||
maturity: chrono::NaiveDate,
|
||||
freq: i32,
|
||||
) -> (chrono::NaiveDate, chrono::NaiveDate) {
|
||||
let months = 12 / freq;
|
||||
let step = chrono::Months::new(months as u32);
|
||||
let mut ncd = maturity;
|
||||
while let Some(prev) = ncd.checked_sub_months(step) {
|
||||
if settlement >= prev {
|
||||
return (prev, ncd);
|
||||
}
|
||||
ncd = prev;
|
||||
}
|
||||
// Fallback if we somehow exit the loop (shouldn't happen in practice)
|
||||
(settlement, maturity)
|
||||
}
|
||||
|
||||
fn compute_payment(
|
||||
rate: f64,
|
||||
nper: f64,
|
||||
@@ -2477,6 +2520,316 @@ impl Model {
|
||||
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
|
||||
};
|
||||
|
||||
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);
|
||||
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
|
||||
};
|
||||
|
||||
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 => 360 / frequency, // 30/360 conventions
|
||||
_ => days_between_dates(pcd, ncd, 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
|
||||
};
|
||||
|
||||
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);
|
||||
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
|
||||
};
|
||||
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
};
|
||||
|
||||
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 step = chrono::Months::new(months as u32);
|
||||
let mut date = maturity_date;
|
||||
let mut count = 0;
|
||||
while settlement_date < date {
|
||||
count += 1;
|
||||
date = match date.checked_sub_months(step) {
|
||||
Some(new_date) => new_date,
|
||||
None => break, // Safety check to avoid infinite loop
|
||||
};
|
||||
}
|
||||
CalcResult::Number(count as f64)
|
||||
}
|
||||
|
||||
// COUPPCD(settlement, maturity, frequency, [basis])
|
||||
pub(crate) fn fn_couppcd(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
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
|
||||
};
|
||||
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
// DOLLARDE(fractional_dollar, fraction)
|
||||
pub(crate) fn fn_dollarde(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 2 {
|
||||
|
||||
Reference in New Issue
Block a user