use chrono::{Datelike, NaiveDate};
use crate::eval::coercion::to_number;
use crate::eval::functions::check_arity;
use crate::eval::functions::date::serial::serial_to_date;
use crate::types::{ErrorKind, Value};
fn is_leap(year: i32) -> bool {
(year % 4 == 0 && year % 100 != 0) || year % 400 == 0
}
fn year_days(year: i32) -> f64 {
if is_leap(year) { 366.0 } else { 365.0 }
}
fn days_30_360_us(start: NaiveDate, end: NaiveDate) -> i32 {
let (mut d1, m1, y1) = (start.day() as i32, start.month() as i32, start.year());
let (mut d2, m2, y2) = (end.day() as i32, end.month() as i32, end.year());
if d1 == 31 { d1 = 30; }
if d2 == 31 && d1 == 30 { d2 = 30; }
(y2 - y1) * 360 + (m2 - m1) * 30 + (d2 - d1)
}
fn days_30_360_eu(start: NaiveDate, end: NaiveDate) -> i32 {
let (mut d1, m1, y1) = (start.day() as i32, start.month() as i32, start.year());
let (mut d2, m2, y2) = (end.day() as i32, end.month() as i32, end.year());
if d1 == 31 { d1 = 30; }
if d2 == 31 { d2 = 30; }
(y2 - y1) * 360 + (m2 - m1) * 30 + (d2 - d1)
}
fn yearfrac_actual_actual(start: NaiveDate, end: NaiveDate) -> f64 {
if start == end {
return 0.0;
}
let (start, end) = if start <= end { (start, end) } else { (end, start) };
let actual_days = end.signed_duration_since(start).num_days();
if start.year() == end.year() {
return actual_days as f64 / year_days(start.year());
}
let first_year_end = NaiveDate::from_ymd_opt(start.year() + 1, 1, 1).unwrap();
let days_first = first_year_end.signed_duration_since(start).num_days() as f64;
let mut total = days_first / year_days(start.year());
for y in (start.year() + 1)..end.year() {
total += 1.0;
let _ = y;
}
let last_year_start = NaiveDate::from_ymd_opt(end.year(), 1, 1).unwrap();
let days_last = end.signed_duration_since(last_year_start).num_days() as f64;
total += days_last / year_days(end.year());
total
}
pub fn yearfrac_fn(args: &[Value]) -> Value {
if let Some(err) = check_arity(args, 2, 3) {
return err;
}
let start_serial = match to_number(args[0].clone()) { Ok(n) => n, Err(e) => return e };
let end_serial = match to_number(args[1].clone()) { Ok(n) => n, Err(e) => return e };
let basis = if args.len() > 2 {
match to_number(args[2].clone()) { Ok(n) => n, Err(e) => return e }
} else {
0.0
};
let start = match serial_to_date(start_serial) {
Some(d) => d,
None => return Value::Error(ErrorKind::Value),
};
let end = match serial_to_date(end_serial) {
Some(d) => d,
None => return Value::Error(ErrorKind::Value),
};
let result = match basis as u32 {
0 => days_30_360_us(start, end) as f64 / 360.0,
1 => yearfrac_actual_actual(start, end),
2 => end.signed_duration_since(start).num_days() as f64 / 360.0,
3 => end.signed_duration_since(start).num_days() as f64 / 365.0,
4 => days_30_360_eu(start, end) as f64 / 360.0,
_ => return Value::Error(ErrorKind::Num),
};
Value::Number(result)
}
#[cfg(test)]
mod tests;