Functional Weave
Code in Python

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.

def day_count_fraction(start_iso: str, end_iso: str, convention: DayCountConvention) -> 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 = Literal["act-365f", "act-360", "30-360", "30e-360", "act-act-isda"]

@dataclass(frozen=True)
class YearFraction:
    """An exact year fraction; divide only at the point of use."""

    #: 0 or more
    numerator: int
    #: 1 or more; the fraction is in lowest terms
    denominator: int

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

from fune.dates.day_count_fraction import day_count_fraction  # dates.day-count-fraction@^1
impl/python.py · 56 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.

from .dates_add_days import days_from_civil, epoch_day_from_iso, is_leap_year, parse_iso_date  ← from dates.add-days ^1.0.0 · built alongside by fune
from .dates_day_count_fraction_types import DayCountConvention, YearFraction


def _gcd(a: int, b: int) -> int:
    while b != 0:
        a, b = b, a % b
    return a


def _reduced(numerator: int, denominator: int) -> YearFraction:
    if numerator == 0:
        return YearFraction(numerator=0, denominator=1)
    g = _gcd(numerator, denominator)
    return YearFraction(numerator=numerator // g, denominator=denominator // g)


def day_count_fraction(start_iso: str, end_iso: str, convention: DayCountConvention) -> YearFraction:
    """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.
    """
    start = parse_iso_date(start_iso)
    end = parse_iso_date(end_iso)
    if start_iso > end_iso:
        raise ValueError("startIso %s must not be after endIso %s" % (start_iso, end_iso))
    first = epoch_day_from_iso(start_iso)
    last = epoch_day_from_iso(end_iso)
    actual = last - first
    if convention == "act-365f":
        return _reduced(actual, 365)
    if convention == "act-360":
        return _reduced(actual, 360)
    if convention in ("30-360", "30e-360"):
        d1 = 30 if start.day == 31 else start.day
        d2 = end.day
        # Bond basis only trims D2 when D1 is at the month end too; Eurobond always does.
        if d2 == 31 and (convention == "30e-360" or d1 == 30):
            d2 = 30
        return _reduced(360 * (end.year - start.year) + 30 * (end.month - start.month) + (d2 - d1), 360)
    if convention == "act-act-isda":
        # Split the actual days by the calendar year they fall in: [start, end).
        leap_days = 0
        other_days = 0
        for year in range(start.year, end.year + 1):
            frm = first if year == start.year else days_from_civil(year, 1, 1)
            to = last if year == end.year else days_from_civil(year + 1, 1, 1)
            if is_leap_year(year):
                leap_days += to - frm
            else:
                other_days += to - frm
        return _reduced(other_days * 366 + leap_days * 365, 365 * 366)
    raise ValueError(
        'unknown day count convention "%s": expected act-365f, act-360, 30-360, 30e-360 or act-act-isda' % (convention,)
    )

Install

fune build

With that line in your source, in a Python project (language python in fune.project), fune build resolves it and its 1 dependency, pins them in fune.lock, downloads only the Python 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. Or pin a range in fune.project and build in one step:

fune add dates.day-count-fraction
Download for Python dates.day-count-fraction-1.0.0-python.fune · 10,541 bytes sha256 1374e38f0f794e0a21eae9086307caafd7eefee9528698a891cbd8b529dc4de0

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

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