Dates, calendars, business-day conventions, and day-count fractions for
financial code — a native Rust library, no_std and free of
floating-point arithmetic, designed after QuantLib's ql/time.
Named for the fasti, the ancient Roman calendar that marked the dies fasti — the days on which business could lawfully be conducted.
Financial code needs to answer questions like "when is the next coupon
date?", "how many days of interest accrued?", and "is the market open
on Friday?" — precisely, deterministically, and without floating-point
drift. General-purpose date libraries answer none of these; QuantLib
answers all of them but brings C++ and doubles. fasti covers the
same capability surface with:
- No float arithmetic — anywhere. Day-count fractions are integer
rationals (
Fraction, ani64/u64reduced fraction). Scaling money by an accrual fraction stays exact. no_std+alloc. No I/O, no clock, no timezone database, no runtime dependencies beyondthiserror. The library has no concept of "now" — you tell it the dates. CI compiles it for a bare-metal target (thumbv7em-none-eabihf), so the claim is checked, not asserted.- Const-first. Every primitive constructor is
const fn, and every built-in calendar is apub const— zero allocation, zero setup. - No panics in library code. Fallible operations return
Result<_, TimeError>.unwrap/expect/panicare clippy-walled. - Property-tested invariants. Conservation laws (ACT-family additivity, adjust idempotence, schedule monotonicity) are proptest suites, not comments. A separate integration test compiles against the crate from outside, so the public API is exercised the way a dependent sees it.
The constraints do not cost speed. Walking is_business_day across a
century of dates (36 525 days) takes ~1.7 ms for us::SETTLEMENT and
~4.2 ms for NYSE, the heaviest built-in — roughly 48 ns and 116 ns per
day, evaluated from const rules with no allocation and no runtime
setup. Reproduce with cargo bench --bench calendar; those are
medians from one 2.1 GHz Xeon core, so read them as an order of
magnitude, not a promise.
Dates and times — chrono, jiff, time. Use them. fasti
is not competing: it has no clock, no time zones, and no resolution
finer than a day. None of the three knows what a business day is.
Business days — bdays, workdays. These answer is this a
holiday and add five business days, and stop there: no day-count
fractions, no business-day conventions past the basics, no schedule
generation.
Quant libraries — RustQuant_time covers much of the same
ground, with wider calendar coverage, as one crate of a broader
quantitative-finance framework. fasti is deliberately narrower: one
job, exact rationals instead of f64, and no std.
| Area | Types |
|---|---|
| Date primitives | Date (serial, 1901-01-01..=2199-12-31), Year, Month, Weekday, Ordinal |
| Durations | Period (days/weeks/months/years), Frequency |
| Holiday rules | Rule: fixed-date (with weekend-shift policies), nth/last weekday, Easter offsets (Western & Orthodox), one-offs, custom fn(Date) -> bool |
| Calendars | Calendar / CalendarBuilder; built-ins: TARGET, UK Settlement, US Settlement, NYSE, Federal Reserve, Government Bond, SOFR, NERC, France Settlement & Exchange, plus WEEKENDS_ONLY / NULL_CALENDAR baselines. Business-day and holiday enumeration over a date range, month edges, and joint calendars via CalendarBuilder::union |
| Business days | BusinessDayConvention (Following, ModifiedFollowing, Preceding, ModifiedPreceding, Unadjusted), adjust, advance |
| Day counts | DayCount: ACT/360, ACT/365F, 30/360 (Bond Basis, US, 30E/360, 30E/360 ISDA), ACT/ACT (ISDA and schedule-aware ICMA) — all returning Fraction |
| Schedules | Schedule / ScheduleBuilder: forward/backward/zero generation, stubs, end-of-month preservation |
Four jurisdictions ship today. If yours is not among them you are not
blocked: the built-ins hold no privileged position — uk::SETTLEMENT
is a pub const Calendar assembled from the same public Rule values
you have, and about sixty of them describe England and Wales, Jubilees
and all. Calendars get added as people need them; the issue template
asks for a published source, because that is what lets a calendar be
reviewed rather than trusted. The list stays short deliberately — a
holiday table is a standing obligation, and state funerals and
coronations arrive without notice.
Built-ins apply their modern holiday regime across the whole supported range, following QuantLib: UK dates before 1978 are the present-day rule projected backwards, not what was observed at the time.
The bindings live in bindings/python and ship as
fasti-dates on PyPI; the import name is fasti, matching the crate.
(The fasti name on PyPI is taken by an unrelated project.)
$ pip install fasti-dates>>> import datetime
>>> from fasti.calendars import us
>>> us.SETTLEMENT.adjust(datetime.date(2024, 7, 4), "following")
datetime.date(2024, 7, 5)Dates cross as datetime.date and year fractions come back as
fractions.Fraction, so the float-free guarantee survives the boundary.
[dependencies]
fasti = "0.2"
# Optional features (both off by default):
# serde — Serialize/Deserialize on the data types
# chrono — From/TryFrom conversions with chrono::NaiveDate,
# chrono::Weekday, and chrono::Month
fasti = { version = "0.2", features = ["serde", "chrono"] }The minimum supported Rust version is 1.90 (edition 2024). CI tests against it on every change, using a committed lockfile pinned to a dependency resolution known to build there. Raising the MSRV is a minor-version bump, never a patch.
Coming from chrono? Enable the chrono feature and convert at the
boundary — let d: fasti::Date = naive_date.try_into()?; (fallible
only because fasti's supported range is 1901..=2199) and
chrono::NaiveDate::from(d) on the way out. fasti never uses chrono
internally.
use fasti::{
ActActISDA, BusinessDayConvention, Date, DayCount, Month, Period,
ScheduleBuilder, calendars::us,
};
fn main() -> Result<(), fasti::TimeError> {
// Is July 4 a US market holiday?
let d = Date::from_ymd(2026, Month::Jul, 3)?; // Sat Jul 4 observed Friday
assert!(us::SETTLEMENT.is_holiday(d));
// Build a 5-year semiannual coupon schedule (backward generation,
// ModifiedFollowing interior dates, unadjusted maturity).
let schedule = ScheduleBuilder::new(
Date::from_ymd(2025, Month::Jan, 15)?,
Date::from_ymd(2030, Month::Jan, 15)?,
Period::Months(6),
us::SETTLEMENT,
)
.backwards()
.with_convention(BusinessDayConvention::ModifiedFollowing)
.with_termination_convention(BusinessDayConvention::Unadjusted)
.build()?;
// Accrue each period under ACT/ACT (ISDA) — exact integer rationals.
for period in schedule.periods() {
let (num, den) = ActActISDA
.year_fraction(period.start, period.end)
.parts();
println!("{} -> {}: {num}/{den}", period.start, period.end);
}
Ok(())
}Output — note that the January 2028 coupon lands on the 18th (the 15th
is a Saturday, and the Monday behind it is Martin Luther King Jr. Day),
and that the periods spanning a leap boundary are the ones a double
would have to round:
2025-01-15 -> 2025-07-15: 181/365
2025-07-15 -> 2026-01-15: 184/365
2026-01-15 -> 2026-07-15: 181/365
2026-07-15 -> 2027-01-15: 184/365
2027-01-15 -> 2027-07-15: 181/365
2027-07-15 -> 2028-01-18: 13685/26718
2028-01-18 -> 2028-07-17: 181/366
2028-07-17 -> 2029-01-16: 2227/4453
2029-01-16 -> 2029-07-16: 181/365
2029-07-16 -> 2030-01-15: 183/365
Run the fuller example:
cargo run --example treasury_scheduleSee ARCHITECTURE.md for the design constraints:
serial dates, the rule-based calendar model, why Fraction instead of
floats, and the supported 1901..=2199 date range.
QuantLib's ql/time is the design reference; ported holiday tables and
lookup data are attributed in the module docs and in
THIRD-PARTY-NOTICES. The Easter tables are
additionally cross-validated in tests against independent computus
implementations, so they are reproducible from first principles.
Pre-1.0. The public surface is small and deliberate but may still move. Planned next: CDS/IMM schedule generation rules and the long-tail day counts (Business/252, ACT/365 Canadian, NASD 30/360).
Issues and pull requests are welcome. CONTRIBUTING.md
covers the development workflow and the ground rules;
ARCHITECTURE.md covers the constraints behind
them. Participation is under the
Code of Conduct.
Calendar and day-count data bugs are the most useful thing you can
report. The issue template for them asks for the published source the
fix will be checked against, because that is what makes such a bug
fixable in one pass. For anything that looks like a vulnerability, read
SECURITY.md first — it also explains why wrong
holiday data, deliberately planted or not, is out of scope there: an
inaccurate holiday table is a correctness bug, so it belongs in a
public issue where others can check it against the source, not in a
private advisory.
Dual-licensed under either of
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
The Easter-Monday lookup tables in src/easter.rs are ported from
QuantLib, which is distributed under a permissive modified-BSD
(3-clause) license. That license permits the redistribution above; its
full text and copyright notice are reproduced in
THIRD-PARTY-NOTICES, which ships inside the
published crate. If your license tooling flags fasti for third-party
content, this is what it has found — there is no copyleft anywhere in
the dependency graph, and cargo deny check enforces that on every
commit.