fix cursor

This commit is contained in:
Brian Hung
2025-07-31 15:56:44 -07:00
committed by Nicolás Hatcher
parent 895fb649da
commit f64a83e0a8

View File

@@ -10,13 +10,20 @@ use crate::{
use super::financial_util::{compute_irr, compute_npv, compute_rate, compute_xirr, compute_xnpv}; use super::financial_util::{compute_irr, compute_npv, compute_rate, compute_xirr, compute_xnpv};
// Financial calculation constants
const DAYS_30_360: i32 = 360;
const DAYS_ACTUAL: i32 = 365;
const DAYS_LEAP_YEAR: i32 = 366;
const DAYS_PER_MONTH_360: i32 = 30;
const TBILL_THRESHOLD_DAYS: f64 = 183.0;
// See: // See:
// https://github.com/apache/openoffice/blob/c014b5f2b55cff8d4b0c952d5c16d62ecde09ca1/main/scaddins/source/analysis/financial.cxx // https://github.com/apache/openoffice/blob/c014b5f2b55cff8d4b0c952d5c16d62ecde09ca1/main/scaddins/source/analysis/financial.cxx
fn is_less_than_one_year(start_date: i64, end_date: i64) -> Result<bool, String> { fn is_less_than_one_year(start_date: i64, end_date: i64) -> Result<bool, String> {
let end = from_excel_date(end_date)?; let end = from_excel_date(end_date)?;
let start = from_excel_date(start_date)?; let start = from_excel_date(start_date)?;
if end_date - start_date < 365 { if end_date - start_date < DAYS_ACTUAL as i64 {
return Ok(true); return Ok(true);
} }
let end_year = end.year(); let end_year = end.year();
@@ -61,20 +68,20 @@ fn days360_us(start: chrono::NaiveDate, end: chrono::NaiveDate) -> i32 {
// Rule 1: If both date A and B fall on the last day of February, then date B will be changed to the 30th // 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) { if is_last_day_of_feb(start) && is_last_day_of_feb(end) {
d2 = 30; d2 = DAYS_PER_MONTH_360;
} }
// Rule 2: If date A falls on the 31st of a month or last day of February, then date A will be changed to the 30th // 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) { if d1 == 31 || is_last_day_of_feb(start) {
d1 = 30; d1 = DAYS_PER_MONTH_360;
} }
// Rule 3: If date A falls on the 30th after applying rule 2 and date B falls on the 31st, then date B will be changed to the 30th // 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 { if d1 == DAYS_PER_MONTH_360 && d2 == 31 {
d2 = 30; d2 = DAYS_PER_MONTH_360;
} }
360 * (y2 - y1) + 30 * (m2 - m1) + (d2 - d1) DAYS_30_360 * (y2 - y1) + DAYS_PER_MONTH_360 * (m2 - m1) + (d2 - d1)
} }
fn days360_eu(start: chrono::NaiveDate, end: chrono::NaiveDate) -> i32 { fn days360_eu(start: chrono::NaiveDate, end: chrono::NaiveDate) -> i32 {
@@ -86,13 +93,16 @@ fn days360_eu(start: chrono::NaiveDate, end: chrono::NaiveDate) -> i32 {
let y2 = end.year(); let y2 = end.year();
if d1 == 31 { if d1 == 31 {
d1 = 30; d1 = DAYS_PER_MONTH_360;
} }
if d2 == 31 { if d2 == 31 {
d2 = 30; d2 = DAYS_PER_MONTH_360;
} }
d2 + m2 * 30 + y2 * 360 - d1 - m1 * 30 - y1 * 360 d2 + m2 * DAYS_PER_MONTH_360 + y2 * DAYS_30_360
- d1
- m1 * DAYS_PER_MONTH_360
- y1 * DAYS_30_360
} }
fn days_30us_360(start: chrono::NaiveDate, end: chrono::NaiveDate) -> i32 { fn days_30us_360(start: chrono::NaiveDate, end: chrono::NaiveDate) -> i32 {
@@ -107,20 +117,20 @@ fn days_30us_360(start: chrono::NaiveDate, end: chrono::NaiveDate) -> i32 {
// Rule 1: If both date A and B fall on the last day of February, then date B will be changed to the 30th // 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) { if is_last_day_of_feb(start) && is_last_day_of_feb(end) {
d2 = 30; d2 = DAYS_PER_MONTH_360;
} }
// Rule 2: If date A falls on the 31st of a month or last day of February, then date A will be changed to the 30th // 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) { if d1 == 31 || is_last_day_of_feb(start) {
d1 = 30; d1 = DAYS_PER_MONTH_360;
} }
// Rule 3: If date A falls on the 30th after applying rule 2 and date B falls on the 31st, then date B will be changed to the 30th // 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 { if d1 == DAYS_PER_MONTH_360 && d2 == 31 {
d2 = 30; d2 = DAYS_PER_MONTH_360;
} }
(y2 - y1) * 360 + (m2 - m1) * 30 + (d2 - d1) (y2 - y1) * DAYS_30_360 + (m2 - m1) * DAYS_PER_MONTH_360 + (d2 - d1)
} }
fn days_30e_360(start: chrono::NaiveDate, end: chrono::NaiveDate) -> i32 { fn days_30e_360(start: chrono::NaiveDate, end: chrono::NaiveDate) -> i32 {
@@ -131,12 +141,12 @@ fn days_30e_360(start: chrono::NaiveDate, end: chrono::NaiveDate) -> i32 {
let y1 = start.year(); let y1 = start.year();
let y2 = end.year(); let y2 = end.year();
if d1 == 31 { if d1 == 31 {
d1 = 30; d1 = DAYS_PER_MONTH_360;
} }
if d2 == 31 { if d2 == 31 {
d2 = 30; d2 = DAYS_PER_MONTH_360;
} }
(y2 - y1) * 360 + (m2 - m1) * 30 + (d2 - d1) (y2 - y1) * DAYS_30_360 + (m2 - m1) * DAYS_PER_MONTH_360 + (d2 - d1)
} }
fn days_between(start: i64, end: i64, basis: i32) -> Result<i32, String> { fn days_between(start: i64, end: i64, basis: i32) -> Result<i32, String> {
@@ -152,15 +162,15 @@ fn days_between(start: i64, end: i64, basis: i32) -> Result<i32, String> {
fn days_in_year(date: chrono::NaiveDate, basis: i32) -> Result<i32, String> { fn days_in_year(date: chrono::NaiveDate, basis: i32) -> Result<i32, String> {
Ok(match basis { Ok(match basis {
0 | 2 | 4 => 360, 0 | 2 | 4 => DAYS_30_360,
1 => { 1 => {
if is_leap_year(date.year()) { if is_leap_year(date.year()) {
366 DAYS_LEAP_YEAR
} else { } else {
365 DAYS_ACTUAL
} }
} }
3 => 365, 3 => DAYS_ACTUAL,
_ => return Err("invalid basis".to_string()), _ => return Err("invalid basis".to_string()),
}) })
} }
@@ -182,11 +192,11 @@ fn year_fraction(
basis: i32, basis: i32,
) -> Result<f64, String> { ) -> Result<f64, String> {
let days = match basis { let days = match basis {
0 => days_30us_360(start, end) as f64 / 360.0, 0 => days_30us_360(start, end) as f64 / DAYS_30_360 as f64,
1 => (end - start).num_days() as f64 / 365.0, 1 => (end - start).num_days() as f64 / DAYS_ACTUAL as f64,
2 => (end - start).num_days() as f64 / 360.0, 2 => (end - start).num_days() as f64 / DAYS_30_360 as f64,
3 => (end - start).num_days() as f64 / 365.0, 3 => (end - start).num_days() as f64 / DAYS_ACTUAL as f64,
4 => days_30e_360(start, end) as f64 / 360.0, 4 => days_30e_360(start, end) as f64 / DAYS_30_360 as f64,
_ => return Err("Invalid basis".to_string()), _ => return Err("Invalid basis".to_string()),
}; };
Ok(days) Ok(days)
@@ -1777,9 +1787,9 @@ impl Model {
} }
let days_in_year = match basis { let days_in_year = match basis {
0 | 2 | 4 => 360.0, 0 | 2 | 4 => DAYS_30_360 as f64,
1 | 3 => 365.0, 1 | 3 => DAYS_ACTUAL as f64,
_ => 360.0, _ => DAYS_30_360 as f64,
}; };
let diff_days = maturity - settlement; let diff_days = maturity - settlement;
if diff_days <= 0.0 { if diff_days <= 0.0 {
@@ -1902,14 +1912,18 @@ impl Model {
} }
// days to maturity // days to maturity
let d_m = maturity - settlement; let d_m = maturity - settlement;
let result = if d_m < 183.0 { let result = if d_m < TBILL_THRESHOLD_DAYS {
365.0 * discount / (360.0 - discount * d_m) DAYS_ACTUAL as f64 * discount / (DAYS_30_360 as f64 - discount * d_m)
} else { } else {
// Equation here is: // Equation here is:
// (1-days*rate/360)*(1+y/2)*(1+d_extra*y/year)=1 // (1-days*rate/360)*(1+y/2)*(1+d_extra*y/year)=1
let year = if d_m == 366.0 { 366.0 } else { 365.0 }; let year = if d_m == DAYS_LEAP_YEAR as f64 {
DAYS_LEAP_YEAR as f64
} else {
DAYS_ACTUAL as f64
};
let d_extra = d_m - year / 2.0; let d_extra = d_m - year / 2.0;
let alpha = 1.0 - d_m * discount / 360.0; let alpha = 1.0 - d_m * discount / DAYS_30_360 as f64;
let beta = 0.5 + d_extra / year; let beta = 0.5 + d_extra / year;
// ay^2+by+c=0 // ay^2+by+c=0
let a = d_extra * alpha / (year * 2.0); let a = d_extra * alpha / (year * 2.0);
@@ -1967,7 +1981,7 @@ impl Model {
} }
// days to maturity // days to maturity
let d_m = maturity - settlement; let d_m = maturity - settlement;
let result = 100.0 * (1.0 - discount * d_m / 360.0); let result = 100.0 * (1.0 - discount * d_m / DAYS_30_360 as f64);
if result.is_infinite() { if result.is_infinite() {
return CalcResult::new_error(Error::DIV, cell, "Division by 0".to_string()); return CalcResult::new_error(Error::DIV, cell, "Division by 0".to_string());
} }
@@ -2017,7 +2031,7 @@ impl Model {
return CalcResult::new_error(Error::NUM, cell, "discount should be >0".to_string()); return CalcResult::new_error(Error::NUM, cell, "discount should be >0".to_string());
} }
let days = maturity - settlement; let days = maturity - settlement;
let result = (100.0 - pr) * 360.0 / (pr * days); let result = (100.0 - pr) * DAYS_30_360 as f64 / (pr * days);
CalcResult::Number(result) CalcResult::Number(result)
} }
@@ -2047,7 +2061,7 @@ impl Model {
Err(s) => return s, Err(s) => return s,
}; };
let frequency = match self.get_number_no_bools(&args[5], cell) { let frequency = match self.get_number_no_bools(&args[5], cell) {
Ok(f) => f.round() as i32, Ok(f) => f.trunc() as i32,
Err(s) => return s, Err(s) => return s,
}; };
if frequency != 1 && frequency != 2 && frequency != 4 { if frequency != 1 && frequency != 2 && frequency != 4 {
@@ -2064,15 +2078,21 @@ impl Model {
"settlement should be < maturity".to_string(), "settlement should be < maturity".to_string(),
); );
} }
if args.len() == 7 { let basis = if args.len() == 7 {
let _basis = match self.get_number_no_bools(&args[6], cell) { match self.get_number_no_bools(&args[6], cell) {
Ok(f) => f, Ok(f) => f.trunc() as i32,
Err(s) => return s, Err(s) => return s,
}; }
// basis is currently ignored } else {
} 0
};
let days_in_year = match basis {
0 | 2 | 4 => DAYS_30_360 as f64,
1 | 3 => DAYS_ACTUAL as f64,
_ => DAYS_30_360 as f64,
};
let days = maturity - settlement; let days = maturity - settlement;
let periods = ((days * frequency as f64) / 365.0).round(); let periods = ((days * frequency as f64) / days_in_year).round();
if periods <= 0.0 { if periods <= 0.0 {
return CalcResult::new_error(Error::NUM, cell, "invalid dates".to_string()); return CalcResult::new_error(Error::NUM, cell, "invalid dates".to_string());
} }
@@ -2225,7 +2245,7 @@ impl Model {
Err(s) => return s, Err(s) => return s,
}; };
let frequency = match self.get_number_no_bools(&args[5], cell) { let frequency = match self.get_number_no_bools(&args[5], cell) {
Ok(f) => f.round() as i32, Ok(f) => f.trunc() as i32,
Err(s) => return s, Err(s) => return s,
}; };
if frequency != 1 && frequency != 2 && frequency != 4 { if frequency != 1 && frequency != 2 && frequency != 4 {
@@ -2242,15 +2262,21 @@ impl Model {
"settlement should be < maturity".to_string(), "settlement should be < maturity".to_string(),
); );
} }
if args.len() == 7 { let basis = if args.len() == 7 {
let _basis = match self.get_number_no_bools(&args[6], cell) { match self.get_number_no_bools(&args[6], cell) {
Ok(f) => f, Ok(f) => f.trunc() as i32,
Err(s) => return s, Err(s) => return s,
}; }
// basis ignored } else {
} 0
};
let days_in_year = match basis {
0 | 2 | 4 => DAYS_30_360 as f64,
1 | 3 => DAYS_ACTUAL as f64,
_ => DAYS_30_360 as f64,
};
let days = maturity - settlement; let days = maturity - settlement;
let periods = ((days * frequency as f64) / 365.0).round(); let periods = ((days * frequency as f64) / days_in_year).round();
if periods <= 0.0 { if periods <= 0.0 {
return CalcResult::new_error(Error::NUM, cell, "invalid dates".to_string()); return CalcResult::new_error(Error::NUM, cell, "invalid dates".to_string());
} }
@@ -2611,7 +2637,7 @@ impl Model {
let (pcd, ncd) = coupon_dates(settlement_date, maturity_date, frequency); let (pcd, ncd) = coupon_dates(settlement_date, maturity_date, frequency);
let days = match basis { let days = match basis {
0 | 4 => 360 / frequency, // 30/360 conventions 0 | 4 => DAYS_30_360 / frequency, // 30/360 conventions
_ => days_between_dates(pcd, ncd, basis), // Actual day counts _ => days_between_dates(pcd, ncd, basis), // Actual day counts
}; };
CalcResult::Number(days as f64) CalcResult::Number(days as f64)