Skip to main content

Workday calculations with a built-in Norwegian calendar

Project description

pyworkdates

Dependency-free workday calculations for Python, with a built-in Norwegian calendar.

PyPI License: MIT

pyworkdates answers questions like is this date a workday?, what date is three workdays from now?, and which workdays fall in this range? — using only the Python standard library. It ships with a built-in Norwegian holiday calendar, and every function accepts a calendar argument to override it with any collection of dates.

Installation

pip install pyworkdates

or with uv:

uv add pyworkdates

Quickstart

Using the built-in Norwegian calendar (the default when no calendar is given):

from pyworkdates import add_workdays, is_workday, next_workday

print(is_workday("2026-07-06"))       # True   (a regular Monday)
print(is_workday("2026-05-01"))       # False  (a holiday in the built-in calendar)
print(is_workday("2026-07-11"))       # False  (a Saturday)

print(add_workdays("2026-07-06", 3))  # 2026-07-09
print(next_workday("2026-07-10"))     # 2026-07-13  (Friday -> Monday)

Overriding the calendar per call — pass any collection of dates:

from pyworkdates import add_workdays, is_workday

us_holidays = ["2026-07-03", "2026-11-26", "2026-12-25"]  # a few US 2026 holidays

print(is_workday("2026-07-03", calendar=us_holidays))       # False
print(is_workday("2026-05-01", calendar=us_holidays))       # True  (not in this calendar)
print(add_workdays("2026-07-02", 1, calendar=us_holidays))  # 2026-07-06

Defining a calendar once and reusing it with WorkdayCalendar:

from pyworkdates import WorkdayCalendar

us = WorkdayCalendar(["2026-07-03", "2026-11-26", "2026-12-25"])

print(us.is_workday("2026-07-03"))    # False
print(us.next_workday("2026-11-25"))  # 2026-11-27
print(us.workdays_between("2026-12-24", "2026-12-28"))
# [datetime.date(2026, 12, 24), datetime.date(2026, 12, 28)]

Key concepts

  • Workday: a day that is neither a weekend day (Saturday/Sunday) nor a date in the active calendar.

  • Built-in calendar: Norwegian holidays, hardcoded for the years 2016–2050. It is the active calendar whenever calendar is not given.

  • Coverage guard: any computation that touches a date outside 2016–2050 while using the built-in calendar raises ValueError — never a silent wrong answer. Pass an explicit calendar to work outside that range:

    >>> from pyworkdates import is_workday
    >>> is_workday("2055-01-04")
    Traceback (most recent call last):
        ...
    ValueError: Date 2055-01-04 is outside the built-in calendar's coverage (2016-2050). Pass an explicit 'calendar' argument to work with dates outside this range.
    >>> is_workday("2055-01-04", calendar=["2055-01-01"])
    True
    
  • Custom calendars: any iterable of datetime.date, datetime.datetime, or ISO strings "YYYY-MM-DD", freely mixed. Custom calendars are trusted as-is and never range-checked.

  • Input and output types: every function accepts a datetime.date, a datetime.datetime (its date part is used), or an ISO string "YYYY-MM-DD". Date-returning functions always return datetime.date. Invalid input raises ValueError.

API reference

Every function takes an optional calendar keyword argument: None (the default) means the built-in Norwegian calendar; otherwise the supplied calendar is used. All examples below are doctest-verified.

is_workday(date, calendar=None) -> bool

True if the date is a workday: neither a weekend day nor a date in the active calendar.

>>> from pyworkdates import is_workday
>>> is_workday("2026-07-06")  # a regular Monday
True
>>> is_workday("2026-05-01")  # a holiday in the built-in calendar
False

is_holiday(date, calendar=None) -> bool

True if the date is in the active calendar. Weekend days are not holidays unless the calendar contains them.

>>> from pyworkdates import is_holiday
>>> is_holiday("2026-05-01")
True
>>> is_holiday("2026-07-11")  # a Saturday, but not in the calendar
False

add_workdays(date, n, calendar=None) -> datetime.date

The date moved n workdays forward, or backward for negative n. Starting from a non-workday does not first roll to one: Saturday plus one workday is Monday, and Saturday minus one workday is Friday (holidays permitting). For n=0 the date is returned unchanged if it is a workday, otherwise it rolls forward to the nearest workday. n must be an int; bool and non-int values raise ValueError.

>>> from pyworkdates import add_workdays
>>> add_workdays("2026-07-06", 3)   # Monday + 3 workdays
datetime.date(2026, 7, 9)
>>> add_workdays("2026-07-06", -1)  # Monday - 1 workday
datetime.date(2026, 7, 3)
>>> add_workdays("2026-07-11", 0)   # Saturday rolls forward to Monday
datetime.date(2026, 7, 13)

next_workday(date, calendar=None) -> datetime.date

The first workday strictly after the date. The result is always later than date, even when date itself is a workday.

>>> from pyworkdates import next_workday
>>> next_workday("2026-07-06")  # a Monday: strictly after
datetime.date(2026, 7, 7)
>>> next_workday("2026-04-01")  # Easter holidays start the next day
datetime.date(2026, 4, 7)

previous_workday(date, calendar=None) -> datetime.date

The first workday strictly before the date. The result is always earlier than date, even when date itself is a workday.

>>> from pyworkdates import previous_workday
>>> previous_workday("2026-07-07")  # a Tuesday: strictly before
datetime.date(2026, 7, 6)
>>> previous_workday("2027-01-04")  # skips New Year's Day and the weekend
datetime.date(2026, 12, 31)

first_workday_of_month(date, calendar=None) -> datetime.date

The first workday of the date's month. Only days within that month are examined. If the month contains no workdays — possible only with a custom calendar — a ValueError is raised.

>>> from pyworkdates import first_workday_of_month
>>> first_workday_of_month("2026-05-20")  # May 1st is a Friday holiday
datetime.date(2026, 5, 4)
>>> every_day_off = [f"2026-02-{day:02d}" for day in range(1, 29)]
>>> first_workday_of_month("2026-02-10", calendar=every_day_off)
Traceback (most recent call last):
    ...
ValueError: Month 2026-02 contains no workdays with the active calendar.

last_workday_of_month(date, calendar=None) -> datetime.date

The last workday of the date's month. Only days within that month are examined, and the same "month contains no workdays" ValueError applies.

>>> from pyworkdates import last_workday_of_month
>>> last_workday_of_month("2026-05-15")  # May 31st 2026 is a Sunday
datetime.date(2026, 5, 29)

last_workday_of_previous_month(date, calendar=None) -> datetime.date

The last workday of the month before the date's month. With the built-in calendar, a January 2016 input raises ValueError because the previous month (December 2015) is outside coverage.

>>> from pyworkdates import last_workday_of_previous_month
>>> last_workday_of_previous_month("2026-01-15")  # crosses the year boundary
datetime.date(2025, 12, 31)

is_first_workday_of_month(date, calendar=None) -> bool

True if the date is its month's first workday. A date that is not itself a workday is never its month's first workday.

>>> from pyworkdates import is_first_workday_of_month
>>> is_first_workday_of_month("2026-05-04")  # the Monday after May 1st
True
>>> is_first_workday_of_month("2026-05-01")  # a holiday is not a workday
False

is_last_workday_of_month(date, calendar=None) -> bool

True if the date is its month's last workday. A date that is not itself a workday is never its month's last workday.

>>> from pyworkdates import is_last_workday_of_month
>>> is_last_workday_of_month("2026-05-29")  # the last Friday of May 2026
True
>>> is_last_workday_of_month("2026-05-31")  # a Sunday is not a workday
False

workdays_between(start, end, calendar=None) -> list[datetime.date]

All workdays d with start <= d <= end, in ascending order. Both endpoints are included when they are workdays. If start is after end, the result is an empty list.

>>> from pyworkdates import workdays_between
>>> workdays_between("2026-07-09", "2026-07-13")  # Thursday through Monday
[datetime.date(2026, 7, 9), datetime.date(2026, 7, 10), datetime.date(2026, 7, 13)]
>>> workdays_between("2026-07-10", "2026-07-06")  # start after end
[]

WorkdayCalendar(calendar=None)

A reusable calendar exposing every function above as a method. Define the calendar once and call the operations without repeating the calendar argument; each method behaves exactly like the module-level function of the same name. Calendar entries are validated at construction, so invalid entries are reported immediately; the methods then delegate to the module-level functions with that calendar. WorkdayCalendar() uses the built-in Norwegian calendar, including its coverage guard.

>>> from pyworkdates import WorkdayCalendar
>>> us = WorkdayCalendar(["2026-07-03", "2026-11-26"])
>>> us.is_workday("2026-07-03")
False
>>> us.add_workdays("2026-07-02", 1)
datetime.date(2026, 7, 6)
>>> WorkdayCalendar()
WorkdayCalendar(built-in Norwegian calendar, 2016-2050)

Development

The project uses uv with a src/ layout and no runtime dependencies.

uv sync              # install dev dependencies
uv run pytest        # tests
uv run ruff check .  # lint
uv run ruff format . # format
uv run ty check      # type check
uv build             # build sdist + wheel

License

MIT — see LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pyworkdates-0.1.0.tar.gz (11.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pyworkdates-0.1.0-py3-none-any.whl (14.0 kB view details)

Uploaded Python 3

File details

Details for the file pyworkdates-0.1.0.tar.gz.

File metadata

  • Download URL: pyworkdates-0.1.0.tar.gz
  • Upload date:
  • Size: 11.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for pyworkdates-0.1.0.tar.gz
Algorithm Hash digest
SHA256 a2de01251ffd97031fcae52768d884806de8a11a8abf44f9e6fee00b389988ac
MD5 8c4acdb11a32fbdcf0b4dfb406974302
BLAKE2b-256 f4885c4ba2adcb8051622250b7631b0cca0247de2b62caf783b379589897770c

See more details on using hashes here.

File details

Details for the file pyworkdates-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: pyworkdates-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 14.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for pyworkdates-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 3b3d45b9b283c7362690ea8ed0c90854346f95ec6420f4b36d2450ad2ba3b927
MD5 61b37646ff26ab1c7bde123cc85afcaa
BLAKE2b-256 ca381f3773c474bf17268c6e4fc02ea7f2f7188a63abf39aaff6d8a1eb49323a

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page