dates.day-count-fraction@1.0.0
impl/typescript.ts
2,536 bytes · the TypeScript 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.
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`);
}
}