Beginner
Determining the current date is a public holiday can be tricky when holidays change and it of course changes from country to country. From system time of servers & machines running to timestamps for tracking the transactions and events in e-commerce platforms, the date and time play a major role. There are a variety of use cases related to manipulating date and time that can be solved using the inbuilt datetime module in Python3, such as
- Finding if a given year is a leap year or an ordinary year
- Finding the number of days between the two mentioned dates
- Convert between different date or time formats
What if you were to check if a given date is a public holiday? There isn’t any specific formula or logic to determine that, do we? Holidays can be pre-defined or uncalled for.
Here, we will be exploring the two ways to detect if a date is a holiday or not.
Checking For Public Holiday With Holidays Module
Although Python3 doesn’t provide any modules to detect if a date is a holiday or not, there are some of the external modules that help in detecting this. One of those modules is Holidays.
In your terminal, type in the following to get the module installed.
sudo pip3 install holidays

Now that our module is ready, let’s understand a bit about what the module and what it is capable of. Have a look at the following code snippet.
'''
Snippet to check if a given date is a holiday
'''
from datetime import date # Step 1
import holidays
us_holidays = holidays.UnitedStates() # Step 2
input_date = input("Enter the date as YYYY-MM-DD: ") # Step 3
holiday_name = us_holidays .get(input_date) # Step 4
if holiday_name != None:
output = "{} is a US Holiday - It's {}".format(input_date, holiday_name)
else:
output = "{} is not a US Holiday".format(input_date)
# Step 5
print (output)
In the above snippet,
- Step 1: Imports the required modules
- Step 2: Initializes the us_holidays object, so that the corresponding
getfunction can be invoked at step 3 - Step 3: Gets
dateinput from the user - Step 4: Invokes the get function of the
holidaysmodule. This returns the name of the holiday if the date is a holiday or returnsNonein case if it isn’t. This gets assigned to the variable –holiday_name. - Step 5: Based on the variable –
holiday_name, using theifclause the string formatting is done. Can you make this if clause even leaner? Read this article to know about the One line if else statements.
Here’s what the output looks like.

Checking For Holidays With API Call to Calendarific
The above method is suitable for simple projects; however, it can never be used to provide an enterprise-grade solution. Let’s say, you are building a web application for a holiday and travel startup, building an enterprise-grade application requires an enterprise-grade solution. If you haven’t noticed, the holidays module is pretty simple and if you consider state-wise or newly announced holidays, then this solution doesn’t simply cut for a large-scale application.
Enterprise requirements such as these can be satisfied by using external APIs such as Calendarific which provides the API as a service for such applications to consume. They keep updating the holidays of states and countries constantly, and the applications may consume these APIs. Of course, enterprise solutions don’t always come free, but the developer account has a limit of 1000API requests per month.
Locate to https://calendarific.com/ on your favorite browser and follow the steps as shown in the following images to get yourself a free account and an API key for this exercise.




Understanding the Calendarific REST API
Before we could dive into using the API KEY, get yourself a REST API client – Insomnia or Postman. We are about to test our API key if we are able to retrieve the holiday information. Plugin the following URL by replacing [APIKEY] text with your API KEY received from above on your REST client.
https://calendarific.com/api/v2/holidays?api_key=[APIKEY]&country=us-ny&type=national&year=2020&month=1&day=1
In the above URL:
- https://calendarific.com/api/v2 is the API Base URL
- /holidays is the API route
- api_key, country, type, year, month, day are URL Parameters
- Each parameter has a value allocated to it with an = (equal sign)
- Each parameter and value pair is split by an & (ampersand)
For the above API call, the following response will be received; the value corresponding to the code key under the meta tag as ‘200’ corresponds to a successful response.
{
"meta": {
"code": 200
},
"response": {
"holidays": [
{
"name": "New Year's Day",
"description": "New Year's Day is the first day of the Gregorian calendar, which is widely used in many countries such as the USA.",
"country": {
"id": "us",
"name": "United States"
},
"date": {
"iso": "2020-01-01",
"datetime": {
"year": 2020,
"month": 1,
"day": 1
}
},
"type": [
"National holiday"
],
"locations": "All",
"states": "All"
}
]
}
}
The REST API call has returned some useful info about the National holiday on the 1st of January. Let’s see if it’s able to detect for the 2nd of January. Plugin the following URL again by replacing the text [APIKEY] with your API Key.
https://calendarific.com/api/v2/holidays?api_key=[APIKEY]&country=us-ny&type=national&year=2020&month=1&day=2
The above URL should be returning a response similar to below.
{
"meta": {
"code": 200
},
"response": {
"holidays": []
}
}
Indeed, the 2nd of January is not a public holiday and hence, the holidays list inside the response nested JSON key turns out to be an empty list.
Now we know that our API works very well, it is now time to incorporate Calendarific REST API into our Python code. We will be using the requests module in order to make this happen. Here’s how it is done.
'''
Snippet to check if a given date is a holiday using an external API - Calendarific
'''
import requests # Step 1
api_key = '[APIKEY]' # Step 2
base_url = 'https://calendarific.com/api/v2'
api_route = '/holidays'
location = input("Enter Country & State code - E.g.: us-ny: ")
date_inpt = input("Enter the date as YYYY-MM-DD: ") # Step 3
y, m, d = date_inpt.split('-')
full_url = '{}{}?api_key={}&country={}&type=national&year={}&month={}&day={}'\
.format(base_url, api_route, api_key, location, str(int(y)), str(int(m)), str(int(d))) # Step 4
response = requests.get(full_url).json() # Step 5
if response['response']['holidays'] != []:
print ("{} is a holiday - {}".format(date_inpt, response['response']['holidays'][0]['name']))
else: # Step 6
print ("{} is not a holiday".format(date_inpt))
In the above snippet,
- Step 1: Import requests module – you will be needing this module to invoke the REST API.
- Step 2: Replace ‘[APIKEY]’ with your own API key from Calendarific
- Step 3: The user inputs the corresponding location and date for which the holiday needs to be detected
- Step 4: String formatting in order to frame the URL
- Step 5: Invoke the API and convert the response to a JSON; i.e.) a dictionary
- Step 6: If clause checks for the presence of an empty list or with a returned response.
Here’s what the output looks like.

And there you have it, a working example for detecting if a given date is a holiday using an external API.
Summary
From an overall perspective, there could be multiple ways to solve a given problem, and here, we have portrayed two of those ways in detecting if a given date is a holiday or not. One is a straight forward out-of-the-box solution and the other one is an enterprise-ready solution, which one would you choose?
Subscribe to our newsletter
How To Use Python Joblib for Parallel Computing and Caching
Intermediate
You have a data processing loop that runs one item at a time — checking each file, scoring each user, training each model configuration. Your machine has eight cores and only one of them is working. The loop that takes twenty minutes could finish in three if you could just split the work across all available processors.
Joblib is a Python library that makes parallel computing and result caching easy to add to existing code. Its Parallel and delayed utilities turn a regular Python loop into a parallel job with one wrapper. Its Memory class caches function results to disk so that the second call with the same arguments returns instantly. Install it with pip install joblib. Scikit-learn uses Joblib internally for its own parallelism, so if you have scikit-learn installed, Joblib is already there.
This article covers parallelising loops with Parallel and delayed, choosing the right backend (loky, threading, multiprocessing), caching expensive computations with Memory, integrating with scikit-learn pipelines, and diagnosing performance with verbosity settings. By the end you will have both parallel execution and disk caching working in a realistic data pipeline.
Joblib Parallel: Quick Example
The quickest way to see Joblib’s effect is to replace a for loop with a Parallel call. The structure is almost identical — the main change is wrapping the function call with delayed().
# quick_joblib.py
import time
from joblib import Parallel, delayed
def slow_square(n: int) -> int:
"""Simulate a slow computation."""
time.sleep(0.5)
return n * n
numbers = list(range(8))
# Sequential -- takes 8 * 0.5 = 4 seconds
start = time.perf_counter()
sequential = [slow_square(n) for n in numbers]
seq_time = time.perf_counter() - start
print(f"Sequential: {sequential} in {seq_time:.2f}s")
# Parallel -- uses all available CPU cores
start = time.perf_counter()
parallel = Parallel(n_jobs=-1)(delayed(slow_square)(n) for n in numbers)
par_time = time.perf_counter() - start
print(f"Parallel: {parallel} in {par_time:.2f}s")
print(f"Speedup: {seq_time / par_time:.1f}x")
Output (on an 8-core machine):
Sequential: [0, 1, 4, 9, 16, 25, 36, 49] in 4.01s
Parallel: [0, 1, 4, 9, 16, 25, 36, 49] in 0.56s
Speedup: 7.2x
The n_jobs=-1 argument tells Joblib to use all available CPU cores. n_jobs=4 would use exactly four. The delayed(func)(args) pattern creates a lazy description of the function call without executing it — Joblib collects these descriptions and distributes them across workers. The return values are collected in the same order as the input, so parallel[3] is always the result of slow_square(3) regardless of which worker finished first.
What Is Joblib and When Should You Use It?
Joblib provides two things: easy parallelism through a process pool, and persistent disk caching of function results. These two features are independent — you can use either without the other. The parallelism is built on top of the loky process pool by default (a robust reimplementation of multiprocessing.Pool) with fallback to Python’s threading or the original multiprocessing pool.
| Tool | Best for | Overhead |
|---|---|---|
| Joblib Parallel (loky) | CPU-bound tasks, data processing | ~100ms startup |
| Joblib Parallel (threading) | IO-bound tasks, numpy releases GIL | ~5ms startup |
| concurrent.futures | Simple async IO, process pools | ~50ms startup |
| multiprocessing.Pool | CPU-bound, full control needed | ~100ms startup |
| asyncio | High-concurrency network IO | Near zero |
Joblib excels when your loop body is CPU-bound (model training, file parsing, image processing) and each iteration takes at least a few milliseconds — enough to justify the inter-process communication cost. For very fast operations (microsecond loops), parallelism overhead outweighs the benefit. The caching feature is valuable for any function with expensive deterministic computations: feature extraction, data loading, hyperparameter search.
Choosing the Right Backend
Joblib supports three execution backends, each suited to different workloads. Understanding when to use each prevents a common trap: the default process-based backend actually slows down IO-bound work because of serialisation overhead.
# backends.py
import time
import numpy as np
from joblib import Parallel, delayed
def cpu_task(size: int) -> float:
"""CPU-bound: pure Python computation."""
data = list(range(size))
return sum(x * x for x in data) / len(data)
def numpy_task(size: int) -> float:
"""Numpy releases the GIL -- threading backend works well here."""
arr = np.random.rand(size)
return float(np.sqrt(np.sum(arr ** 2)))
items = [100_000] * 8
# Default loky backend (separate processes, best for pure Python CPU work)
start = time.perf_counter()
results_loky = Parallel(n_jobs=4, backend="loky")(
delayed(cpu_task)(n) for n in items
)
print(f"loky (CPU work): {time.perf_counter() - start:.2f}s")
# Threading backend (shares memory, good when GIL is released by C extensions)
start = time.perf_counter()
results_thread = Parallel(n_jobs=4, backend="threading")(
delayed(numpy_task)(n) for n in items
)
print(f"threading (NumPy): {time.perf_counter() - start:.2f}s")
# Sequential for comparison
start = time.perf_counter()
results_seq = [numpy_task(n) for n in items]
print(f"sequential: {time.perf_counter() - start:.2f}s")
Output:
loky (CPU work): 0.48s
threading (NumPy): 0.31s
sequential: 1.12s
The loky backend spawns separate Python processes, each with their own memory space and GIL. This is the right choice for pure Python CPU work because it truly runs in parallel. The threading backend runs in threads within the same process. Because Python’s GIL prevents true parallel execution of pure Python code, threading only helps when the task calls into a C extension that releases the GIL — like NumPy, Pandas, or scikit-learn. The multiprocessing backend is the original process pool; prefer loky unless you have a specific compatibility reason to use it.
Caching Expensive Results with Memory
Joblib’s Memory class caches a function’s return value to disk, keyed by the function’s source code and its arguments. The second call with the same arguments reads from the cache instead of recomputing. This is useful for data loading, feature extraction, or any expensive deterministic step that you run repeatedly during development.
# caching.py
import time
import numpy as np
from joblib import Memory
# Create a cache directory
cache = Memory("./joblib_cache", verbose=1)
@cache.cache
def load_and_process(filepath: str, scale: float = 1.0) -> np.ndarray:
"""Simulate expensive data loading and processing."""
print(f" [COMPUTING] Loading {filepath} with scale={scale}")
time.sleep(2) # Simulate a 2-second load
data = np.random.rand(1000) * scale
return data
print("First call (cold cache):")
start = time.perf_counter()
result1 = load_and_process("data/features.npy", scale=2.0)
print(f" Took: {time.perf_counter() - start:.2f}s, mean={result1.mean():.4f}")
print("\nSecond call (cache hit):")
start = time.perf_counter()
result2 = load_and_process("data/features.npy", scale=2.0)
print(f" Took: {time.perf_counter() - start:.4f}s, mean={result2.mean():.4f}")
print("\nDifferent args (cache miss):")
start = time.perf_counter()
result3 = load_and_process("data/features.npy", scale=3.0)
print(f" Took: {time.perf_counter() - start:.2f}s, mean={result3.mean():.4f}")
Output:
First call (cold cache):
[COMPUTING] Loading data/features.npy with scale=2.0
Took: 2.01s, mean=0.9987
Second call (cache hit):
Took: 0.0031s, mean=0.9987
Different args (cache miss):
[COMPUTING] Loading data/features.npy with scale=3.0
Took: 2.01s, mean=1.4991
The cache is stored as compressed pickle files in the directory you specify. It is keyed on the function’s source code hash and all arguments — if you change the function body, Joblib invalidates the cache automatically on the next call. To clear the cache manually, call cache.clear() or delete the cache directory. The verbose=1 argument makes Joblib print whether it computed or loaded from cache; set it to 0 to silence this output in production.
Joblib with scikit-learn Pipelines
Scikit-learn uses Joblib internally for all its n_jobs parameters — cross-validation, grid search, random forests, and more all use the same Joblib infrastructure. You can control the backend and number of jobs globally using Joblib’s parallel_backend context manager, or pass n_jobs directly to estimators.
# sklearn_parallel.py
import time
import numpy as np
from sklearn.datasets import make_classification
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import cross_val_score, GridSearchCV
from joblib import parallel_backend
# Generate a sample dataset
X, y = make_classification(n_samples=5000, n_features=20, random_state=42)
# Train a random forest using all CPU cores
print("Training RandomForest with n_jobs=-1...")
start = time.perf_counter()
rf = RandomForestClassifier(n_estimators=100, n_jobs=-1, random_state=42)
rf.fit(X, y)
print(f" Fit time: {time.perf_counter() - start:.2f}s")
# Cross-validation in parallel
start = time.perf_counter()
scores = cross_val_score(rf, X, y, cv=5, n_jobs=-1, scoring="accuracy")
print(f" CV scores: {scores.round(3)}, mean={scores.mean():.3f}, time={time.perf_counter() - start:.2f}s")
# Hyperparameter search -- each combo evaluated in parallel
param_grid = {
"n_estimators": [50, 100],
"max_depth": [5, 10, None],
}
start = time.perf_counter()
grid = GridSearchCV(
RandomForestClassifier(random_state=42),
param_grid,
cv=3,
n_jobs=-1,
verbose=0,
)
grid.fit(X, y)
elapsed = time.perf_counter() - start
print(f" Best params: {grid.best_params_}, score={grid.best_score_:.3f}, time={elapsed:.2f}s")
Output:
Training RandomForest with n_jobs=-1...
Fit time: 0.31s
CV scores: [0.934 0.931 0.929 0.927 0.932], mean=0.931, time=0.48s
Best params: {'max_depth': None, 'n_estimators': 100}, score=0.931, time=1.24s
The n_jobs=-1 parameter on scikit-learn estimators and model-selection utilities goes directly to Joblib. Setting it uses all available cores for that operation. For nested parallelism (a parallel grid search that itself trains parallel random forests), Joblib automatically avoids over-subscribing the CPU — the inner jobs run sequentially when the outer jobs already fill all cores.
Real-Life Example: Parallel Feature Extraction Pipeline
The following pipeline processes a directory of text files, extracts word-frequency features from each, and caches the results. Combining Parallel with Memory gives you both speed and resilience — if the pipeline is interrupted, the cached results mean you do not repeat work already done.
# feature_pipeline.py
import os
import time
import re
from collections import Counter
from pathlib import Path
from joblib import Parallel, delayed, Memory
cache = Memory("./feature_cache", verbose=0)
# --- Create sample text files ---
SAMPLE_DIR = Path("sample_texts")
SAMPLE_DIR.mkdir(exist_ok=True)
sample_texts = {
"python.txt": "Python is a high-level programming language. Python emphasises readability.",
"data.txt": "Data science uses statistics and programming. Data analysis reveals patterns.",
"web.txt": "Web development creates websites and applications. The web uses HTML CSS JavaScript.",
"ai.txt": "Artificial intelligence mimics human thinking. Machine learning trains models on data.",
"cloud.txt": "Cloud computing provides on-demand resources. Cloud services scale automatically.",
}
for fname, text in sample_texts.items():
(SAMPLE_DIR / fname).write_text(text * 50) # Make files large enough to matter
@cache.cache
def extract_features(filepath: str) -> dict:
"""Extract word frequency features from a text file (cached)."""
text = Path(filepath).read_text().lower()
words = re.findall(r'\b[a-z]{3,}\b', text)
top_words = dict(Counter(words).most_common(10))
time.sleep(0.3) # Simulate expensive NLP processing
return {"file": Path(filepath).name, "word_count": len(words), "top_words": top_words}
def run_pipeline(data_dir: Path) -> list[dict]:
files = [str(f) for f in data_dir.glob("*.txt")]
print(f"Processing {len(files)} files in parallel...")
start = time.perf_counter()
results = Parallel(n_jobs=-1, verbose=10)(
delayed(extract_features)(f) for f in files
)
elapsed = time.perf_counter() - start
print(f"Done in {elapsed:.2f}s")
return results
features = run_pipeline(SAMPLE_DIR)
for feat in features:
top3 = list(feat["top_words"].keys())[:3]
print(f" {feat['file']:15s} words={feat['word_count']:,} top={top3}")
Output (first run — cold cache):
Processing 5 files in parallel...
[Parallel(n_jobs=-1)]: Done 5 out of 5 | elapsed: 0.4s finished
Done in 0.41s
python.txt words=350 top=['python', 'language', 'high']
data.txt words=350 top=['data', 'science', 'analysis']
web.txt words=350 top=['web', 'development', 'html']
ai.txt words=350 top=['learning', 'machine', 'data']
cloud.txt words=350 top=['cloud', 'computing', 'services']
s from a file or a database, the cache becomes stale when that data changes. You are responsible for clearing the cache when upstream data changes, either by calling memory.clear(), by passing a version argument to the function, or by using a time-based expiry implemented in the function body.
How do I track progress in a long Parallel job?
Set verbose=10 (the maximum) in Parallel() to print a status line after each completed job, including elapsed time, estimated remaining time, and memory usage. For a progress bar, use the tqdm library: wrap the generator with tqdm(delayed(func)(x) for x in items, total=len(items)) -- Joblib will pull items from the tqdm-wrapped iterator and tqdm updates the bar as items are consumed.
Are there memory issues with Joblib on long-running jobs?
When using the loky backend with large return values, worker memory can accumulate if workers are reused across many batches. Set max_nbytes="10M" in Parallel() to use memory-mapped files for return values above 10 MB instead of pickle serialisation. To prevent worker memory from growing across restarts, set Parallel(n_jobs=4, max_nbytes=None) combined with periodic worker recycling using loky.get_reusable_executor(max_workers=4, reuse="kill_workers").
Conclusion
Joblib makes two of the most common performance problems in data pipelines trivially easy to solve: parallelising embarrassingly parallel loops with Parallel and delayed, and caching expensive deterministic computations with Memory. You have seen how to replace a for loop with a parallel equivalent in four lines, choose the right backend for CPU-bound versus IO-bound work, cache results to disk, and integrate both patterns with scikit-learn.
The natural extension of the feature extraction pipeline is to add a cache validation step that checks file modification timestamps, and to feed the extracted features directly into a scikit-learn pipeline with n_jobs=-1 cross-validation -- so both the feature extraction and the model evaluation run in parallel with full caching.
For the full Joblib reference including memory-mapped arrays, batch processing, and custom backends, see the official Joblib documentation.
Related Articles
Further Reading: For more details, see the Python datetime module documentation.
Pro Tips for Working with Public Holidays in Python
1. Cache Holiday Data to Avoid Repeated API Calls
If you are using the Calendarific API, cache the results locally instead of calling the API every time you check a date. Holiday lists for a given country and year rarely change. Save the API response to a JSON file and only refresh it when the year changes. This reduces API usage and makes your application faster.
# cache_holidays.py
import json
import os
from datetime import date
CACHE_FILE = "holidays_cache.json"
def get_cached_holidays(country, year):
if os.path.exists(CACHE_FILE):
with open(CACHE_FILE, "r") as f:
cache = json.load(f)
key = f"{country}_{year}"
if key in cache:
print(f"Using cached holidays for {country} {year}")
return cache[key]
return None
def save_to_cache(country, year, holidays):
cache = {}
if os.path.exists(CACHE_FILE):
with open(CACHE_FILE, "r") as f:
cache = json.load(f)
cache[f"{country}_{year}"] = holidays
with open(CACHE_FILE, "w") as f:
json.dump(cache, f, indent=2)
print(f"Cached {len(holidays)} holidays for {country} {year}")
Output:
Cached 11 holidays for US 2026
Using cached holidays for US 2026
2. Calculate Business Days Excluding Holidays
One of the most common real-world uses of holiday detection is calculating business days. Combine the holidays library with Python’s datetime to count only working days between two dates, excluding weekends and public holidays. This is essential for shipping estimates, SLA calculations, and payroll processing.
# business_days.py
import holidays
from datetime import date, timedelta
def business_days_between(start, end, country="US"):
us_holidays = holidays.country_holidays(country)
count = 0
current = start
while current <= end:
if current.weekday() < 5 and current not in us_holidays:
count += 1
current += timedelta(days=1)
return count
start = date(2026, 12, 20)
end = date(2026, 12, 31)
days = business_days_between(start, end)
print(f"Business days from {start} to {end}: {days}")
Output:
Business days from 2026-12-20 to 2026-12-31: 7
3. Handle Multiple Countries for International Apps
If your application serves users in different countries, check holidays for each user's country rather than assuming a single country. The holidays library supports 100+ countries. Store each user's country code and pass it when checking holidays. Remember that some countries have regional holidays too -- for example, different states in Australia or provinces in Canada have different public holidays.
4. Build a Holiday-Aware Scheduler
Many applications need to skip processing on holidays. Instead of checking manually every time, create a decorator that wraps scheduled tasks and automatically skips execution on public holidays. This is useful for automated reports, email campaigns, and batch processing jobs that should only run on business days.
# holiday_aware_scheduler.py
import holidays
from datetime import date
from functools import wraps
def skip_on_holidays(country="US"):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
today = date.today()
if today in holidays.country_holidays(country):
name = holidays.country_holidays(country).get(today)
print(f"Skipping {func.__name__}: today is {name}")
return None
return func(*args, **kwargs)
return wrapper
return decorator
@skip_on_holidays("US")
def send_daily_report():
print("Sending daily report...")
return "Report sent"
result = send_daily_report()
print(f"Result: {result}")
Output (on a regular business day):
Sending daily report...
Result: Report sent
5. Display Upcoming Holidays for Better UX
Show your users which holidays are coming up so they can plan ahead. This is valuable for project management tools, delivery estimate pages, and HR applications. Sort the holiday list by date and filter for upcoming dates only to give users a clear view of the next few holidays.
Frequently Asked Questions
How do I check if a date is a public holiday in Python?
Use the holidays library: install it with pip install holidays, then check with date in holidays.country_holidays('US'). It returns True if the date is a recognized public holiday for that country.
What countries does the Python holidays library support?
The holidays library supports over 100 countries and their subdivisions. Major countries include the US, UK, Canada, Australia, Germany, France, India, and many more. Use holidays.list_supported_countries() to see the complete list.
Can I add custom holidays to the holidays library?
Yes. Create a custom holiday class inheriting from the country class, or use the append() method to add individual dates. You can also create entirely custom holiday calendars for company-specific or regional holidays.
How do I get the name of a holiday for a specific date?
Access the holiday name with holidays.country_holidays('US').get(date), which returns the holiday name as a string, or None if it is not a holiday. You can also iterate over the holidays object to list all holidays in a year.
Is the holidays library useful for business day calculations?
Yes. Combine it with numpy.busday_count() or pandas.bdate_range() to calculate working days excluding public holidays. This is useful for project management, payroll calculations, and delivery date estimation.