diff --git a/freedays/README.md b/freedays/README.md index dc0e794..4e38345 100644 --- a/freedays/README.md +++ b/freedays/README.md @@ -52,6 +52,15 @@ app launch. | **Releasing refunds only the future** | Give back a day that has not arrived and it returns to the pool. Give back today, and it does not: those hours were already unguarded. | | **Silent** | No notification, no low-budget warning, no prompt when a gate fires. You find out by asking. | +## Weekly workdays + +`freedays.WORKDAYS` (`{1, 2, 3}`, Tue/Wed/Thu in `date.weekday()` numbering), +`NON_WORKDAYS` and `is_workday(day=None)` are the one definition of the weekly +workdays every gate app reads — screen-locker's relaxed days, leetcode-guard's +cheap days, wake-alarm's ramp hold and workday stick. Import it; never write +the set out again. Dart/Kotlin code that cannot import it keeps a literal copy +guarded by a drift test against this package. + ## Free days are not sick days screen-locker's sick days stay exactly where they are, with their rolling diff --git a/freedays/freedays/__init__.py b/freedays/freedays/__init__.py index 220f764..8a94798 100644 --- a/freedays/freedays/__init__.py +++ b/freedays/freedays/__init__.py @@ -52,9 +52,12 @@ from freedays._errors import ( from freedays._model import FreeDay from freedays._paths import Paths from freedays._sync import SyncUnavailableError, sync, sync_quietly +from freedays._workdays import NON_WORKDAYS, WORKDAYS, is_workday __all__ = [ "DEFAULT_ANNUAL_BUDGET", + "NON_WORKDAYS", + "WORKDAYS", "AlreadyFreeError", "BudgetExhaustedError", "FreeDay", @@ -68,6 +71,7 @@ __all__ = [ "free_days_in_year", "is_exhausted", "is_free_day", + "is_workday", "load", "lookup", "mark", diff --git a/freedays/freedays/_workdays.py b/freedays/freedays/_workdays.py new file mode 100644 index 0000000..6aa0717 --- /dev/null +++ b/freedays/freedays/_workdays.py @@ -0,0 +1,38 @@ +# Copyright (c) 2026 Krzysztof Rudnicki +"""The weekly workdays: Tuesday, Wednesday and Thursday. + +The one definition every gate app reads, so "which days are workdays" cannot +drift between them. Consumers each give it their own meaning: screen-locker +relaxes the lock, leetcode-guard charges the cheap price, wake-alarm's ramp +holds the wake time instead of stepping it earlier, and a missed workday +morning costs tomorrow its leniency. + +Languages that cannot import this package (the Dart and Kotlin sides of the +phone apps) keep a literal copy and a test that fails when it drifts from +this one. +""" + +from __future__ import annotations + +from datetime import date +from typing import Final + +from freedays._day import today + +#: Python ``date.weekday()`` numbering: Mon=0 ... Sun=6. +WORKDAYS: Final[frozenset[int]] = frozenset({1, 2, 3}) + +#: The complement: Monday, Friday, Saturday and Sunday. +NON_WORKDAYS: Final[frozenset[int]] = frozenset(range(7)) - WORKDAYS + + +def is_workday(day: date | None = None) -> bool: + """Return whether *day* (default: today, local) is a weekly workday. + + Args: + day: The date to classify; ``None`` means today. + + Returns: + True on Tuesday, Wednesday and Thursday. + """ + return today(day).weekday() in WORKDAYS diff --git a/freedays/freedays/tests/test_workdays.py b/freedays/freedays/tests/test_workdays.py new file mode 100644 index 0000000..5346335 --- /dev/null +++ b/freedays/freedays/tests/test_workdays.py @@ -0,0 +1,39 @@ +# Copyright (c) 2026 Krzysztof Rudnicki +"""The weekly workdays: the one Tue/Wed/Thu definition every app reads.""" + +from __future__ import annotations + +from datetime import date + +import pytest + +import freedays + + +def test_workdays_are_tuesday_to_thursday() -> None: + assert freedays.WORKDAYS == frozenset({1, 2, 3}) + + +def test_non_workdays_are_the_complement() -> None: + assert freedays.NON_WORKDAYS == frozenset({0, 4, 5, 6}) + assert not freedays.WORKDAYS & freedays.NON_WORKDAYS + + +@pytest.mark.parametrize( + ("day", "expected"), + [ + (date(2026, 9, 28), False), # Monday + (date(2026, 9, 29), True), # Tuesday + (date(2026, 9, 30), True), # Wednesday + (date(2026, 10, 1), True), # Thursday + (date(2026, 10, 2), False), # Friday + (date(2026, 10, 3), False), # Saturday + (date(2026, 10, 4), False), # Sunday + ], +) +def test_is_workday(day: date, expected: bool) -> None: + assert freedays.is_workday(day) is expected + + +def test_is_workday_defaults_to_today() -> None: + assert freedays.is_workday() is (date.today().weekday() in {1, 2, 3}) diff --git a/freedays/pyproject.toml b/freedays/pyproject.toml index 2b720b3..e9707e8 100644 --- a/freedays/pyproject.toml +++ b/freedays/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "freedays" -version = "0.1.0" +version = "0.3.0" description = "Shared pre-declared free-day pool: one global list of days on which every gate app stands down" requires-python = ">=3.14" # Deliberately NOT gatelock. Its __init__ imports tkinter for the lock-window