BlogCalendar

Business days vs calendar days: computing deadlines with holidays

How to count and add business days correctly — the O(1) weekday formula, the holiday-calendar problem, and the inclusive/exclusive and time-zone edge cases that break naive code.

A deadline expressed in "business days" is a different quantity than the same number of "calendar days", and the gap between them is where billing disputes, missed filings, and angry customers come from. "Net 30" on an invoice means 30 calendar days. "Ships in 3 business days" excludes the weekend. "Respond within 5 business days" on a legal notice excludes weekends and public holidays, and which holidays depends on jurisdiction. The math looks trivial and almost never is, because the definition of a business day is local, mutable, and full of edge cases.

Why the distinction matters

The two units diverge by up to several days over any span longer than a week, and contracts, SLAs, and statutes pick one deliberately.

  • Payment terms. "Net 30" is 30 calendar days; "30 business days" is roughly 42 calendar days. Treating one as the other shifts a due date by a week and a half and can trigger late fees or breach clauses.
  • SLAs. A support SLA of "next business day" response means a Friday-evening ticket is due Monday, not Saturday — and due Tuesday if Monday is a holiday.
  • Shipping ETAs. Carriers quote transit in business days. A 3-business-day shipment ordered Thursday arrives the following Tuesday, not Sunday.
  • Legal and filing deadlines. Courts and tax authorities define windows in business days and often add a rule that a deadline landing on a weekend or holiday rolls forward to the next business day. Getting this wrong is not a rounding error; it is a missed filing.

Get the unit wrong and the answer is wrong by a predictable amount, which is worse than a random error because nobody notices until money or a deadline is on the line.

Definitions

Calendar days count everything: weekdays, weekends, holidays. The difference between two dates in calendar days is plain subtraction of day numbers.

Business days (working days) exclude weekends and public holidays. Two assumptions hide here, and both are wrong somewhere:

  • The weekend is not universal. Most of the world rests Saturday and Sunday, but parts of the Middle East use a Friday–Saturday weekend, and a few jurisdictions have used Thursday–Friday or a single rest day. "Weekend" is a configuration value, not a constant.
  • Holidays are local. A business day in one country is a holiday in another, and within a country a holiday may be national, regional, or specific to an exchange or industry.

So a business day is "a day that is neither a configured weekend day nor a holiday in the relevant calendar." Everything below depends on having that calendar correct.

Counting business days between two dates

The naive approach iterates one day at a time, incrementing a counter when the day is a weekday and not a holiday. It is correct and easy to read, and it is O(n) in the number of days — fine for a few weeks, wasteful for a span of years.

The weekday count between two dates has a closed-form O(1) shortcut. The key observation: every full block of 7 days contributes exactly 5 weekdays. So split the span into full weeks plus a remainder.

# Count WEEKDAYS in the half-open interval [start, end)
# (start included, end excluded). Mon=0 .. Sun=6.

total_days   = end - start            # in days
full_weeks   = total_days // 7
remainder    = total_days % 7         # 0..6 leftover days

weekdays = full_weeks * 5

# Walk only the remainder days, starting at start's weekday:
for i in 0 .. remainder-1:
    if (weekday(start) + i) % 7 < 5:   # 0..4 are Mon..Fri
        weekdays += 1

# Now subtract holidays that fall on a weekday inside [start, end):
business_days = weekdays - count_weekday_holidays(start, end)

The remainder loop runs at most six times regardless of span length, so the whole thing is O(1) plus the cost of checking holidays. If your holiday data is a sorted list or a set keyed by date, counting the holidays in range is a bounded lookup rather than a full scan.

Watch the endpoints. The formula above counts the half-open interval [start, end) — start counted, end not. Inclusive-both ([start, end]) is one more day; exclusive-both is one fewer. Almost every off-by-one bug in date math is an endpoint convention mismatch between two parts of the same system. Pick a convention, write it in a comment, and apply it everywhere. When in doubt, half-open intervals compose cleanly: the count over [a, c) equals the count over [a, b) plus [b, c), which closed intervals do not give you for free.

The holiday subtraction has its own trap: only subtract holidays that fall on a weekday. A holiday landing on a Saturday was never counted as a weekday in the first place, so subtracting it would undercount.

Adding N business days to a date

Counting between dates and advancing a date are different operations, and the formula above does not directly invert into "add N business days." Advancing is cleanly done by iterate-and-skip:

# Return the date that is N business days after `start`.
# N > 0 moves forward. Does not count `start` itself.

date = start
added = 0
while added < N:
    date = date + 1 day
    if is_weekday(date) and not is_holiday(date):
        added += 1
return date

This is O(N) in business days, which is fine in practice — nobody adds ten thousand business days. A closed-form version exists (compute full weeks, then walk the remainder), but the iterate-and-skip form is hard to get wrong and trivial to extend with a custom weekend or holiday set.

The thing to internalize: "+5 business days" is not "+7 calendar days" except by coincidence. Add 5 business days to a Monday and you land on the next Monday — 7 calendar days — only if no holiday falls in between. Add 5 business days to a Wednesday and you reach the following Wednesday, also 7 calendar days, unless there's a holiday, in which case it's 8. Start mid-week with a holiday in range and the calendar gap can be 9 or more. The relationship is not a fixed multiplier.

The holiday problem

Weekends are a fixed rule. Holidays are data, and data rots.

  • Holidays are per-region. A correct calculation needs the holiday set for the relevant jurisdiction — and "relevant" may be a country, a state or province, or a specific stock exchange or bank system. A US federal calendar will not match a New York Stock Exchange trading calendar, which will not match a German Land.
  • Observed shifts. When a fixed-date holiday lands on a weekend, many systems observe it on an adjacent weekday — a Saturday holiday observed the preceding Friday, a Sunday holiday the following Monday. The observed day is the business-day exclusion, not the nominal date, and the rule for which way it shifts is itself jurisdiction-specific.
  • Moving holidays. Some holidays are defined relative to other events or by lunar or lunisolar calendars and fall on a different Gregorian date every year. Their dates cannot be computed by a simple fixed-month-and-day rule and have to come from an authority that publishes them.

This is why a holiday list hardcoded in 2026 is wrong by 2028: new holidays get declared, one-off holidays (elections, royal events, national mourning) appear, and the moving ones have shifted. A business-day calculator needs a maintained calendar — a library that tracks the rules, or a data source you update — not a constant baked into the source. Our holiday calendar tool exists to be that source: per-country sets you can inspect rather than guess at.

Edge cases that bite

  • Half-days. Some calendars include partial business days — an exchange closing early, a half-day before a major holiday. If your unit is whole business days, decide explicitly whether a half-day counts as one, zero, or a fraction, and document it.
  • Time zones. "End of business day" is meaningless without a zone. A deadline at 5:00 PM in whose city? A submission that is on time in Los Angeles can be a day late in Frankfurt. Date math that ignores the zone produces deadlines that are off by a full day for anyone on the other side of a date line — the same class of bug discussed in Unix timestamps and the 2038 problem.
  • Cutoff times. Many "business day" rules have a same-day cutoff: an order placed before 2:00 PM ships today, after 2:00 PM counts as the next business day. The cutoff effectively moves the start date before any business-day arithmetic begins, so apply it first.
  • Roll conventions. When a computed deadline lands on a non-business day, the contract decides whether it rolls forward to the next business day, backward to the previous one, or stays put. This is a separate rule from the count itself and has to be stated.

The arithmetic is simple once the inputs are pinned: a weekend definition, a holiday calendar, an endpoint convention, a time zone, and a roll rule. The errors live almost entirely in leaving one of those implicit.

If you need an answer without wiring up a calendar library, our business days calculator counts working days between two dates and adds or subtracts business days against a maintained per-country holiday set — endpoint conventions handled, so you can check a deadline without rebuilding the holiday logic yourself.