How I count business days with public holidays in JavaScript (rules, an API, and the corrections it needed)
Every online business day calculator I tried gave me a slightly different answer for the same two dates, and none of them showed which days they had skipped. So I wrote my own and put the skipped days on a calendar under
Every online business day calculator I tried gave me a slightly different answer for the same two dates, and none of them showed which days they had skipped. So I wrote my own and put the skipped days on a calendar under the result. I made this tool: https://instantfreetools.com/business-days-calculator/
This post is about how the counting works, because most of the bugs were not where I expected them.
The count itself is the easy part
A business day is a date that is not a weekend day, not a public holiday and not a day off you added yourself. The core loop is boring on purpose:
export function isBusinessDay(dt, weekend, holidays, custom) {
const iso = toISO(dt);
return !weekend.has(dt.getUTCDay()) && !(iso in holidays) && !custom.has(iso);
}
weekend is a Set of weekday numbers (0 is Sunday), so a Friday and Saturday weekend is new Set([5, 6]) instead of a special case. holidays is a plain object keyed by YYYY-MM-DD.
The counter walks from the start date to the end date one day at a time. That sounds slow, but a span of ten years is about 3,650 iterations, which is nothing. A closed-form formula would be faster, but every holiday you add makes it harder to get right, and the loop has one more benefit: while it walks, it can record each weekday it skipped and the reason. That list is what draws the calendar.
All dates are created with Date.UTC and read with getUTCDay(). If you mix local time in, a date can move by a day around a daylight saving change, and you get an off-by-one error that only shows up two nights a year.
Include the start date or not?
This is where calculators disagree most. Excel's NETWORKDAYS counts both the start and the end date. WORKDAY, which adds days, does not count the start date. So "10 business days from 20 November" and "business days from 20 November to 4 December" are answering different questions.
I made both ends a checkbox and kept the Excel behaviour as the default, then printed the matching formula next to the result so people can check it in a spreadsheet:
=NETWORKDAYS.INTL(DATE(2026,11,6), DATE(2026,12,31), "0000011", H2:H4)
The seven-character string is the weekend, Monday first, with 1 for a day off. "0000011" is Saturday and Sunday, and "0000110" is Friday and Saturday. Building it from the same Set the counter uses means the formula and the result can't drift apart.
For the US, 6 November to 31 December 2026 comes out at 37 business days: 40 weekdays minus Veterans Day, Thanksgiving and Christmas. The H2:H4 range is those three dates.
US federal holidays: the observed rule
Most US federal holidays are either a fixed date or "the nth Monday of a month". The fixed ones have a catch: if one falls on a Saturday, the day off is the Friday before, and on a Sunday it is the Monday after.
function usObserved(dt) {
const w = dt.getUTCDay();
if (w === 6) return addDays(dt, -1);
if (w === 0) return addDays(dt, 1);
return dt;
}
In 2026 Independence Day is a Saturday, so the holiday in the calculator is Friday 3 July.
The case that broke my first version was New Year's Day 2028, which is a Saturday. Its observed day is Friday 31 December 2027, in the previous year. If you build holidays one year at a time, that day lands in the wrong year's list or disappears. Now the code checks next year's 1 January while building this year, and drops this year's own 1 January if it moved back into last year. December 2027 has 21 business days in the US, with both 24 December (Christmas observed) and 31 December skipped.
Two holidays also need a start year: Martin Luther King Jr. Day from 1986 and Juneteenth from 2021. Without that, counts for older dates are off by one.
England and Wales: computing Easter
UK bank holidays for England and Wales include Good Friday and Easter Monday, so you need Easter Sunday. There is a well-known arithmetic method for the Gregorian calendar (often credited to Meeus, Jones and Butcher) that needs no tables. For 2027 it gives 28 March, which puts Good Friday on 26 March and Easter Monday on 29 March. UK bank holidays that fall on a weekend move to the next weekday, which is a different rule from the US one.
I checked both sets against the official tables (the GOV.UK bank holidays JSON and the OPM federal holiday pages) and kept those dates as tests.
Everywhere else: an API, then corrections
Writing rules for 200 more countries by hand was not realistic. For the rest I use the free Nager.Date API. The browser sends only a country code and a year, and the dates you type stay on your device. Results are cached per country and year, so changing the dates does not refetch.
The data is good but not perfect, and I found a few gaps by comparing it with official calendars:
- Japan: a weekday between two holidays becomes a holiday too (the "citizens' holiday"). In 2026 that makes 22 September a day off, between Respect for the Aged Day on the 21st and the Autumnal Equinox on the 23rd. The API did not have it, so the code adds it from the rule.
- South Korea: a few temporary and substitute holidays were missing, and for holidays moved off a weekend the original date had been dropped. The code puts both back.
- Brazil: Corpus Christi is listed as a national holiday, but at the federal level it is an optional day, so it is removed.
- Mexico: Labour Day is fixed by law on 1 May, and the code pins it there.
These corrections are pure functions that take the holiday map and return a new one, so each has a small test with the official dates.
I also limit the API to 1990 through 2035. Outside that range the values look generated from rules rather than published, and I would rather show an error than a confident wrong number.
Things I'd tell myself at the start
- Store dates as
YYYY-MM-DDstrings and do the maths in UTC. Most of my bugs went away after that. - Decide what "from" and "to" mean before writing code, and show the user the choice.
- Show the skipped days. Nobody trusted the number until they could see why it was 37 and not 40.
- Holiday data needs checking against official sources. Treat any dataset, mine included, as a starting point.
If you know a country or year where the observed-holiday rules get strange, I would really like to hear about it in the comments.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.