Functional Weave
Code in Rust

dates.day-count-fraction@1.0.0

impl/rust.rs

3,224 bytes · the Rust implementation · view raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::dates_add_days::{days_from_civil, epoch_day_from_iso, is_leap_year, parse_iso_date};  ← from dates.add-days ^1.0.0 · built alongside by fune

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(),
    ))
}