use super::funejson::Value; use super::dates_add_days::{days_from_civil, epoch_day_from_iso, is_leap_year, parse_iso_date}; fn gcd(mut a: i64, mut b: i64) -> i64 { while b != 0 { let r = a % b; a = b; b = r; } a } fn reduced(numerator: i64, denominator: i64) -> YearFraction { if numerator == 0 { return YearFraction { numerator: 0, denominator: 1 }; } let g = gcd(numerator, denominator); YearFraction { numerator: numerator / g, denominator: denominator / g, } } /// The year fraction from `start_iso` to `end_iso` under a day count convention /// (2006 ISDA Definitions 4.16), as an exact fraction in lowest terms. Kept /// exact so the caller multiplies principal and rate by the numerator and /// rounds once, instead of rounding a float year fraction first. /// /// # Panics /// Panics on a malformed date, a start after the end, or an unknown convention. pub fn day_count_fraction(start_iso: &str, end_iso: &str, convention: &str) -> YearFraction { let start = parse_iso_date(start_iso); let end = parse_iso_date(end_iso); if start_iso > end_iso { panic!("startIso {} must not be after endIso {}", start_iso, end_iso); } let first = epoch_day_from_iso(start_iso); let last = epoch_day_from_iso(end_iso); let actual = last - first; match convention { "act-365f" => reduced(actual, 365), "act-360" => reduced(actual, 360), "30-360" | "30e-360" => { let d1 = if start.day == 31 { 30 } else { start.day }; let mut d2 = end.day; // Bond basis only trims D2 when D1 is at the month end too; Eurobond always does. if d2 == 31 && (convention == "30e-360" || d1 == 30) { d2 = 30; } reduced( 360 * (end.year - start.year) + 30 * (end.month - start.month) + (d2 - d1), 360, ) } "act-act-isda" => { // Split the actual days by the calendar year they fall in: [start, end). let mut leap_days = 0i64; let mut other_days = 0i64; for year in start.year..=end.year { let from = if year == start.year { first } else { days_from_civil(year, 1, 1) }; let to = if year == end.year { last } else { days_from_civil(year + 1, 1, 1) }; if is_leap_year(year) { leap_days += to - from; } else { other_days += to - from; } } reduced(other_days * 366 + leap_days * 365, 365 * 366) } _ => panic!( "unknown day count convention \"{}\": expected act-365f, act-360, 30-360, 30e-360 or act-act-isda", convention ), } } pub fn year_fraction_to_value(fraction: &YearFraction) -> Value { Value::obj(vec![ ("numerator", Value::Int(fraction.numerator)), ("denominator", Value::Int(fraction.denominator)), ]) } pub fn fune_vector(args: &[Value]) -> Value { year_fraction_to_value(&day_count_fraction( args[0].as_str(), args[1].as_str(), args[2].as_str(), )) }