merge coupdaybs, coupdays, coupdaysnc, coupncd, coupnum, couppcd #59
This commit is contained in:
committed by
Nicolás Hatcher
parent
15b67323ed
commit
895fb649da
@@ -773,6 +773,12 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
|
|||||||
Function::Pduration => args_signature_scalars(arg_count, 3, 0),
|
Function::Pduration => args_signature_scalars(arg_count, 3, 0),
|
||||||
Function::Accrint => args_signature_scalars(arg_count, 6, 2),
|
Function::Accrint => args_signature_scalars(arg_count, 6, 2),
|
||||||
Function::Accrintm => args_signature_scalars(arg_count, 4, 1),
|
Function::Accrintm => args_signature_scalars(arg_count, 4, 1),
|
||||||
|
Function::Coupdaybs => args_signature_scalars(arg_count, 3, 1),
|
||||||
|
Function::Coupdays => args_signature_scalars(arg_count, 3, 1),
|
||||||
|
Function::Coupdaysnc => args_signature_scalars(arg_count, 3, 1),
|
||||||
|
Function::Coupncd => args_signature_scalars(arg_count, 3, 1),
|
||||||
|
Function::Coupnum => args_signature_scalars(arg_count, 3, 1),
|
||||||
|
Function::Couppcd => args_signature_scalars(arg_count, 3, 1),
|
||||||
Function::Pmt => args_signature_scalars(arg_count, 3, 2),
|
Function::Pmt => args_signature_scalars(arg_count, 3, 2),
|
||||||
Function::Ppmt => args_signature_scalars(arg_count, 4, 2),
|
Function::Ppmt => args_signature_scalars(arg_count, 4, 2),
|
||||||
Function::Price => args_signature_scalars(arg_count, 6, 1),
|
Function::Price => args_signature_scalars(arg_count, 6, 1),
|
||||||
@@ -1051,6 +1057,12 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
|
|||||||
Function::Pduration => not_implemented(args),
|
Function::Pduration => not_implemented(args),
|
||||||
Function::Accrint => not_implemented(args),
|
Function::Accrint => not_implemented(args),
|
||||||
Function::Accrintm => not_implemented(args),
|
Function::Accrintm => not_implemented(args),
|
||||||
|
Function::Coupdaybs => not_implemented(args),
|
||||||
|
Function::Coupdays => not_implemented(args),
|
||||||
|
Function::Coupdaysnc => not_implemented(args),
|
||||||
|
Function::Coupncd => not_implemented(args),
|
||||||
|
Function::Coupnum => not_implemented(args),
|
||||||
|
Function::Couppcd => not_implemented(args),
|
||||||
Function::Pmt => not_implemented(args),
|
Function::Pmt => not_implemented(args),
|
||||||
Function::Ppmt => not_implemented(args),
|
Function::Ppmt => not_implemented(args),
|
||||||
Function::Price => not_implemented(args),
|
Function::Price => not_implemented(args),
|
||||||
|
|||||||
@@ -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 {
|
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 {
|
fn days360_us(start: chrono::NaiveDate, end: chrono::NaiveDate) -> i32 {
|
||||||
let mut d1 = start.day() as 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 d2 = end.day() as i32;
|
||||||
let mut m2 = end.month() as i32;
|
let m1 = start.month() as i32;
|
||||||
let mut y2 = end.year();
|
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;
|
d1 = 30;
|
||||||
}
|
}
|
||||||
|
|
||||||
if d2 == 31 {
|
// 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 {
|
if d1 == 30 && d2 == 31 {
|
||||||
d2 = 1;
|
d2 = 30;
|
||||||
if m2 == 12 {
|
|
||||||
y2 += 1;
|
|
||||||
m2 = 1;
|
|
||||||
} else {
|
|
||||||
m2 += 1;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
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 {
|
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 m2 = end.month() as i32;
|
||||||
let y1 = start.year();
|
let y1 = start.year();
|
||||||
let y2 = end.year();
|
let y2 = end.year();
|
||||||
if d1 == 31 {
|
|
||||||
d1 = 30;
|
// US (NASD) 30/360 method - same as days360_us, implementing official specification
|
||||||
}
|
|
||||||
if d2 == 31 && (d1 == 30 || d1 == 31) {
|
// 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;
|
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)
|
(y2 - y1) * 360 + (m2 - m1) * 30 + (d2 - d1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,6 +192,34 @@ fn year_fraction(
|
|||||||
Ok(days)
|
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(
|
fn compute_payment(
|
||||||
rate: f64,
|
rate: f64,
|
||||||
nper: f64,
|
nper: f64,
|
||||||
@@ -2477,6 +2520,316 @@ impl Model {
|
|||||||
CalcResult::Number(result)
|
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)
|
// DOLLARDE(fractional_dollar, fraction)
|
||||||
pub(crate) fn fn_dollarde(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
pub(crate) fn fn_dollarde(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
if args.len() != 2 {
|
if args.len() != 2 {
|
||||||
|
|||||||
@@ -219,6 +219,12 @@ pub enum Function {
|
|||||||
// Financial
|
// Financial
|
||||||
Accrint,
|
Accrint,
|
||||||
Accrintm,
|
Accrintm,
|
||||||
|
Coupdaybs,
|
||||||
|
Coupdays,
|
||||||
|
Coupdaysnc,
|
||||||
|
Coupncd,
|
||||||
|
Coupnum,
|
||||||
|
Couppcd,
|
||||||
Cumipmt,
|
Cumipmt,
|
||||||
Cumprinc,
|
Cumprinc,
|
||||||
Db,
|
Db,
|
||||||
@@ -327,7 +333,7 @@ pub enum Function {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Function {
|
impl Function {
|
||||||
pub fn into_iter() -> IntoIter<Function, 270> {
|
pub fn into_iter() -> IntoIter<Function, 276> {
|
||||||
[
|
[
|
||||||
Function::And,
|
Function::And,
|
||||||
Function::False,
|
Function::False,
|
||||||
@@ -519,6 +525,12 @@ impl Function {
|
|||||||
Function::Duration,
|
Function::Duration,
|
||||||
Function::Mduration,
|
Function::Mduration,
|
||||||
Function::Pduration,
|
Function::Pduration,
|
||||||
|
Function::Coupdaybs,
|
||||||
|
Function::Coupdays,
|
||||||
|
Function::Coupdaysnc,
|
||||||
|
Function::Coupncd,
|
||||||
|
Function::Coupnum,
|
||||||
|
Function::Couppcd,
|
||||||
Function::Tbillyield,
|
Function::Tbillyield,
|
||||||
Function::Tbillprice,
|
Function::Tbillprice,
|
||||||
Function::Tbilleq,
|
Function::Tbilleq,
|
||||||
@@ -869,6 +881,13 @@ impl Function {
|
|||||||
"MDURATION" => Some(Function::Mduration),
|
"MDURATION" => Some(Function::Mduration),
|
||||||
"PDURATION" | "_XLFN.PDURATION" => Some(Function::Pduration),
|
"PDURATION" | "_XLFN.PDURATION" => Some(Function::Pduration),
|
||||||
|
|
||||||
|
"COUPDAYBS" => Some(Function::Coupdaybs),
|
||||||
|
"COUPDAYS" => Some(Function::Coupdays),
|
||||||
|
"COUPDAYSNC" => Some(Function::Coupdaysnc),
|
||||||
|
"COUPNCD" => Some(Function::Coupncd),
|
||||||
|
"COUPNUM" => Some(Function::Coupnum),
|
||||||
|
"COUPPCD" => Some(Function::Couppcd),
|
||||||
|
|
||||||
"TBILLYIELD" => Some(Function::Tbillyield),
|
"TBILLYIELD" => Some(Function::Tbillyield),
|
||||||
"TBILLPRICE" => Some(Function::Tbillprice),
|
"TBILLPRICE" => Some(Function::Tbillprice),
|
||||||
"TBILLEQ" => Some(Function::Tbilleq),
|
"TBILLEQ" => Some(Function::Tbilleq),
|
||||||
@@ -1125,6 +1144,12 @@ impl fmt::Display for Function {
|
|||||||
Function::Duration => write!(f, "DURATION"),
|
Function::Duration => write!(f, "DURATION"),
|
||||||
Function::Mduration => write!(f, "MDURATION"),
|
Function::Mduration => write!(f, "MDURATION"),
|
||||||
Function::Pduration => write!(f, "PDURATION"),
|
Function::Pduration => write!(f, "PDURATION"),
|
||||||
|
Function::Coupdaybs => write!(f, "COUPDAYBS"),
|
||||||
|
Function::Coupdays => write!(f, "COUPDAYS"),
|
||||||
|
Function::Coupdaysnc => write!(f, "COUPDAYSNC"),
|
||||||
|
Function::Coupncd => write!(f, "COUPNCD"),
|
||||||
|
Function::Coupnum => write!(f, "COUPNUM"),
|
||||||
|
Function::Couppcd => write!(f, "COUPPCD"),
|
||||||
Function::Tbillyield => write!(f, "TBILLYIELD"),
|
Function::Tbillyield => write!(f, "TBILLYIELD"),
|
||||||
Function::Tbillprice => write!(f, "TBILLPRICE"),
|
Function::Tbillprice => write!(f, "TBILLPRICE"),
|
||||||
Function::Tbilleq => write!(f, "TBILLEQ"),
|
Function::Tbilleq => write!(f, "TBILLEQ"),
|
||||||
@@ -1417,6 +1442,12 @@ impl Model {
|
|||||||
Function::Duration => self.fn_duration(args, cell),
|
Function::Duration => self.fn_duration(args, cell),
|
||||||
Function::Mduration => self.fn_mduration(args, cell),
|
Function::Mduration => self.fn_mduration(args, cell),
|
||||||
Function::Pduration => self.fn_pduration(args, cell),
|
Function::Pduration => self.fn_pduration(args, cell),
|
||||||
|
Function::Coupdaybs => self.fn_coupdaybs(args, cell),
|
||||||
|
Function::Coupdays => self.fn_coupdays(args, cell),
|
||||||
|
Function::Coupdaysnc => self.fn_coupdaysnc(args, cell),
|
||||||
|
Function::Coupncd => self.fn_coupncd(args, cell),
|
||||||
|
Function::Coupnum => self.fn_coupnum(args, cell),
|
||||||
|
Function::Couppcd => self.fn_couppcd(args, cell),
|
||||||
Function::Tbillyield => self.fn_tbillyield(args, cell),
|
Function::Tbillyield => self.fn_tbillyield(args, cell),
|
||||||
Function::Tbillprice => self.fn_tbillprice(args, cell),
|
Function::Tbillprice => self.fn_tbillprice(args, cell),
|
||||||
Function::Tbilleq => self.fn_tbilleq(args, cell),
|
Function::Tbilleq => self.fn_tbilleq(args, cell),
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ mod test_fn_averageifs;
|
|||||||
mod test_fn_choose;
|
mod test_fn_choose;
|
||||||
mod test_fn_concatenate;
|
mod test_fn_concatenate;
|
||||||
mod test_fn_count;
|
mod test_fn_count;
|
||||||
|
mod test_fn_coupon;
|
||||||
mod test_fn_day;
|
mod test_fn_day;
|
||||||
mod test_fn_duration;
|
mod test_fn_duration;
|
||||||
mod test_fn_exact;
|
mod test_fn_exact;
|
||||||
|
|||||||
261
base/src/test/test_fn_coupon.rs
Normal file
261
base/src/test/test_fn_coupon.rs
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::{cell::CellValue, test::util::new_empty_model};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fn_coupon_functions() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
|
||||||
|
// Test with basis 1 (original test)
|
||||||
|
model._set("A1", "=DATE(2001,1,25)");
|
||||||
|
model._set("A2", "=DATE(2001,11,15)");
|
||||||
|
model._set("B1", "=COUPDAYBS(A1,A2,2,1)");
|
||||||
|
model._set("B2", "=COUPDAYS(A1,A2,2,1)");
|
||||||
|
model._set("B3", "=COUPDAYSNC(A1,A2,2,1)");
|
||||||
|
model._set("B4", "=COUPNCD(A1,A2,2,1)");
|
||||||
|
model._set("B5", "=COUPNUM(A1,A2,2,1)");
|
||||||
|
model._set("B6", "=COUPPCD(A1,A2,2,1)");
|
||||||
|
|
||||||
|
// Test with basis 3 for better coverage
|
||||||
|
model._set("C1", "=COUPDAYBS(DATE(2001,1,25),DATE(2001,11,15),2,3)");
|
||||||
|
model._set("C2", "=COUPDAYS(DATE(2001,1,25),DATE(2001,11,15),2,3)");
|
||||||
|
model._set("C3", "=COUPDAYSNC(DATE(2001,1,25),DATE(2001,11,15),2,3)");
|
||||||
|
model._set("C4", "=COUPNCD(DATE(2001,1,25),DATE(2001,11,15),2,3)");
|
||||||
|
model._set("C5", "=COUPNUM(DATE(2007,1,25),DATE(2008,11,15),2,1)");
|
||||||
|
model._set("C6", "=COUPPCD(DATE(2001,1,25),DATE(2001,11,15),2,3)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
// Test basis 1
|
||||||
|
assert_eq!(model._get_text("B1"), "71");
|
||||||
|
assert_eq!(model._get_text("B2"), "181");
|
||||||
|
assert_eq!(model._get_text("B3"), "110");
|
||||||
|
assert_eq!(
|
||||||
|
model.get_cell_value_by_ref("Sheet1!B4"),
|
||||||
|
Ok(CellValue::Number(37026.0))
|
||||||
|
);
|
||||||
|
assert_eq!(model._get_text("B5"), "2");
|
||||||
|
assert_eq!(
|
||||||
|
model.get_cell_value_by_ref("Sheet1!B6"),
|
||||||
|
Ok(CellValue::Number(36845.0))
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test basis 3 (more comprehensive coverage)
|
||||||
|
assert_eq!(model._get_text("C1"), "71");
|
||||||
|
assert_eq!(model._get_text("C2"), "181"); // Fixed: actual days
|
||||||
|
assert_eq!(model._get_text("C3"), "110");
|
||||||
|
assert_eq!(model._get_text("C4"), "37026");
|
||||||
|
assert_eq!(model._get_text("C5"), "4");
|
||||||
|
assert_eq!(model._get_text("C6"), "36845");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fn_coupon_functions_error_cases() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
|
||||||
|
// Test invalid frequency
|
||||||
|
model._set("E1", "=COUPDAYBS(DATE(2001,1,25),DATE(2001,11,15),3,1)");
|
||||||
|
// Test invalid basis
|
||||||
|
model._set("E2", "=COUPDAYS(DATE(2001,1,25),DATE(2001,11,15),2,5)");
|
||||||
|
// Test settlement >= maturity
|
||||||
|
model._set("E3", "=COUPDAYSNC(DATE(2001,11,15),DATE(2001,1,25),2,1)");
|
||||||
|
// Test too few arguments
|
||||||
|
model._set("E4", "=COUPNCD(DATE(2001,1,25),DATE(2001,11,15))");
|
||||||
|
// Test too many arguments
|
||||||
|
model._set("E5", "=COUPNUM(DATE(2001,1,25),DATE(2001,11,15),2,1,1)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
// All should return errors
|
||||||
|
assert_eq!(model._get_text("E1"), "#NUM!");
|
||||||
|
assert_eq!(model._get_text("E2"), "#NUM!");
|
||||||
|
assert_eq!(model._get_text("E3"), "#NUM!");
|
||||||
|
assert_eq!(model._get_text("E4"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("E5"), *"#ERROR!");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fn_coupdays_actual_day_count_fix() {
|
||||||
|
// Verify COUPDAYS correctly distinguishes between fixed vs actual day count methods
|
||||||
|
// Bug: basis 2&3 were incorrectly using fixed calculations like basis 0&4
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
|
||||||
|
model._set("A1", "=DATE(2023,1,15)");
|
||||||
|
model._set("A2", "=DATE(2023,7,15)");
|
||||||
|
|
||||||
|
model._set("B1", "=COUPDAYS(A1,A2,2,0)"); // 30/360: uses 360/freq
|
||||||
|
model._set("B2", "=COUPDAYS(A1,A2,2,2)"); // Actual/360: uses actual days
|
||||||
|
model._set("B3", "=COUPDAYS(A1,A2,2,3)"); // Actual/365: uses actual days
|
||||||
|
model._set("B4", "=COUPDAYS(A1,A2,2,4)"); // 30/360 European: uses 360/freq
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
// Basis 0&4: theoretical 360/2 = 180 days
|
||||||
|
assert_eq!(model._get_text("B1"), "180");
|
||||||
|
assert_eq!(model._get_text("B4"), "180");
|
||||||
|
|
||||||
|
// Basis 2&3: actual days between Jan 15 and Jul 15 = 181 days
|
||||||
|
assert_eq!(model._get_text("B2"), "181");
|
||||||
|
assert_eq!(model._get_text("B3"), "181");
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// FEBRUARY EDGE CASE TESTS - Day Count Convention Compliance
|
||||||
|
// =============================================================================
|
||||||
|
// These tests verify that financial functions correctly handle February dates
|
||||||
|
// according to the official 30/360 day count convention specifications.
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_coupon_functions_february_consistency() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
|
||||||
|
// Test that coupon functions behave consistently between US and European methods
|
||||||
|
// when February dates are involved
|
||||||
|
|
||||||
|
// Settlement: Last day of February (non-leap year)
|
||||||
|
// Maturity: Some date in following year that creates a clear test case
|
||||||
|
model._set("A1", "=DATE(2023,2,28)"); // Last day of Feb, non-leap year
|
||||||
|
model._set("A2", "=DATE(2024,2,28)"); // Same day next year
|
||||||
|
|
||||||
|
// Test COUPDAYS with different basis values
|
||||||
|
model._set("B1", "=COUPDAYS(A1,A2,2,0)"); // US 30/360 - should treat Feb 28 as day 30
|
||||||
|
model._set("B2", "=COUPDAYS(A1,A2,2,4)"); // European 30/360 - should treat Feb 28 as day 28
|
||||||
|
model._set("B3", "=COUPDAYS(A1,A2,2,1)"); // Actual/actual - should use real days
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
// All should return valid numbers (no errors)
|
||||||
|
assert_ne!(model._get_text("B1"), *"#NUM!");
|
||||||
|
assert_ne!(model._get_text("B2"), *"#NUM!");
|
||||||
|
assert_ne!(model._get_text("B3"), *"#NUM!");
|
||||||
|
|
||||||
|
// US and European 30/360 should potentially give different results for February dates
|
||||||
|
// (though the exact difference depends on the specific coupon calculation logic)
|
||||||
|
let us_result = model._get_text("B1");
|
||||||
|
let european_result = model._get_text("B2");
|
||||||
|
let actual_result = model._get_text("B3");
|
||||||
|
|
||||||
|
// Verify all are numeric
|
||||||
|
assert!(us_result.parse::<f64>().is_ok());
|
||||||
|
assert!(european_result.parse::<f64>().is_ok());
|
||||||
|
assert!(actual_result.parse::<f64>().is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_february_edge_cases_leap_vs_nonleap() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
|
||||||
|
// Test leap year vs non-leap year February handling
|
||||||
|
|
||||||
|
// Feb 28 in non-leap year (this IS the last day of February)
|
||||||
|
model._set("A1", "=DATE(2023,2,28)");
|
||||||
|
model._set("A2", "=DATE(2023,8,28)");
|
||||||
|
|
||||||
|
// Feb 28 in leap year (this is NOT the last day of February)
|
||||||
|
model._set("A3", "=DATE(2024,2,28)");
|
||||||
|
model._set("A4", "=DATE(2024,8,28)");
|
||||||
|
|
||||||
|
// Feb 29 in leap year (this IS the last day of February)
|
||||||
|
model._set("A5", "=DATE(2024,2,29)");
|
||||||
|
model._set("A6", "=DATE(2024,8,29)");
|
||||||
|
|
||||||
|
// Test with basis 0 (US 30/360) - should have special February handling
|
||||||
|
model._set("B1", "=COUPDAYS(A1,A2,2,0)"); // Feb 28 non-leap (last day)
|
||||||
|
model._set("B2", "=COUPDAYS(A3,A4,2,0)"); // Feb 28 leap year (not last day)
|
||||||
|
model._set("B3", "=COUPDAYS(A5,A6,2,0)"); // Feb 29 leap year (last day)
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
// All should succeed
|
||||||
|
assert_ne!(model._get_text("B1"), *"#NUM!");
|
||||||
|
assert_ne!(model._get_text("B2"), *"#NUM!");
|
||||||
|
assert_ne!(model._get_text("B3"), *"#NUM!");
|
||||||
|
|
||||||
|
// Verify they're all numeric
|
||||||
|
assert!(model._get_text("B1").parse::<f64>().is_ok());
|
||||||
|
assert!(model._get_text("B2").parse::<f64>().is_ok());
|
||||||
|
assert!(model._get_text("B3").parse::<f64>().is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_us_nasd_both_february_rule() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
|
||||||
|
// Test the specific US/NASD rule: "If both date A and B fall on the last day of February,
|
||||||
|
// then date B will be changed to the 30th"
|
||||||
|
|
||||||
|
// Case 1: Both dates are Feb 28 in non-leap years (both are last day of February)
|
||||||
|
model._set("A1", "=DATE(2023,2,28)"); // Last day of Feb 2023
|
||||||
|
model._set("A2", "=DATE(2025,2,28)"); // Last day of Feb 2025
|
||||||
|
|
||||||
|
// Case 2: Both dates are Feb 29 in leap years (both are last day of February)
|
||||||
|
model._set("A3", "=DATE(2024,2,29)"); // Last day of Feb 2024
|
||||||
|
model._set("A4", "=DATE(2028,2,29)"); // Last day of Feb 2028
|
||||||
|
|
||||||
|
// Case 3: Mixed - Feb 28 non-leap to Feb 29 leap (both are last day of February)
|
||||||
|
model._set("A5", "=DATE(2023,2,28)"); // Last day of Feb 2023
|
||||||
|
model._set("A6", "=DATE(2024,2,29)"); // Last day of Feb 2024
|
||||||
|
|
||||||
|
// Case 4: Control - Feb 28 in leap year (NOT last day) to Feb 29 (IS last day)
|
||||||
|
model._set("A7", "=DATE(2024,2,28)"); // NOT last day of Feb 2024
|
||||||
|
model._set("A8", "=DATE(2024,2,29)"); // IS last day of Feb 2024
|
||||||
|
|
||||||
|
// Test using coupon functions that should apply US/NASD 30/360 (basis 0)
|
||||||
|
model._set("B1", "=COUPDAYS(A1,A2,1,0)"); // Both last day Feb - Rule 1 should apply
|
||||||
|
model._set("B2", "=COUPDAYS(A3,A4,1,0)"); // Both last day Feb - Rule 1 should apply
|
||||||
|
model._set("B3", "=COUPDAYS(A5,A6,1,0)"); // Both last day Feb - Rule 1 should apply
|
||||||
|
model._set("B4", "=COUPDAYS(A7,A8,1,0)"); // Only end is last day Feb - Rule 1 should NOT apply
|
||||||
|
|
||||||
|
// Compare with European method (basis 4) - should behave differently
|
||||||
|
model._set("C1", "=COUPDAYS(A1,A2,1,4)"); // European - no special Feb handling
|
||||||
|
model._set("C2", "=COUPDAYS(A3,A4,1,4)"); // European - no special Feb handling
|
||||||
|
model._set("C3", "=COUPDAYS(A5,A6,1,4)"); // European - no special Feb handling
|
||||||
|
model._set("C4", "=COUPDAYS(A7,A8,1,4)"); // European - no special Feb handling
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
// All should succeed without errors
|
||||||
|
for row in ["B1", "B2", "B3", "B4", "C1", "C2", "C3", "C4"] {
|
||||||
|
assert_ne!(model._get_text(row), *"#NUM!", "Failed for {}", row);
|
||||||
|
assert!(
|
||||||
|
model._get_text(row).parse::<f64>().is_ok(),
|
||||||
|
"Non-numeric result for {}",
|
||||||
|
row
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_coupon_functions_february_edge_cases() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
|
||||||
|
// Test that coupon functions handle February dates correctly without errors
|
||||||
|
|
||||||
|
// Settlement: February 28, 2023 (non-leap), Maturity: February 28, 2024 (leap)
|
||||||
|
model._set("A1", "=DATE(2023,2,28)");
|
||||||
|
model._set("A2", "=DATE(2024,2,28)");
|
||||||
|
|
||||||
|
// Test with basis 0 (US 30/360 - should use special February handling)
|
||||||
|
model._set("B1", "=COUPDAYBS(A1,A2,2,0)");
|
||||||
|
model._set("B2", "=COUPDAYS(A1,A2,2,0)");
|
||||||
|
model._set("B3", "=COUPDAYSNC(A1,A2,2,0)");
|
||||||
|
|
||||||
|
// Test with basis 4 (European 30/360 - should NOT use special February handling)
|
||||||
|
model._set("C1", "=COUPDAYBS(A1,A2,2,4)");
|
||||||
|
model._set("C2", "=COUPDAYS(A1,A2,2,4)");
|
||||||
|
model._set("C3", "=COUPDAYSNC(A1,A2,2,4)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
// With US method (basis 0), February dates should be handled specially
|
||||||
|
// With European method (basis 4), February dates should use actual dates
|
||||||
|
// Key point: both should work without errors
|
||||||
|
|
||||||
|
// We're ensuring functions complete successfully with February dates
|
||||||
|
assert_ne!(model._get_text("B1"), *"#NUM!");
|
||||||
|
assert_ne!(model._get_text("B2"), *"#NUM!");
|
||||||
|
assert_ne!(model._get_text("B3"), *"#NUM!");
|
||||||
|
assert_ne!(model._get_text("C1"), *"#NUM!");
|
||||||
|
assert_ne!(model._get_text("C2"), *"#NUM!");
|
||||||
|
assert_ne!(model._get_text("C3"), *"#NUM!");
|
||||||
|
}
|
||||||
@@ -15,12 +15,12 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
|
|||||||
| ACCRINTM | <Badge type="tip" text="Available" /> | [ACCRINTM](financial/accrintm) |
|
| ACCRINTM | <Badge type="tip" text="Available" /> | [ACCRINTM](financial/accrintm) |
|
||||||
| AMORDEGRC | <Badge type="info" text="Not implemented yet" /> | – |
|
| AMORDEGRC | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| AMORLINC | <Badge type="info" text="Not implemented yet" /> | – |
|
| AMORLINC | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| COUPDAYBS | <Badge type="info" text="Not implemented yet" /> | – |
|
| COUPDAYBS | <Badge type="tip" text="Available" /> | – |
|
||||||
| COUPDAYS | <Badge type="info" text="Not implemented yet" /> | – |
|
| COUPDAYS | <Badge type="tip" text="Available" /> | – |
|
||||||
| COUPDAYSNC | <Badge type="info" text="Not implemented yet" /> | – |
|
| COUPDAYSNC | <Badge type="tip" text="Available" /> | – |
|
||||||
| COUPNCD | <Badge type="info" text="Not implemented yet" /> | – |
|
| COUPNCD | <Badge type="tip" text="Available" /> | – |
|
||||||
| COUPNUM | <Badge type="info" text="Not implemented yet" /> | – |
|
| COUPNUM | <Badge type="tip" text="Available" /> | – |
|
||||||
| COUPPCD | <Badge type="info" text="Not implemented yet" /> | – |
|
| COUPPCD | <Badge type="tip" text="Available" /> | – |
|
||||||
| CUMIPMT | <Badge type="tip" text="Available" /> | – |
|
| CUMIPMT | <Badge type="tip" text="Available" /> | – |
|
||||||
| CUMPRINC | <Badge type="tip" text="Available" /> | – |
|
| CUMPRINC | <Badge type="tip" text="Available" /> | – |
|
||||||
| DB | <Badge type="tip" text="Available" /> | – |
|
| DB | <Badge type="tip" text="Available" /> | – |
|
||||||
|
|||||||
@@ -7,6 +7,5 @@ lang: en-US
|
|||||||
# COUPDAYBS
|
# COUPDAYBS
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
🚧 This function is not yet available in IronCalc.
|
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
||||||
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
|
||||||
:::
|
:::
|
||||||
@@ -7,6 +7,5 @@ lang: en-US
|
|||||||
# COUPDAYS
|
# COUPDAYS
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
🚧 This function is not yet available in IronCalc.
|
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
||||||
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
|
||||||
:::
|
:::
|
||||||
@@ -7,6 +7,5 @@ lang: en-US
|
|||||||
# COUPDAYSNC
|
# COUPDAYSNC
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
🚧 This function is not yet available in IronCalc.
|
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
||||||
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
|
||||||
:::
|
:::
|
||||||
@@ -7,6 +7,5 @@ lang: en-US
|
|||||||
# COUPNCD
|
# COUPNCD
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
🚧 This function is not yet available in IronCalc.
|
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
||||||
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
|
||||||
:::
|
:::
|
||||||
@@ -7,6 +7,5 @@ lang: en-US
|
|||||||
# COUPNUM
|
# COUPNUM
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
🚧 This function is not yet available in IronCalc.
|
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
||||||
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
|
||||||
:::
|
:::
|
||||||
@@ -7,6 +7,5 @@ lang: en-US
|
|||||||
# COUPPCD
|
# COUPPCD
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
🚧 This function is not yet available in IronCalc.
|
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
||||||
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
|
||||||
:::
|
:::
|
||||||
Reference in New Issue
Block a user