Day counts
This page lists everything you need to turn an accrual period into a year fraction: the twelve conventions, the three readings (two dates alone, a coupon in its schedule, or a stated calendar), their arguments, and the cases where two conventions or two readings part.
Quick example
This measures one period, 15 February to 31 August 2026, under all twelve conventions.
use chrono::{Duration, NaiveDate};
use helixrs::DayCountFraction;
fn date(year: i32, month: u32, day: u32) -> NaiveDate {
NaiveDate::from_ymd_opt(year, month, day).expect("a real calendar date")
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let start = date(2026, 2, 15);
let end = date(2026, 8, 31);
let actual_days = (end - start).num_days();
// One period, twelve conventions. The convention and its code string name the
// same convention, so either may be passed wherever a day count is taken.
let members = [
DayCountFraction::Act360,
DayCountFraction::Act365Fixed,
DayCountFraction::Nl365,
DayCountFraction::Thirty360,
DayCountFraction::Thirty360Us,
DayCountFraction::ThirtyE360,
DayCountFraction::ThirtyE360Isda,
DayCountFraction::ActActIsda,
DayCountFraction::ActActIcma,
DayCountFraction::ActActAfb,
DayCountFraction::Bus252,
DayCountFraction::One,
];
println!("{start} to {end}: {actual_days} actual days");
println!("{:<14} year fraction", "code");
for member in members {
let fraction = member.year_fraction(start, end);
println!("{:<14} {fraction:.10}", member.code());
}
// The two fixed-denominator counts are actual days over their denominator.
Ok(())
}import datetime as dt
import helixio as hlx
DayCountFraction = hlx.dates.DayCountFraction
START = dt.date(2026, 2, 15)
END = dt.date(2026, 8, 31)
ACTUAL_DAYS = (END - START).days
# One period, twelve conventions. The typed member and its code string name the
# same convention, so either may be passed wherever a day count is taken.
members = [
DayCountFraction.Act360,
DayCountFraction.Act365Fixed,
DayCountFraction.Nl365,
DayCountFraction.Thirty360,
DayCountFraction.Thirty360Us,
DayCountFraction.ThirtyE360,
DayCountFraction.ThirtyE360Isda,
DayCountFraction.ActActIsda,
DayCountFraction.ActActIcma,
DayCountFraction.ActActAfb,
DayCountFraction.Bus252,
DayCountFraction.One,
]
print(f"{START} to {END}: {ACTUAL_DAYS} actual days")
print(f"{'code':<14} year fraction")
fractions = {}
for member in members:
fraction = member.year_fraction(START, END)
fractions[member.code()] = fraction
print(f"{member.code():<14} {fraction:.10f}")
# The two fixed-denominator counts are actual days over their denominator.2026-02-15 to 2026-08-31: 197 actual days
code year fraction
ACT/360 0.5472222222
ACT/365.FIXED 0.5397260274
NL/365 0.5397260274
30/360 0.5444444444
30U/360 0.5444444444
30E/360 0.5416666667
30E/360.ISDA 0.5416666667
ACT/ACT.ISDA 0.5397260274
ACT/ACT.ICMA 0.5000000000
ACT/ACT.AFB 0.5397260274
BUS/252 0.5317460317
1/1 1.0000000000
The same period comes out a few days' interest apart from one convention to the next, which is why a coupon on the wrong convention is wrong.
How it's priced
With the actual days from to , the actual conventions are
ACT/ACT.ISDA splits the period at each year boundary and divides each part by its own year's length; NL/365 is ACT/365.FIXED without 29 February. The 30/360 family writes each date as with adjusted days :
A coupon inside a regular period of a schedule paying times a year accrues, under ACT/ACT.ICMA,
BUS/252 is , with the business days in , and 1/1 is for any period, so a zero-coupon period accrues its whole rate once. Which market uses which convention is covered in [how dates and conventions work.
API
| Symbol | Meaning | Rust | Python |
|---|---|---|---|
| the reading from two dates alone | DayCountFraction::year_fraction |
DayCountFraction.year_fraction |
|
| with , | the schedule-aware reading | DayCountFraction::year_fraction_with_context with a PeriodContext |
DayCountFraction.year_fraction_with_context |
| on a stated calendar | DayCountFraction::year_fraction_with_calendar |
DayCountFraction.year_fraction_with_calendar |
|
| , , , , | the dates' parts | the two dates you pass | the two dates you pass |
| a convention | one of twelve, by member or code | DayCountFraction, parsed from its code |
dates.DayCountFraction |
| Argument | Type | Default | Meaning |
|---|---|---|---|
DayCountFraction(code) |
str |
required | "ACT/360", "ACT/365.FIXED", "NL/365", "30/360", "30U/360", "30E/360", "30E/360.ISDA", "ACT/ACT.ISDA", "ACT/ACT.ICMA", "ACT/ACT.AFB", "BUS/252", "1/1"; case is ignored |
year_fraction(start, end) |
date, date |
required | the context-free reading |
year_fraction_with_context(start, end, period_end, frequency, schedule_end, ...) |
date, date, date, Frequency or str, date |
required | the reading inside a schedule: the coupon's period end, the schedule's frequency and its last date |
year_fraction_with_context(..., end_of_month=) |
bool |
False |
whether the schedule rolls on month end, which turns on the 30U/360 February rule |
year_fraction_with_context(..., reference_days=) |
int or None |
None |
states the regular period's length in days instead of deriving it |
year_fraction_with_calendar(start, end, calendar) |
date, date, Calendar |
required | BUS/252 counts this calendar; every other convention ignores it |
Defaults and limits
- Leap years. Across 29 February, ACT/ACT.ISDA divides each year's part by its own length, ACT/365.FIXED divides the whole by 365, and NL/365 doesn't count 29 February at all.
- ACT/ACT.ICMA needs its schedule. A full regular period accrues exactly . From two dates
alone, the convention takes the dates as their own reference period, so a short first coupon
reads as a whole number of months, not its accrual. Use
year_fraction_with_context. - The 30U/360 February rule applies only in a schedule that rolls on month end: the last day of February then counts as the 30th. From two dates alone, 30U/360 agrees with 30/360.
- BUS/252 defaults to Brazil. With no calendar it counts
BRBD, Brazil's national calendar; a city calendar such asBRSPcan only make the count smaller. São Paulo closes on 24 and 31 December, so a period across the year end counts two fewer days there. - Codes are matched ignoring case, and a spelling that isn't a code is refused with the list of
codes that are.
ACT/365is notACT/365.FIXED.
!!! warning "A member is not equal to its string"
DayCountFraction.Act360 != "ACT/360". Compare member.code() with the string, or parse the
string first.
More examples
The three readings in one program: a leap year, the ICMA reference period of a short first coupon, 30U/360 and February, BUS/252 on two calendars, and naming a day count. The example reads no shared data.
use chrono::{Duration, NaiveDate};
use helixrs::PeriodContext;
use helixrs::{DayCountFraction, Frequency, Period, add_period, calendar};
fn date(year: i32, month: u32, day: u32) -> NaiveDate {
NaiveDate::from_ymd_opt(year, month, day).expect("a real calendar date")
}
fn days(start: NaiveDate, end: NaiveDate) -> f64 {
(end - start).num_days() as f64
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
// A period across the end of a leap year. ACT/ACT.ISDA splits it at the year
// boundary and divides each part by its own year's length; ACT/365.FIXED
// divides the whole by 365; NL/365 does not count 29 February at all.
let leap_start = date(2027, 11, 15);
let leap_end = date(2028, 5, 15);
let isda = DayCountFraction::ActActIsda.year_fraction(leap_start, leap_end);
let fixed = DayCountFraction::Act365Fixed.year_fraction(leap_start, leap_end);
let no_leap = DayCountFraction::Nl365.year_fraction(leap_start, leap_end);
let in_2027 = days(leap_start, date(2028, 1, 1));
let in_2028 = days(date(2028, 1, 1), leap_end);
println!("{leap_start} to {leap_end}: {in_2027} days in 2027, {in_2028} in 2028");
println!("ACT/ACT.ISDA {isda:.10}");
println!("ACT/365.FIXED {fixed:.10}");
println!("NL/365 {no_leap:.10}");
// ACT/ACT.ICMA measures a period against the regular coupon period it sits
// in. A semiannual bond's short first coupon runs from issue to 15 June; the
// regular period it belongs to is the six months before 15 June.
let issue = date(2026, 3, 10);
let first_coupon = date(2026, 6, 15);
let maturity = date(2031, 6, 15);
let context = |period_end: NaiveDate| PeriodContext::Scheduled {
period_end,
frequency: Frequency::SemiAnnual,
schedule_end: maturity,
end_of_month: false,
reference_days: None,
};
let icma = DayCountFraction::ActActIcma;
let short_first =
icma.year_fraction_with_context(issue, first_coupon, context(first_coupon));
// A full regular period is exactly one over the frequency.
let next_coupon = date(2026, 12, 15);
let regular =
icma.year_fraction_with_context(first_coupon, next_coupon, context(next_coupon));
let six_months_back: Period = "-6M".parse().expect("a tenor");
let notional_start = add_period(first_coupon, six_months_back);
// Given two dates alone, the convention takes the dates as their own
// reference period: the nearest whole number of months, as a share of a
// year. The short first coupon then reads as three months, which is not
// its accrual. A context that states no schedule gives the same answer.
let alone = icma.year_fraction(issue, first_coupon);
println!("notional period {notional_start} to {first_coupon}");
println!("short first coupon {short_first:.10}");
println!("regular coupon {regular:.10}");
println!("two dates alone {alone:.10}");
// 30U/360 is the US 30/360 with the February rule: in a schedule that rolls on
// month end, the last day of February counts as the 30th. Read from two dates
// alone it applies no February rule and agrees with 30/360.
let (feb_end, aug_end) = (date(2026, 2, 28), date(2026, 8, 31));
let us = DayCountFraction::Thirty360Us;
let alone_us = us.year_fraction(feb_end, aug_end);
let month_end = us.year_fraction_with_context(
feb_end,
aug_end,
PeriodContext::Scheduled {
period_end: aug_end,
frequency: Frequency::SemiAnnual,
schedule_end: date(2031, 8, 31),
end_of_month: true,
reference_days: None,
},
);
// Without the rule: 28 February is day 28, so 31 August stays day 31.
// With it: 28 February is day 30, which makes 31 August day 30 too.
println!("30U/360, two dates alone {alone_us:.10}");
println!("30U/360, month-end roll {month_end:.10}");
// Across a year end, where São Paulo closes on 24 and 31 December and the
// national calendar does not.
let (ye_start, ye_end) = (date(2027, 12, 1), date(2028, 1, 31));
let sao_paulo = calendar("BRSP").expect("a registered business centre");
let bus = DayCountFraction::Bus252;
let national = bus.year_fraction(ye_start, ye_end);
let with_calendar = bus.year_fraction_with_calendar(ye_start, ye_end, sao_paulo.as_ref());
let business_days = sao_paulo.business_days_between(ye_start, ye_end);
println!("BUS/252, two dates alone {national:.10}");
println!("BUS/252, BRSP calendar {with_calendar:.10} ({business_days} business days)");
// A code is matched ignoring case, and a spelling that is not a code is
// refused with the list of codes that are. ACT/365 is not ACT/365.FIXED.
let folded: DayCountFraction = "act/360".parse().expect("a code in any case");
let refused = if "ACT/365".parse::<DayCountFraction>().is_err() { "refused" } else { "" };
println!("'act/360' -> {}", folded.code());
println!("'ACT/365' -> {refused}");
Ok(())
}import datetime as dt
import helixio as hlx
DayCountFraction = hlx.dates.DayCountFraction
# A period across the end of a leap year. ACT/ACT.ISDA splits it at the year
# boundary and divides each part by its own year's length; ACT/365.FIXED divides
# the whole by 365; NL/365 does not count 29 February at all.
LEAP_START = dt.date(2027, 11, 15)
LEAP_END = dt.date(2028, 5, 15)
isda = DayCountFraction.ActActIsda.year_fraction(LEAP_START, LEAP_END)
fixed = DayCountFraction.Act365Fixed.year_fraction(LEAP_START, LEAP_END)
no_leap = DayCountFraction.Nl365.year_fraction(LEAP_START, LEAP_END)
in_2027 = (dt.date(2028, 1, 1) - LEAP_START).days
in_2028 = (LEAP_END - dt.date(2028, 1, 1)).days
print(f"{LEAP_START} to {LEAP_END}: {in_2027} days in 2027, {in_2028} in 2028")
print(f"ACT/ACT.ISDA {isda:.10f}")
print(f"ACT/365.FIXED {fixed:.10f}")
print(f"NL/365 {no_leap:.10f}")
# ACT/ACT.ICMA measures a period against the regular coupon period it sits in.
# A semiannual bond's short first coupon runs from issue to 15 June; the regular
# period it belongs to is the six months before 15 June.
ISSUE = dt.date(2026, 3, 10)
FIRST_COUPON = dt.date(2026, 6, 15)
MATURITY = dt.date(2031, 6, 15)
short_first = DayCountFraction.ActActIcma.year_fraction_with_context(
ISSUE, FIRST_COUPON, FIRST_COUPON, hlx.dates.Frequency.SemiAnnual, MATURITY
)
# A full regular period is exactly one over the frequency.
regular = DayCountFraction.ActActIcma.year_fraction_with_context(
FIRST_COUPON, dt.date(2026, 12, 15), dt.date(2026, 12, 15), "6M", MATURITY
)
notional_start = hlx.dates.add_period(FIRST_COUPON, "-6M")
# Given two dates alone, the convention takes the dates as their own reference
# period: the nearest whole number of months, as a share of a year. The short
# first coupon then reads as three months, which is not its accrual.
alone = DayCountFraction.ActActIcma.year_fraction(ISSUE, FIRST_COUPON)
print(f"notional period {notional_start} to {FIRST_COUPON}")
print(f"short first coupon {short_first:.10f}")
print(f"regular coupon {regular:.10f}")
print(f"two dates alone {alone:.10f}")
# 30U/360 is the US 30/360 with the February rule: in a schedule that rolls on
# month end, the last day of February counts as the 30th. Read from two dates
# alone it applies no February rule and agrees with 30/360.
FEB_END, AUG_END = dt.date(2026, 2, 28), dt.date(2026, 8, 31)
US = DayCountFraction.Thirty360Us
alone_us = US.year_fraction(FEB_END, AUG_END)
month_end = US.year_fraction_with_context(FEB_END, AUG_END, AUG_END, "6M", dt.date(2031, 8, 31), end_of_month=True)
# Without the rule: 28 February is day 28, so 31 August stays day 31.
# With it: 28 February is day 30, which makes 31 August day 30 too.
print(f"30U/360, two dates alone {alone_us:.10f}")
print(f"30U/360, month-end roll {month_end:.10f}")
# Across a year end, where São Paulo closes on 24 and 31 December and the
# national calendar does not.
YEAR_END = (dt.date(2027, 12, 1), dt.date(2028, 1, 31))
SAO_PAULO = hlx.Calendar("BRSP")
national = DayCountFraction.Bus252.year_fraction(*YEAR_END)
with_calendar = DayCountFraction.Bus252.year_fraction_with_calendar(*YEAR_END, SAO_PAULO)
business_days = SAO_PAULO.business_days_between(*YEAR_END)
print(f"BUS/252, two dates alone {national:.10f}")
print(f"BUS/252, BRSP calendar {with_calendar:.10f} ({business_days} business days)")
# A code is matched ignoring case, and a spelling that is not a code is refused
# with the list of codes that are. ACT/365 is not ACT/365.FIXED.
refused = ""
try:
DayCountFraction("ACT/365")
except ValueError:
refused = "refused"
print(f"'act/360' -> {DayCountFraction('act/360').code()}")
print(f"'ACT/365' -> {refused}")2027-11-15 to 2028-05-15: 47 days in 2027, 135 in 2028
ACT/ACT.ISDA 0.4976195823
ACT/365.FIXED 0.4986301370
NL/365 0.4958904110
notional period 2025-12-15 to 2026-06-15
short first coupon 0.2664835165
regular coupon 0.5000000000
two dates alone 0.2500000000
30U/360, two dates alone 0.5083333333
30U/360, month-end roll 0.5000000000
BUS/252, two dates alone 0.1706349206
BUS/252, BRSP calendar 0.1626984127 (41 business days)
'act/360' -> ACT/360
'ACT/365' -> refused
See also
- Source:
crates/helixrs/src/statics/daycounts.rs - Related pages:
- How dates and conventions work, for which market uses which convention, and each convention rebuilt from its formula.
- Calendars, for the business days BUS/252 counts.
- Schedules, for the periods a day count measures.
- Legs and coupons, for the day count a coupon accrues on.
From the 0.0 line at revision bf7f2e2. Every printed number is that revision's own output.