Functional Weave
Code in Rust

dates.day-count-fraction

Year fraction between two dates by ACT/365F, ACT/360, 30/360, 30E/360 or ACT/ACT ISDA, as an exact fraction.

1.0.0 · published 2026-10-03 by charlie · Anterra

Pinned by 21 tests, run in TypeScript, Python and Rust.

What it does

The fraction of a year between two dates under the day count convention a loan, bond or swap names. It returns an exact fraction, numerator and denominator in lowest terms, rather than a float: interest is then principal x rate x numerator / denominator in integer arithmetic, with one rounding at the end (math.round-div), and three languages cannot disagree in the fifteenth decimal place.

The conventions are those of the 2006 ISDA Definitions, section 4.16:

For example

  • day_count_fraction(2026-01-01, 2026-07-01, act-365f) → numerator 181, denominator 365 ACT/365F over half of 2026: 181 actual days
  • day_count_fraction(2026-01-01, 2026-07-01, act-360) → numerator 181, denominator 360 ACT/360 over the same 181 days
  • day_count_fraction(2026-01-01, 2026-04-01, act-360) → numerator 1, denominator 4 ACT/360 over 90 days is exactly a quarter, reduced to lowest terms

The function

The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.

pub fn day_count_fraction(start_iso: &str, end_iso: &str, convention: &str) -> YearFraction
start_isodatestart of the accrual period, ISO
end_isodateend of the accrual period, ISO; not before startIso
conventionDayCountConventionwhich day count basis the contract names
returnsYearFractionthe fraction of a year, in lowest terms

The types it declares, generated into your project

// DayCountConvention is a string in Rust, one of: "act-365f", "act-360", "30-360", "30e-360", "act-act-isda".
// Parameters take it as &str and results hold it as String.

/// An exact year fraction; divide only at the point of use.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct YearFraction {
    /// 0 or more
    pub numerator: i64,
    /// 1 or more; the fraction is in lowest terms
    pub denominator: i64,
}

Your code names it in one line, in the file that uses it

fune!(dates.day-count-fraction@^1);  // then call day_count_fraction(…)
impl/rust.rs · 90 lines · open · 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(),
    ))
}

Install

fune build

With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 1 dependency, pins them in fune.lock, downloads only the Rust package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. A crate’s build.rs runs it before every compile. Or pin a range in fune.project and build in one step:

fune add dates.day-count-fraction
Download for Rust dates.day-count-fraction-1.0.0-rust.fune · 11,302 bytes sha256 0b0f0fe7c8c080db0d83c2691fd5a868fd6d901c034b4152273b83e8a8bf53e3

The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./dates.day-count-fraction-1.0.0-rust.fune, or fetch it from a terminal with fune pull dates.day-count-fraction@1.0.0:rust.

The whole function, every language, is one file too: dates.day-count-fraction-1.0.0.fune, 16,521 bytes, sha256 c19c350e9bcfd350f52a9fa1766f89525c83765d2f239ae9948617001b15a651. It installs into a project of any language.

Customise it in your app

The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.

before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.

// fune: before dates.day-count-fraction

after — your function gets the result and the arguments, and returns the final result.

// fune: after dates.day-count-fraction

replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.

// fune: replace dates.add-days in dates.day-count-fraction

step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show dates.day-count-fraction --steps.

// fune: step dates.day-count-fraction after <n|label>

Tests

A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.

CaseArgumentsExpected
ACT/365F over half of 2026: 181 actual days 2026-01-01, 2026-07-01, act-365f → numerator 181, denominator 365
ACT/360 over the same 181 days 2026-01-01, 2026-07-01, act-360 → numerator 181, denominator 360
ACT/360 over 90 days is exactly a quarter, reduced to lowest terms 2026-01-01, 2026-04-01, act-360 → numerator 1, denominator 4
ACT/365F over a whole leap year is 366/365, more than one 2024-01-01, 2025-01-01, act-365f → numerator 366, denominator 365
ACT/365F across 29 February from the last day of 2023 2023-12-31, 2024-12-31, act-365f → numerator 366, denominator 365
30/360 bond basis: 31 Jan to 28 Feb, D1 becomes 30, so 28 days 2026-01-31, 2026-02-28, 30-360 → numerator 7, denominator 90
30/360 bond basis: 31 Jan to 31 Mar, both ends trimmed, 60 days 2026-01-31, 2026-03-31, 30-360 → numerator 1, denominator 6
30/360 bond basis keeps D2 = 31 when D1 is not at month end: 76 days 2026-01-15, 2026-03-31, 30-360 → numerator 19, denominator 90
30E/360 always trims D2 = 31: the same dates give 75 days 2026-01-15, 2026-03-31, 30e-360 → numerator 5, denominator 24
30E/360 31 Jan to 31 Mar 2026-01-31, 2026-03-31, 30e-360 → numerator 1, denominator 6
Show the other 11 tests
CaseArgumentsExpected
30/360 has no end-of-February rule: 28 Feb to 1 Mar is 3 days, not 1 2026-02-28, 2026-03-01, 30-360 → numerator 1, denominator 120
30/360 over a whole year is exactly 1 2025-03-15, 2026-03-15, 30-360 → numerator 1, denominator 1
ACT/ACT ISDA splits the days by year: 184/365 + 182/366 2023-07-01, 2024-07-01, act-act-isda → numerator 66,887, denominator 66,795
ACT/ACT ISDA over a whole leap year is exactly 1 2024-01-01, 2025-01-01, act-act-isda → numerator 1, denominator 1
ACT/ACT ISDA over two whole years is exactly 2 2024-01-01, 2026-01-01, act-act-isda → numerator 2, denominator 1
ACT/ACT ISDA within one ordinary year 2026-01-01, 2026-07-01, act-act-isda → numerator 181, denominator 365
ACT/ACT ISDA within one leap year 2024-01-01, 2024-07-01, act-act-isda → numerator 91, denominator 183
the same date twice is zero, as 0/1 2026-09-22, 2026-09-22, act-360 → numerator 0, denominator 1
a start after the end is an error 2026-09-23, 2026-09-22, act-365f → error: must not be after endIso
an unknown convention is an error, not a default 2026-01-01, 2026-07-01, act-365 → error: unknown day count convention "act-365"
an impossible date is an error 2026-02-30, 2026-07-01, act-360 → error: is not a real calendar date

More from the author

- `act-365f` (Actual/365 (Fixed), 4.16(d)): actual days / 365, even in a leap year, so a whole leap year is 366/365. - `act-360` (Actual/360, 4.16(e)): actual days / 360, the money-market basis. - `30-360` (30/360, Bond Basis, 4.16(f)): each month counts as 30 days. D1 = 31 becomes 30; D2 = 31 becomes 30 only if D1 is (now) 30. Fraction is (360 x (Y2 - Y1) + 30 x (M2 - M1) + (D2 - D1)) / 360. - `30e-360` (30E/360, Eurobond Basis, 4.16(g)): as above, but D2 = 31 always becomes 30. - `act-act-isda` (Actual/Actual (ISDA), 4.16(b)): the days falling in a leap year / 366 plus the days falling in a non-leap year / 365. The start date counts, the end date does not.

What it does not do. There is no end-of-February rule in `30-360`: ISDA bond basis has none, unlike the "30/360 US" (NASD / SIA) variant, where the last day of February counts as the 30th; 28 February to 1 March is 3/360 here. It does not implement ACT/ACT ICMA, which needs the coupon schedule as well as two dates, or 30E/360 ISDA, which needs the maturity date. Business day adjustment of the dates is the caller's job, before calling.

The start must not be after the end; the same date twice is 0/1. Actual days come from the dates.add-days kernel, so leap years and centuries are exact.

Files

PathBytes
README.md1,765
impl/python.py2,495
impl/rust.rs3,224
impl/typescript.ts2,536
vectors.json3,476