Functional Weave
Code in TypeScript

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

  • dayCountFraction(2026-01-01, 2026-07-01, act-365f) → numerator 181, denominator 365 ACT/365F over half of 2026: 181 actual days
  • dayCountFraction(2026-01-01, 2026-07-01, act-360) → numerator 181, denominator 360 ACT/360 over the same 181 days
  • dayCountFraction(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.

export function dayCountFraction(startIso: string, endIso: string, convention: DayCountConvention): YearFraction
startIsodatestart of the accrual period, ISO
endIsodateend 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

export type DayCountConvention = "act-365f" | "act-360" | "30-360" | "30e-360" | "act-act-isda";

/** An exact year fraction; divide only at the point of use. */
export interface YearFraction {
  /** 0 or more */
  readonly numerator: number;
  /** 1 or more; the fraction is in lowest terms */
  readonly denominator: number;
}

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

import { dayCountFraction } from "#fune/dates.day-count-fraction@^1";
impl/typescript.ts · 58 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.

import { daysFromCivil, epochDayFromIso, isLeapYear, parseIsoDate } from "./dates_add_days.ts";  ← from dates.add-days ^1.0.0 · built alongside by fune
import { type DayCountConvention, type YearFraction } from "./dates_day_count_fraction_types.ts";

function gcd(a: number, b: number): number {
  while (b !== 0) [a, b] = [b, a % b];
  return a;
}

function reduced(numerator: number, denominator: number): YearFraction {
  if (numerator === 0) return { numerator: 0, denominator: 1 };
  const g = gcd(numerator, denominator);
  return { numerator: numerator / g, denominator: denominator / g };
}

/**
 * The year fraction from `startIso` to `endIso` 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.
 */
export function dayCountFraction(startIso: string, endIso: string, convention: DayCountConvention): YearFraction {
  const start = parseIsoDate(startIso);
  const end = parseIsoDate(endIso);
  if (startIso > endIso) {
    throw new RangeError(`startIso ${startIso} must not be after endIso ${endIso}`);
  }
  const actual = epochDayFromIso(endIso) - epochDayFromIso(startIso);
  switch (convention) {
    case "act-365f":
      return reduced(actual, 365);
    case "act-360":
      return reduced(actual, 360);
    case "30-360":
    case "30e-360": {
      const d1 = start.day === 31 ? 30 : start.day;
      let 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;
      return reduced(360 * (end.year - start.year) + 30 * (end.month - start.month) + (d2 - d1), 360);
    }
    case "act-act-isda": {
      // Split the actual days by the calendar year they fall in: [start, end).
      let leapDays = 0;
      let otherDays = 0;
      const first = epochDayFromIso(startIso);
      const last = epochDayFromIso(endIso);
      for (let year = start.year; year <= end.year; year++) {
        const from = year === start.year ? first : daysFromCivil(year, 1, 1);
        const to = year === end.year ? last : daysFromCivil(year + 1, 1, 1);
        if (isLeapYear(year)) leapDays += to - from;
        else otherDays += to - from;
      }
      return reduced(otherDays * 366 + leapDays * 365, 365 * 366);
    }
    default:
      throw new RangeError(`unknown day count convention "${convention}": expected act-365f, act-360, 30-360, 30e-360 or act-act-isda`);
  }
}

Install

fune build

With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 1 dependency, pins them in fune.lock, downloads only the TypeScript 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 TypeScript dates.day-count-fraction-1.0.0-typescript.fune · 10,588 bytes sha256 303cd04eb8f7275102cd6323260db8092f023e6082221caef00692ffcfcb5b0a

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

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