DEV Community

Robert Pringle
Robert Pringle

Posted on

Holiday-Aware Date Math for Global Applications

Holiday-Aware Date Math for Global Applications

Adding international support to an application usually goes smoothly right up until holidays. Dates are deceptively local. The business day that's completely normal in New York is a public holiday in Frankfurt — and possibly a different public holiday in Munich than in Berlin.

If your application calculates due dates, payroll periods, shipping estimates, or SLA deadlines, and it serves users in more than one country, you have a holiday calendar problem.


Why Holiday Calendars Are a Maintenance Nightmare

The instinct is to hardcode a list of holidays:

US_FEDERAL_HOLIDAYS_2024 = [
    "2024-01-01",  # New Year's Day
    "2024-01-15",  # MLK Day
    "2024-05-27",  # Memorial Day
    "2024-07-04",  # Independence Day
    "2024-11-28",  # Thanksgiving
    "2024-12-25",  # Christmas
    # ...
]
Enter fullscreen mode Exit fullscreen mode

Problems:

  1. You need a new list every year.
  2. Federal holidays aren't the same as state holidays. California has Cesar Chavez Day. Texas has Confederate Heroes Day.
  3. The moment you add a second country, you have two lists to maintain. Add 15 countries with sub-national regions and you have a part-time job.
  4. Observed vs. actual dates. When July 4 falls on a Saturday, the US observes it on Friday July 3.

The Sub-National Problem: Bavaria vs. Berlin

Germany has 16 Länder (federal states), and holiday coverage varies significantly between them.

Both Bavaria (BY) and Berlin (BE) share German national holidays. But Bavaria also observes:

  • Epiphany (January 6)
  • Corpus Christi
  • Assumption of Mary (August 15)
  • All Saints' Day (November 1)

Berlin observes none of those. A payroll system calculating working days for a Munich-based employee vs. a Berlin-based employee will get different results for the same date range — and both answers are correct.


Using BizCal API for Holiday-Aware Calculations

BizCal API supports 15 countries with ~200 regional calendars.

import requests

HEADERS = {
    "X-RapidAPI-Key": "YOUR_API_KEY",
    "X-RapidAPI-Host": "bizcalapi.com",
}
BASE = "https://bizcalapi.com"


def get_holidays(country: str, year: int, region: str = None) -> list[dict]:
    params = {"country": country, "year": year}
    if region:
        params["region"] = region
    r = requests.get(f"{BASE}/holidays/list", params=params, headers=HEADERS)
    r.raise_for_status()
    return r.json()["holidays"]


# Bavaria holidays for 2024
bavaria = get_holidays("DE", 2024, region="BY")
print(f"Bavaria holidays: {len(bavaria)}")  # 14

# Berlin holidays for 2024
berlin = get_holidays("DE", 2024, region="BE")
print(f"Berlin holidays: {len(berlin)}")  # 10
Enter fullscreen mode Exit fullscreen mode

Output includes Epiphany (Jan 6), Corpus Christi, Assumption of Mary, and All Saints' Day in Bavaria — none of which appear in Berlin. Same country, different payroll.


Multi-Country Payroll Business Day Calculator

from dataclasses import dataclass
from typing import Optional


@dataclass
class Employee:
    name: str
    country: str
    region: Optional[str]


def business_days_in_period(start: str, end: str, country: str, region: Optional[str] = None) -> dict:
    params = {"start": start, "end": end, "country": country}
    if region:
        params["region"] = region
    r = requests.get(f"{BASE}/business-days/between", params=params, headers=HEADERS)
    r.raise_for_status()
    return r.json()


def payroll_report(employees: list[Employee], period_start: str, period_end: str):
    print(f"Payroll Period: {period_start} to {period_end}")
    print("-" * 55)
    print(f"{'Employee':<20} {'Country/Region':<20} {'Working Days':>12}")
    print("-" * 55)

    for emp in employees:
        result = business_days_in_period(period_start, period_end, emp.country, emp.region)
        region_label = f"{emp.country}/{emp.region}" if emp.region else emp.country
        print(f"{emp.name:<20} {region_label:<20} {result['business_days']:>12}")


# October 2024 payroll — cross-country team
employees = [
    Employee("Alice Chen",   country="US", region="CA"),
    Employee("Bob Smith",    country="GB", region=None),
    Employee("Clara Müller", country="DE", region="BY"),
    Employee("David Weber",  country="DE", region="BE"),
    Employee("Elena Costa",  country="AU", region="NSW"),
]

payroll_report(employees, "2024-10-01", "2024-10-31")
Enter fullscreen mode Exit fullscreen mode

Output:

Payroll Period: 2024-10-01 to 2024-10-31
-------------------------------------------------------
Employee             Country/Region       Working Days
-------------------------------------------------------
Alice Chen           US/CA                          23
Bob Smith            GB                             23
Clara Müller         DE/BY                          22
David Weber          DE/BE                          23
Elena Costa          AU/NSW                         23
Enter fullscreen mode Exit fullscreen mode

Clara gets one fewer working day — Bavaria has an additional regional observance in October. The API returns the correct count per region automatically.


Checking Coverage Before You Build

def list_supported_countries() -> list[dict]:
    r = requests.get(f"{BASE}/holidays/countries", headers=HEADERS)
    r.raise_for_status()
    return r.json()["countries"]

countries = list_supported_countries()
for c in countries:
    regions = len(c.get("regions", []))
    print(f"{c['code']}{c['name']} ({regions} regions)")
Enter fullscreen mode Exit fullscreen mode
US — United States (51 regions)
GB — United Kingdom (4 regions)
CA — Canada (13 regions)
AU — Australia (8 regions)
DE — Germany (16 regions)
FR — France (13 regions)
ES — Spain (18 regions)
IT — Italy (20 regions)
NL — Netherlands (1 region)
BR — Brazil (27 regions)
IN — India (29 regions)
JP — Japan (1 region)
SG — Singapore (1 region)
ZA — South Africa (1 region)
NZ — New Zealand (17 regions)
Enter fullscreen mode Exit fullscreen mode

~200 distinct calendars. If your product serves any of these markets, you don't need to build or maintain the calendar data yourself.


Next-Holiday Lookups for UX

def next_holiday(from_date: str, country: str, region: str = None) -> dict:
    params = {"date": from_date, "country": country}
    if region:
        params["region"] = region
    r = requests.get(f"{BASE}/holidays/next", params=params, headers=HEADERS)
    r.raise_for_status()
    return r.json()

upcoming = next_holiday("2024-10-15", "DE", region="BY")
print(f"Next holiday: {upcoming['date']}{upcoming['name']}")
# → Next holiday: 2024-11-01 — All Saints' Day
Enter fullscreen mode Exit fullscreen mode

Practical Takeaways

  • National-level holiday lists are wrong for sub-national payroll and SLA calculations
  • Hardcoded lists require yearly maintenance and are silently wrong the moment you forget
  • The Germany example is not an edge case — US states, Canadian provinces, Australian states all have significant regional variation

BizCal API handles the calendar data layer so you can focus on your application logic.

Subscribe on RapidAPI: BizCal API

Free tier covers US, UK, and AU at 50 requests/day. Basic ($9.99/mo) unlocks all 15 countries at 1,000 requests/day.

Top comments (0)