Good Friday isn't a weekend, but the NYSE is still closed
Almanac parses plain-English date phrases and resolves them against a real market calendar, so 'next trading day' knows about Good Friday, Juneteenth, and observed holidays, not just Saturday and Sunday.
Ask most natural-language date libraries for "the next business day" after a Thursday and they will happily say Friday. That's right about fifty weeks a year. On the Thursday before Good Friday it is wrong, because the NYSE is closed and the real answer is Monday. Good Friday isn't a weekend. It isn't a federal holiday either. It is just a day the market doesn't trade, and a weekend-skipping heuristic has no way to know that.
Almanac is a small TypeScript library that
parses phrases like next trading day, 3 business days after Thanksgiving, or
the trading day before Christmas and resolves them against an actual market holiday
calendar. It exists because the usual fix is ugly: you install a language parser
(chrono, a date-fns plugin) for the words, a separate market-calendar package for the
holidays, and then you hand-write glue to make the two agree. That glue is the part that
goes subtly wrong, because it's never quite clear which layer resolves "next trading day":
the parser's naive weekend skip, or the calendar's holiday-aware one.
One system, not two glued together
The whole design turns on refusing to have two layers. In Almanac the parser and the
business-day math are the same system. parse() never resolves a phrase with a private
weekend check; it always walks a Calendar, so holiday awareness isn't a feature you opt
into, it's the only code path that exists. There is no naive fallback to disagree with.
That Calendar is the deliberate seam. Almanac ships an NYSE calendar out of the box, but
nothing in the parser or the arithmetic is NYSE-specific. Every function takes a calendar
argument; the NYSE one is just the default instance you reach for, not an assumption baked
into the algorithm. Hand it a different exchange, or a ListCalendar built from your
company's internal holiday list, and every phrase resolves identically against it:
const myCalendar = new ListCalendar([{ date: '2024-10-31', name: 'Founders Day' }]);
parse('the trading day before Founders Day', myCalendar);
// -> { ok: true, date: <2024-10-30> }
Walk the days, don't compute an offset
The tempting way to write addBusinessDays is a closed-form trick: figure out how many
weeks the offset spans, multiply, adjust for the remainder. That works right up until
holidays enter, and holiday sets are not uniform. They differ by exchange, by year, by
jurisdiction. Juneteenth only became an NYSE holiday in 2022. So Almanac doesn't compute an
offset, it walks: step one calendar day at a time, ask the calendar "is this a trading
day," and count down. Slower in theory, but it's correct in practice and trivially
auditable. You can read the loop and see exactly why it landed where it did.
The subtle case day-walking gets for free is observation. When a holiday falls on a Saturday, the market observes it the preceding Friday; on a Sunday, the following Monday. That's why "the trading day before Christmas" isn't just December 24th minus one: some years the 24th itself is a weekend, and the answer shifts. A formula would need special cases for all of it. A day-walk against a correct calendar just steps over whatever the calendar marks as closed.
Two things worth calling out honestly. parse() never throws: it returns
{ ok: true, date } or { ok: false, reason }, so a form validator, a batch job, and the
playground all treat "didn't understand that" as data, and anything outside the supported
phrase set comes back as false rather than a confident wrong guess. And the built-in NYSE
data covers 2024 through 2027. Weekends are always recognized, but a date outside that
window is treated as a trading day even if it would really be a holiday. No silent
guessing at holidays for years it hasn't been told about. The whole thing ships with zero
runtime dependencies, which is the point: nothing to fall out of sync with.
Try it
Open the playground and type
the trading day before Christmas. Then try 3 business days after Thanksgiving, or
next trading day on a Thursday in early April, and watch it skip past Good Friday to
Monday. If you'd rather read the day-walk than trust it, the
code is on GitHub.
This post is part of the build log: every app my automated factory ships gets written up here, honestly. Browse everything at apps.charliekrug.com. Comments are open below.
Loading comments…