Calculate Business Days Between Two Dates in JavaScript
To count business days, iterate calendar days with Temporal.PlainDate and count those whose dayOfWeek is 1-5, optionally skipping a holiday set. Part of Temporal.Duration Arithmetic.
Why this scenario is tricky
"Business days between two dates" is one of those requirements that sounds like subtraction but is really a filtered count, and the filtering is where every naive attempt goes wrong. You cannot take the total number of days and multiply by five-sevenths, because that only works on average and is off for any specific short range β a Monday-to-Wednesday span has three weekdays, not 3Γ5/7. And you cannot do it on absolute time, because "day" here means a civil calendar day, so millisecond arithmetic drags in daylight-saving hours that have nothing to do with which weekday a date falls on. The correct approach steps day by day on a zoneless calendar type and counts the days that pass the filter.
The second layer of difficulty is that "business day" is not a fixed universal concept. The Monday-to-Friday default is only a starting point: real payroll, shipping-estimate, and SLA calculations must also exclude public holidays, and those holidays differ by country, by region, and by year. So a production business-day counter is really a weekday filter plus a holiday set, and its correctness depends entirely on feeding it the right holiday calendar. Recognizing that the "hard part" is the holiday data, not the loop, is what keeps the implementation honest.
There is no closed-form business-day count that survives arbitrary holidays, so the reliable approach is to walk the range day by day on a calendar type. Doing it on Date risks DST drift; doing it on PlainDate means each step is exactly one civil day and dayOfWeek (1 = Monday β¦ 7 = Sunday) is unambiguous.
Minimal working solution
The minimal version steps from the start date to just before the end date, incrementing a counter whenever the current day's ISO dayOfWeek is 1 through 5 (Monday through Friday). Because it runs on PlainDate and advances with add({ days: 1 }), each step is exactly one calendar day regardless of any zone or transition, so the count is deterministic everywhere. The loop uses Temporal.PlainDate.compare(d, end) < 0 as its condition, which makes the range half-open β the end date is excluded β matching the convention used across this topic so that adjacent ranges tile without double-counting the shared boundary.
Half-open is the right default here for the same reason it is elsewhere: it composes. If you count business days for January and then for February using half-open ranges, the last day of January and the first day of February are each counted exactly once, in exactly one of the two ranges. Decide deliberately whether your domain wants the end date included β "how many business days until the deadline, inclusive of the deadline" sometimes does β and if so, add one day to the end before counting, naming the choice so the next reader knows it was intentional.
import { Temporal } from '@js-temporal/polyfill';
function businessDays(start: Temporal.PlainDate, end: Temporal.PlainDate) {
let n = 0;
for (let d = start; Temporal.PlainDate.compare(d, end) < 0; d = d.add({ days: 1 }))
if (d.dayOfWeek <= 5) n++; // 1..5 = Mon..Fri
return n;
}
Full production version
The production counter takes a holiday set alongside the date range and skips any day that is either a weekend or a member of that set. Representing holidays as a Set of ISO date strings ('2026-12-25') makes the membership check a fast, exact lookup, and it keeps the holiday data declarative β sourced from a per-country calendar that can be updated each year without touching the counting logic. The key discipline is that the counter itself stays dumb: it filters weekends and consults the holiday set, and all the jurisdictional complexity lives in the data you pass in.
Sourcing that data correctly is the real work. Holidays shift year to year (many are the "first Monday" of a month, or move when they fall on a weekend), differ between a company's locations, and sometimes include half-days. A robust system generates the holiday set per region per year from an authoritative source and caches it, rather than hard-coding a list that silently goes stale. For very large ranges where a day-by-day loop is a concern, you can compute the full weeks analytically (each contributes five business days) and only iterate the partial weeks at each end, then subtract holidays β but for the ranges most applications use, the straightforward loop is clear and fast enough, and clarity usually wins.
import { Temporal } from '@js-temporal/polyfill';
function businessDays(start: Temporal.PlainDate, end: Temporal.PlainDate, holidays = new Set<string>()) {
let n = 0;
for (let d = start; Temporal.PlainDate.compare(d, end) < 0; d = d.add({ days: 1 }))
if (d.dayOfWeek <= 5 && !holidays.has(d.toString())) n++;
return n;
}
Verification snippet
The assertions that catch bugs are the boundary and filter cases. Assert that a Monday-to-Friday range (half-open, ending the following Monday) counts five business days, and that a range wholly within a weekend counts zero. Assert that a range starting on a Saturday and ending mid-week counts only the weekdays, proving the weekend filter works from either end. Add a same-day range and confirm it counts zero under the half-open convention, since the end is excluded.
The holiday assertions are what separate a toy from a production counter: pass a holiday set containing a weekday inside the range and assert the count drops by exactly one, then pass a holiday that falls on a weekend and assert the count is unchanged (because that day was never counted anyway). Finally, run the whole suite under several TZ values and confirm the counts never change, which proves the calculation is genuinely calendar-based and no host-zone assumption leaked into the day stepping.
Common pitfalls
The dominant pitfall is approximating business days as a fraction of total days, which is wrong for any specific range and only coincidentally right on long averages. Count them explicitly. The second is doing the stepping on absolute time or a legacy Date, where daylight-saving hours and zone drift can occasionally push a boundary onto the wrong calendar day and miscount. Step on a zoneless PlainDate. The third is forgetting holidays entirely, which produces counts that are correct in the abstract but wrong for real payroll or delivery estimates β the weekday filter alone is rarely sufficient for a business requirement.
A subtler mistake is an inconsistent inclusivity convention: counting one range as end-inclusive and the next as end-exclusive, so chained ranges either double-count or skip their shared boundary. Pick half-open and apply it everywhere. Another is using the wrong weekday numbering β Temporal's Monday-is-1 versus legacy Sunday-is-0 β which shifts the entire weekend filter and silently counts Sundays as business days. Finally, remember that some regions have a different working week entirely (Sunday-to-Thursday in parts of the world), so hard-coding Saturday and Sunday as the weekend is itself a localization assumption worth making configurable.
Frequently Asked Questions
How do I count business days between two dates?
Iterate the range on Temporal.PlainDate and count the days whose dayOfWeek is 1 through 5 (Monday to Friday). To exclude public holidays, keep a Set of their ISO strings and skip any date it contains.
Why not just divide the day count by seven and multiply by five?
That estimate is wrong whenever the range covers partial weeks or contains holidays. Because Temporal.PlainDate.dayOfWeek is exact and holidays are arbitrary, walking the range and counting is the only reliable method.
How do I count business days between two dates in JavaScript?
Step from the start date to just before the end on a Temporal.PlainDate, advancing with add({ days: 1 }), and increment a counter whenever dayOfWeek is 1β5 (MondayβFriday). Because PlainDate is zoneless, each step is exactly one calendar day and the count is deterministic across time zones. For real business use, also skip public holidays by checking each day against a holiday set for the relevant region and year.
How do I exclude public holidays from a business-day count?
Pass a Set of ISO date strings for the holidays and skip any day that is a weekend or a member of that set. Keep the counter itself simple β weekend filter plus set lookup β and put the jurisdictional complexity in the data, generating the holiday set per country and per year from an authoritative source. Holidays shift year to year and differ by region, so hard-coding a static list silently goes stale.
Why not estimate business days as total days times five-sevenths?
Because that ratio is only correct on long averages, not for any specific range. A Monday-to-Wednesday span has three weekdays, not 3Γ5/7 β 2.14, and short ranges near weekends can be off by a day or more. Counting the weekdays explicitly with a calendar loop is exact, and it is the only approach that can also subtract holidays, which a fractional estimate cannot represent at all.