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 daysday_count_fraction(2026-01-01, 2026-07-01, act-360)→ numerator 181, denominator 360 ACT/360 over the same 181 daysday_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_iso | date | start of the accrual period, ISO |
| end_iso | date | end of the accrual period, ISO; not before startIso |
| convention | DayCountConvention | which day count basis the contract names |
| returns | YearFraction | the 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(…)
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
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.
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Path | Bytes |
|---|---|
| README.md | 1,765 |
| impl/python.py | 2,495 |
| impl/rust.rs | 3,224 |
| impl/typescript.ts | 2,536 |
| vectors.json | 3,476 |