diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..01164f62 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,8 @@ +[run] +omit = + # leading `*/` for pytest-dev/pytest-cov#456 + */.tox/* + */pep517-build-env-* + +[report] +show_missing = True diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..304196f8 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,19 @@ +root = true + +[*] +charset = utf-8 +indent_style = tab +indent_size = 4 +insert_final_newline = true +end_of_line = lf + +[*.py] +indent_style = space +max_line_length = 88 + +[*.{yml,yaml}] +indent_style = space +indent_size = 2 + +[*.rst] +indent_style = space diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000..48b2e246 --- /dev/null +++ b/.flake8 @@ -0,0 +1,9 @@ +[flake8] +max-line-length = 88 + +# jaraco/skeleton#34 +max-complexity = 10 + +extend-ignore = + # Black creates whitespace before colon + E203 diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..89ff3396 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 +updates: + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "daily" + allow: + - dependency-type: "all" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 89a92b5e..00000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,86 +0,0 @@ -name: CI - -on: - push: - branches: - - '*' - tags: - - '*' - # Empty pull request argument means "all pull-requests" - pull_request: - -jobs: - # 1. linters - check-lint: - strategy: - matrix: - include: - - name: flake8 - tox-env: flake8 - - name: pyupgrade - tox-env: pyupgrade - - name: Check ${{ matrix.name }} - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: 3.x - - name: Install dependencies - run: | - set -xeu - python --version - pip install tox - - name: Check ${{ matrix.name }} - run: tox -e ${{ matrix.tox-env }} - - # 2. Unit tests - tests: - strategy: - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - python-version: [3.6, 3.7, 3.8, 3.9] - include: - - python-version: 3.6 - tox-env: py36 - - python-version: 3.7 - tox-env: py37 - - python-version: 3.8 - tox-env: py38 - - python-version: 3.9 - tox-env: py39 - - name: Test (python ${{ matrix.python-version }}/${{ matrix.os }}) - runs-on: ${{ matrix.os }} - - steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - name: Cache pip - uses: actions/cache@v2 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('setup.cfg') }} - restore-keys: | - ${{ runner.os }}-pip- - - name: Install dependencies - run: | - python --version - pip install tox coverage - - name: Run tox targets for ${{ matrix.python-version }} - run: tox -e ${{ matrix.tox-env }} - - report-status: - name: success - runs-on: ubuntu-latest - needs: - - check-lint - - tests - steps: - - name: Report success - run: echo 'Success !' diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000..63fa1e8e --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,63 @@ +name: tests + +on: [push, pull_request] + +jobs: + test: + strategy: + matrix: + python: + - 3.7 + - '3.10' + - '3.11' + platform: + - ubuntu-latest + - macos-latest + - windows-latest + runs-on: ${{ matrix.platform }} + steps: + - uses: actions/checkout@v3 + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python }}-dev + - name: Install tox + run: | + python -m pip install tox + - name: Run tests + run: tox + + check: # This job does nothing and is only used for the branch protection + if: always() + + needs: + - test + + runs-on: ubuntu-latest + + steps: + - name: Decide whether the needed jobs succeeded or failed + uses: re-actors/alls-green@release/v1 + with: + jobs: ${{ toJSON(needs) }} + + release: + needs: + - check + if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: "3.11-dev" + - name: Install tox + run: | + python -m pip install tox + - name: Release + run: tox -e release + env: + TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 4656a718..00000000 --- a/.gitignore +++ /dev/null @@ -1,13 +0,0 @@ -*.pyc -*.egg-info -dist/ -build/ -.coverage -.tox/ -Pipfile -Pipfile.lock -pyproject.toml -# py.test cache -.cache/ -.pytest-cache/ -.pytest_cache/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..af502010 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,5 @@ +repos: +- repo: https://github.com/psf/black + rev: 22.6.0 + hooks: + - id: black diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 00000000..cc698548 --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,6 @@ +version: 2 +python: + install: + - path: . + extra_requirements: + - docs diff --git a/Changelog.md b/CHANGES.rst similarity index 53% rename from Changelog.md rename to CHANGES.rst index 5c0d2159..e0848a37 100644 --- a/Changelog.md +++ b/CHANGES.rst @@ -1,38 +1,40 @@ -# CHANGELOG +v7.5.0 +------ -## v16.3.0 (2022-02-22) +Incorporate changes from workalendar v16.3.0 (2022-02-22) -### New calendars +Calendars - New calendar: Added Georgia (country) calendar by @atj01 (#687). - New calendar: Added Kazakhstan calendar by @atj01 (#688). - New calendar: Added USA Federal Reserve System calendar by @ludsoft (#695) -### Bugfixes and other changes +Bugfixes and other changes - Removed duplicate Proclamation Day for Latvia, by @Daglina (#686). - Documentation: Fix the ``keep_datetime`` usage example in the "basic" doc (#690). - Added conditional holidays on 26th December and 2nd January in Neuchatel (Switzerland) (#697). - Added Federal Thanksgiving Monday and two conditional holidays on 26th December and 2nd January in Neuchatel (Switzerland) (#697). -## v16.2.0 (2021-12-10) +Incorporate changes from workalendar v16.2.0 (2021-12-10) - Update China's public holidays for 2022, thx to @iamsk (#693). -## v16.1.0 (2021-10-01) +Incorporate changes from workalendar v16.1.0 (2021-10-01) - Fixed United Kingdom's 2022 holidays ; Spring Bank Holiday has been moved to the 3rd of June and Queen's Platinum Jubilee added to 2nd of June. - New calendar: Added Guernsey calendar by @ludsoft (#681) -## v16.0.0 (2021-09-16) +v7.4.0 +------ -### New calendar +Incorporate changes from workalendar v16.0.0 (2021-09-16) -- New calendar: Added Philippines calendar by @micodls (#396) +Calendars -### Internal changes +- New calendar: Added Philippines calendar by @micodls (#396) -**See the README for more details about requirements, you may have to make changes to your installed packages in your environment.** +Internal - Remove `skyfield` dependency, added `[astronomy]` as extra dependency (#660). - Replace `pyCalverter` with `convertdate` (#536). @@ -42,62 +44,66 @@ - Added the "Workalendar maintainers" in the LICENSE file. - Changed the maintainer email. -## v15.4.0 (2021-07-12) +v7.3.0 +------ + +Incorporate changes from workalendar v15.4.0 (2021-07-12) - New calendar: Added Nigeria calendar by @taiyeoguns (#656) - Fix: Chilean calendar floating dates, add Indigenous Peoples Day using solar term, thx @ajcobo. -## v15.3.1 (2021-07-02) +Incorporate changes from workalendar v15.3.1 (2021-07-02) -### Bugfixes +Bugfixes - Updated Japan calendar because of the Olympics, thx @lxlgarnett. (#662) - Fixed Japan "Sports Day" label depending on the year. -### Documentation +Documentation - The Workalendar project has been moved from Peopledoc's organization to its own (#651, #653, thx to @ewjoachim). -## v15.3.0 (2021-05-07) +Incorporate changes from workalendar v15.3.0 (2021-05-07) - Fix Barbados calendar to add 2 non computable public holiday and fix boxing day computation, thx to @ludsoft (#647). -## v15.2.0 (2021-04-23) +Incorporate changes from workalendar v15.2.0 (2021-04-23) - Fixed Cuiaba City calendar (Brazil), adding Easter Sunday, Corpus Christi and Good Friday, thx @leogregianin (#642). - Fix Catalonian calendar: add missing St John the Baptist public holiday, thx @lozlow (#643). -## v15.1.0 (2021-03-12) +Incorporate changes from workalendar v15.1.0 (2021-03-12) - Bugfix: Bulgaria holidays are now computed using the Orthodox calendar, include shifting rules for holidays that fall on a week-end (#596). - Bugfix: `get_working_days_delta` method to integrate the `extra_holidays` and `extra_working_days` args (#631). -## v15.0.2 (2021-03-05) +Incorporate changes from workalendar v15.0.2 (2021-03-05) - Bugfix: USA calendar would take the `shift_exceptions` into account, even if the exceptions are set in the next or previous year (#610). - Requirements: Unpin `pyupgrade` library (#634). -## v15.0.1 (2021-02-26) +Incorporate changes from workalendar v15.0.1 (2021-02-26) - Hotfix: Taiwan exceptional working day on February, 20th 2021 (#628). - Hotfix: September 11th is a working day in Taiwan (#628). -## v15.0.0 (2021-02-19) +v7.2.0 +------ -### Major changes +Incorporate changes from workalendar v15.0.0 (2021-02-19) + +Major changes - API: New method available in `core` module: `Calendar.get_iso_week_date()` to find the weekday X of the week number Y (#619). - Requirements: Replace pytz with `(backports.)zoneinfo`, thx to @eumiro (#614) - Doc: Documented the different (in)compatibilities due to the use of `zoneinfo` (#614). -### Minor changes - -#### Bugfixes +Bugfixes - Small fixes in Netherlands School calendars (#619). - Temporary downgrade of `pyupgrade` to fix the `pyup_dirs`. -#### Improving test coverage +Improving test coverage - Improve Netherlands coverage (#546, #619). - Improve Russia coverage (#546). @@ -105,19 +111,22 @@ - Improve the `astronomy.py` module coverage (#546). - Improve coverage for the `tests/__init__.py` module (#546). *Note:* system-dependant test branch (if Windows) won't be counted for coverage. -## v14.3.0 (2021-01-15) +v7.1.0 +------ -### Calendar change +Incorporate changes from workalendar v14.3.0 (2021-01-15) + +Calendars - Update Malaysia 2022-2024 (Deepavali + Thaipusam) by @jack-pace -## v14.2.0 (2021-01-08) +Incorporate changes from workalendar v14.2.0 (2021-01-08) -### Calendar changes +Calendars - Update Singapore for range from 2022 to 2030 (Deepavali), by @hprobotic -### Internal changes +Internal - Replace `os.path.*` calls with `pathlib.Path`, thx to @eumiro (#603) - Use f-string for string formatting, thx to @eumiro (#605) @@ -126,71 +135,82 @@ - Adding Mac OS & Windows tests to the test matrix (related to #607). - Fix tests when running them on Windows (#607). -## v14.1.0 (2020-12-10) +v7.0.0 +------ + +New feature + +- Enhanced support for multi-day "series" holidays such as Chinese Spring + Festival and Islamic Eid. Previously, if one day of the series was shifted + as per the observance shift rules, it would "merge" into the next day of the + series, effectively shortening the observed series. Now, all the following + days of the series are shifted, maintaining its duration. + +Incorporate changes from workalendar v14.1.0 (2020-12-10) - Fix Russia 2021 holidays, thx @MichalChromcak for the bug report (#578). -## v14.0.0 (2020-11-27) +Incorporate changes from workalendar v14.0.0 (2020-11-27) -### Fixes +- Fixes -- Fix Russia calendar: non-working days are shifted to the next MON when they happen on the week-end (#589). -- Fix Russia New year holidays. It has become a week off since 2005 (related to #578). -- Added Russia COVID-19 non-working days for the year 2020 ; these days are not shifted to next MON (#578). -- Fixed Russia Christmas day ; December 25th is not a public holiday. Fixed several other Orthodox calendars (#530). -- Update China's public holidays for 2021, thanks @iamsk. + - Fix Russia calendar: non-working days are shifted to the next MON when they happen on the week-end (#589). + - Fix Russia New year holidays. It has become a week off since 2005 (related to #578). + - Added Russia COVID-19 non-working days for the year 2020 ; these days are not shifted to next MON (#578). + - Fixed Russia Christmas day ; December 25th is not a public holiday. Fixed several other Orthodox calendars (#530). + - Update China's public holidays for 2021, thanks @iamsk. -### Minor changes +- Minor changes -- Added a `daterange` function in `workalendar.core` module to iterate between two dates. + - Added a `daterange` function in `workalendar.core` module to iterate between two dates. -## v13.0.0 (2020-11-13) +Incorporate changes from workalendar v13.0.0 (2020-11-13) -### Calendars +- Calendars -- Add optional school holidays to Netherlands calendar, by @Flix6x (#556). -- Add optional carnival to Netherlands calendar. + - Add optional school holidays to Netherlands calendar, by @Flix6x (#556). + - Add optional carnival to Netherlands calendar. -### Documentation +- Documentation -- Moving the `contributing.md` file to the `docs/` directory (#573). -- Changed from `setup.py` to a nice `setup.cfg` file, thanks @ewjoachim (#576). -- Added documentation about class options (#572). -- Converted `README.rst` file into `README.md` (#575). -- Fixed Pull Request template to reference `setup.cfg` (#587). + - Moving the `contributing.md` file to the `docs/` directory (#573). + - Changed from `setup.py` to a nice `setup.cfg` file, thanks @ewjoachim (#576). + - Added documentation about class options (#572). + - Converted `README.rst` file into `README.md` (#575). + - Fixed Pull Request template to reference `setup.cfg` (#587). -### Other changes +- Other changes -- Switched from Travis CI to Github Actions for CI jobs, thanks to @mgu. -- Added support of Python 3.9 (#557). -- Changed from `setup.py` to a nice `setup.cfg` file, thanks @ewjoachim (#576). -- Use the `setup.cfg` file in the key to cache in `ci.yml` file (#587). -- [OBSOLETE] Switched from bionic to focal on Travis CI (we've switched to GH actions after that). + - Switched from Travis CI to Github Actions for CI jobs, thanks to @mgu. + - Added support of Python 3.9 (#557). + - Changed from `setup.py` to a nice `setup.cfg` file, thanks @ewjoachim (#576). + - Use the `setup.cfg` file in the key to cache in `ci.yml` file (#587). + - [OBSOLETE] Switched from bionic to focal on Travis CI (we've switched to GH actions after that). -## v12.1.0 (2020-10-16) +Incorporate changes from workalendar v12.1.0 (2020-10-16) -### New calendars +- New calendars -- Added Spain regions: Andalusia, Aragon, Castile and León, Castilla-La Mancha, Canary Islands, Extremadura, Galicia, Balearic Islands, La Rioja, Community of Madrid, Murcia, Navarre, Asturias, Basque Country, Cantabria, Valencian Community (#531). -- Added all ISO codes for Spain regions - thx @ainarela for your help on this (#531). + - Added Spain regions: Andalusia, Aragon, Castile and León, Castilla-La Mancha, Canary Islands, Extremadura, Galicia, Balearic Islands, La Rioja, Community of Madrid, Murcia, Navarre, Asturias, Basque Country, Cantabria, Valencian Community (#531). + - Added all ISO codes for Spain regions - thx @ainarela for your help on this (#531). -### Other changes +- Other changes -- Refactored Spain test modules (#531). -- Fix Catalonia calendar by removing *Sant Juan* day, which does not appear to be an official holiday (#531). -- Improve coverage of `workalendar/core.py` module (#546). -- Improve coverage for the Netherlands calendar - Queen's Day (#546). -- Improve coverage for the Romania calendar - Liberation day (#546). -- Improve coverage for the New Zealand calendar (#546). -- Added a tox entrypoint to ensure code is Python 3.6+, using ``pyupgrade`` (#566). -- Added the pyupgrade tox job to the test suite, amended contributing documentation (#566). + - Refactored Spain test modules (#531). + - Fix Catalonia calendar by removing *Sant Juan* day, which does not appear to be an official holiday (#531). + - Improve coverage of `workalendar/core.py` module (#546). + - Improve coverage for the Netherlands calendar - Queen's Day (#546). + - Improve coverage for the Romania calendar - Liberation day (#546). + - Improve coverage for the New Zealand calendar (#546). + - Added a tox entrypoint to ensure code is Python 3.6+, using ``pyupgrade`` (#566). + - Added the pyupgrade tox job to the test suite, amended contributing documentation (#566). -## v12.0.0 (2020-10-02) +Incorporate changes from workalendar v12.0.0 (2020-10-02) - **Deprecation:** Dropped support for Python 3.5. As of this version, workalendar now requires Python 3.6+ (#330). - Improve coverage of Singapore calendar (#546). -## v11.0.1 (2020-09-11) +Incorporate changes from workalendar v11.0.1 (2020-09-11) - Add ISO code decorator to Catalonia calendar, thanks to @jbagot (#551). - Improve coverage of South Africa calendar (#546). @@ -198,402 +218,400 @@ - Improve coverage of Canada (Nunavut) calendar (#546). - Improve coverage of Israel calendar (#546). -## v11.0.0 (2020-09-04) +Incorporate changes from workalendar v11.0.0 (2020-09-04) -### New calendar +- New calendar -- Added Mozambique calendar by @mr-shovel (#542). + - Added Mozambique calendar by @mr-shovel (#542). -### New feature +- New feature -- Added iCal export feature, initiated by @joooeey (#197). -- Fix PRODID pattern for iCal exports: `"PRODID:-//workalendar//ical {__version__}//EN"`, using current workalendar version (#543). + - Added iCal export feature, initiated by @joooeey (#197). + - Fix PRODID pattern for iCal exports: `"PRODID:-//workalendar//ical {__version__}//EN"`, using current workalendar version (#543). -## v10.4.0 (2020-08-28) +Incorporate changes from workalendar v10.4.0 (2020-08-28) -### New calendar +- New calendar -- Added Monaco calendar by @joaopbnogueira (#538). + - Added Monaco calendar by @joaopbnogueira (#538). -### Major changes and bugfixes +- Major changes and bugfixes -- Migrating Labour Day as a worldwide holiday, disabled by default, but activated (to date) for about 50 countries (including label change when necessary), `contributing.md` documentation amended (#467). -- Bugfix: Avoid Cesar Chavez Day duplicated shifts by refactoring the California shift rules (#528). + - Migrating Labour Day as a worldwide holiday, disabled by default, but activated (to date) for about 50 countries (including label change when necessary), `contributing.md` documentation amended (#467). + - Bugfix: Avoid Cesar Chavez Day duplicated shifts by refactoring the California shift rules (#528). -### Other changes +- Other changes -- Small refactoring for the Colombia / added docstrings & comments to explain why we're not using stock options. Added tests for year 2020 and handling shift exceptions (#509). -- Tech: Replace occurrences of `assertEquals` with `assertEqual` to clear warnings (#533). -- Use `include_immaculate_conception` flag for Portugal, Brazil, Argentina, Paraguay calendars (#529). + - Small refactoring for the Colombia / added docstrings & comments to explain why we're not using stock options. Added tests for year 2020 and handling shift exceptions (#509). + - Tech: Replace occurrences of `assertEquals` with `assertEqual` to clear warnings (#533). + - Use `include_immaculate_conception` flag for Portugal, Brazil, Argentina, Paraguay calendars (#529). -## v10.3.0 (2020-07-10) +Incorporate changes from workalendar v10.3.0 (2020-07-10) -### Bugfixes +- Bugfixes -- Belarus: removing day after Radonitsa, which is apparently not a holiday. -- Algeria: assigning the week-end days as FRI+SAT, as it's following a Islamic calendar. + - Belarus: removing day after Radonitsa, which is apparently not a holiday. + - Algeria: assigning the week-end days as FRI+SAT, as it's following a Islamic calendar. -### Other changes +- Other changes -- Refactoring the core ``Calendar`` classes / mixins for better understanding. Only one ``Calendar`` subclass should be imported / used in calendar classes, the rest (when possible) should be ``Mixins`` (related to #511). -- Declaring the New year's Day as a worldwide holiday, with only two exceptions (to date): Israel & Qatar (#511). -- Fixed `contributing.md` documentation with the new class/mixin organization (#511). + - Refactoring the core ``Calendar`` classes / mixins for better understanding. Only one ``Calendar`` subclass should be imported / used in calendar classes, the rest (when possible) should be ``Mixins`` (related to #511). + - Declaring the New year's Day as a worldwide holiday, with only two exceptions (to date): Israel & Qatar (#511). + - Fixed `contributing.md` documentation with the new class/mixin organization (#511). -## v10.2.0 (2020-06-26) +Incorporate changes from workalendar v10.2.0 (2020-06-26) - Bugfix: setting *Consciência Negra day* as a non-holiday by default for Brazilian calendars, thx to @edniemeyer (#516). - Bugfix: Introducing the changes in Croatia holidays as of 2020 - Remembrance Day, Independence Day, Statehood Day... thx to @davidpodrebarac for the bug report (#515). -## v10.1.0 (2020-06-18) +Incorporate changes from workalendar v10.1.0 (2020-06-18) -### Calendar fix +- Calendar fix -- Adding All Souls' Day to Lithuania calendar, starting of 2020, thx to @norkunas (#512). + - Adding All Souls' Day to Lithuania calendar, starting of 2020, thx to @norkunas (#512). -### Minor changes +- Minor changes -- Small fixes (docstrings, use of extends, etc) on Cayman Islands calendar (#507). -- Moving Carnaval / Mardi Gras / Fat Tuesday calculation into the `workalendar.core` module, because it's used in at least 3 countries and some States / Counties in the USA. + - Small fixes (docstrings, use of extends, etc) on Cayman Islands calendar (#507). + - Moving Carnaval / Mardi Gras / Fat Tuesday calculation into the `workalendar.core` module, because it's used in at least 3 countries and some States / Counties in the USA. -## v10.0.0 (2020-06-05) +Incorporate changes from workalendar v10.0.0 (2020-06-05) - **BREAKING CHANGE**: the ``IsoRegistry.get_calendar_class()`` method has been removed from the code and should no longer be used (#375, #495). -## v9.2.0 (2020-06-02) +Incorporate changes from workalendar v9.2.0 (2020-06-02) -### New Calendars +- New Calendars - Added rules for all Switzerland Cantons, branching off the initial work by @brutasse (#497). -## v9.0.1 (2020-05-22) +Incorporate changes from workalendar v9.0.1 (2020-05-22) - Making the Israel calendar more efficient (#498). - Fixing duplicated holidays in Hong-Kong and Hong-Kong Bank holiday calendars (#496). - Integrating Hong-Kong holidays for 2021 (#496). -## v9.0.0 (2020-04-24) +Incorporate changes from workalendar v9.0.0 (2020-04-24) - **BREAKING CHANGE**: the ``IsoRegistry.items()`` method has been removed from the API. You must use the ``get_calendars()`` to perform the same registry queries (#375, #491). - *Deprecation notice*: The usage of ``IsoRegistry.get_calendar_class()`` is strongly discouraged, in favor of ``get()``. The ``get_calendar_class`` method will be dropped in a further release. In the meantime, they'll be both equivalent (#375, #418). -## v8.4.0 (2020-04-17) +Incorporate changes from workalendar v8.4.0 (2020-04-17) -### New Calendar +- New Calendar -- Added Kenyan calendar, by @KidkArolis (#484) + - Added Kenyan calendar, by @KidkArolis (#484) -### Minor fixes +- Minor fixes -- Fixed Lithuania calendar to use the core flags for Assumption and All Saints (#468). -- Fixed Malta calendar ; January 1st was already included, no need to add it to the ``FIXED_HOLIDAYS`` property (#469). -- Small refactor in Netherlands calendar to use core constants (#470). + - Fixed Lithuania calendar to use the core flags for Assumption and All Saints (#468). + - Fixed Malta calendar ; January 1st was already included, no need to add it to the ``FIXED_HOLIDAYS`` property (#469). + - Small refactor in Netherlands calendar to use core constants (#470). -## v8.3.0 (2020-04-14) +Incorporate changes from workalendar v8.3.0 (2020-04-14) - Fixing Hong-Kong calendar, where SAT are common working days (#477). - Introducing Hong-Kong Bank calendar. For banks, Saturdays are non-working days (#477). -## v8.2.2 (2020-04-10) +Incorporate changes from workalendar v8.2.2 (2020-04-10) - Fixed Argentina's "Malvinas Day" date for 2020, shifted to March 31st because of the coronavirus crisis (#476). - Fixed Argentina's label for "Malvinas Day" and "Día de la Memoria" (#476). -## v8.2.1 (2020-04-03) +Incorporate changes from workalendar v8.2.1 (2020-04-03) - Added BrazilBankCalendar to support `include_` flags and make it possible to extend and change these flags to support custom bank calendars (#474). -## v8.2.0 (2020-03-13) +Incorporate changes from workalendar v8.2.0 (2020-03-13) - Added Belarus calendar, by @alexdoesstuff (#472). -## v8.1.0 (2020-02-07) +Incorporate changes from workalendar v8.1.0 (2020-02-07) - Added Israel holidays eves and removed holidays which are not affecting the working days in Israel (#461). - Fix warning in China's holidays to dynamically read supported years, thx @fredrike (#459). -## v8.0.2 (2020-01-24) +Incorporate changes from workalendar v8.0.2 (2020-01-24) - Fix several miscalculations in Georgia (USA) calendar (#451). -## v8.0.1 (2020-01-24) +Incorporate changes from workalendar v8.0.1 (2020-01-24) - Fix Family Day for British Columbia (Canada) which was switched from 2nd to 3rd Monday of February in 2019 - thx @jbroudou for the bug report (#454). -## v8.0.0 (2020-01-10) +v6.1.2 +------ + +#14: Replaced implicit dependency on setuptools with explicit +dependency on importlib.metadata. + +v6.1.1 +------ + +Fix version inference when installed from sdist. + +v6.1.0 +------ + +Incorporate changes from workalendar v8.0.0 (2020-01-10) - **BREAKING CHANGE** Drop Support for Python 2 - EOL January 1st 2020 (#442). - Added Ukraine calendar, by @apelloni. - Small cleanup in the ``.travis.yml`` file, thx to @Natim. -### ISO Registry API Change - - Changes in the ``registry.items()`` method API. - This method is aliased to ``get_calendars()``. In a near release, the ``items()`` method will change its purpose. - The ``get_calendars()`` method accepts an empty/missing ``region_codes`` argument to retrieve the full registry. Please see the [ISO Registry documentation](https://workalendar.github.io/workalendar/iso-registry.html) for extensive usage docs (#403, #375). -## v7.2.0 (2019-12-06) +Incorporate changes from workalendar v7.2.0 (2019-12-06) -### New calendars +New calendars - Added Serbia calendar, by @apelloni (#435). - Added Argentina calendar, by @ftatarli (#419). -### Other changes +Other changes - Update China's public holidays for 2020, thx @nut-free (#429). - Update Malaysia and Singapore for 2021 (Deepavali + Thaipusam) by @jack-pace (#432). - Small refactorings on the Gevena (Switzerland) holiday class, thx to @cw-intellineers (#420). -## v7.1.1 (2019-11-22) +Incorporate changes from workalendar v7.1.1 (2019-11-22) - **Bugfix** for USA: Fixed incorrect implementation for Thanksgiving Friday, thx @deveshvar (#422). - Fix Advanced usage documentation about Thanksgiving Day (#426). - Added Geneva calendar by @cw-intellineers (#420). -## v7.1.0 (2019-11-15) +Incorporate changes from workalendar v7.1.0 (2019-11-15) -### New calendars +New calendars - Added 27 Brazil calendars -- thanks a lot to @luismalta & @mileo, (#409 & #415) -### Enhancements +Enhancements - Added compatibility with Python 3.8 (#406). - Added an IBGE_REGISTER to reference IBGE (brazilian) calendars with related tests (#415). - Improve ISO registry interface by raising an error when trying to register a non-Calendar class (#412). -### Other changes +Other changes - Fixes and additions to some Brazil calendars ; again, thanks to @luismalta & @mileo, (#409 & #415) - Fix Denmark, re-add Christmas Eve, which is widely treated as public holiday ; thx to @KidkArolis, (#414). - Increase Malaysia coverage by adding tests for missing Deepavali & Thaipusam. - Increase China coverage by adding tests for special extra-holidays & extra-working days cases. +v6.0.0 +------ -## v7.0.0 (2019-09-20) +Require Python 3.6 or later. + +v5.0.0 +------ + +#11: Add support for ``__add__`` and ``__sub__`` for +``Holiday`` instances on Python 3.8 and later. Now adding +a timedelta to a ``Holiday`` returns another ``Holiday``. + +Incorporate changes from workalendar v7.0.0 (2019-09-20) - Drop `ephem` astronomical calculation library, in favor of `skyfield` and `skyfield-data` for providing minimal data files to enable computation (#302, #348). Many thanks to @GammaSagittarii for the tremendous help on finding the right way to compute Chinese Solar Terms. Also thanks to @antvig and @DainDwarf for testing the beta version (#398). -## v6.0.1 (2019-09-17) +Incorporate changes from workalendar v6.0.1 (2019-09-17) - Fix Turkey Republic Day (#399, thx to @mhmtozc & @Natim). -## v6.0.0 (2019-08-02) - -**Deprecation Notice:** *The global ISO registry now returns plain `dict` objects from its various methods.* +Incorporate changes from workalendar v6.0.0 (2019-08-02) +- **Deprecation Notice:** *The global ISO registry now returns plain `dict` objects from its various methods.* - Global registry now returns plain built-in dicts (#375). - Removed `EphemMixin` in favor of astronomical functions (#302). - Added first day counting when computing working_days delta (#393), thx @Querdos. -## v5.2.3 (2019-07-11) +Incorporate changes from workalendar v5.2.3 (2019-07-11) +- Fix Romania, make sure Easter and related holidays are calculated using the Orthodox calendar, thx to @KidkArolis (#389). -**Deprecation Warning:** *Currently the registry returns `OrderedDict` objects when you're querying for regions or subregions. Expect that the next major release will preferrably return plain'ol' `dict` objects. If your scripts rely on the order of the objects returned, you'll have to sort them yourself.* -- Fix Romania, make sure Easter and related holidays are calculated using the Orthodox calendar, thx to @KidkArolis (#389). +v4.0.0 +------ -## v5.2.2. (2019-07-07) +Incorporate changes from workalendar v5.2.2. (2019-07-07) -- Fix Denmark, remove observances (remove Palm Sunday, Constitution Day, Christmas Eve and New Year's Eve), thx to @KidkArolis (#387, #386). +- **Deprecation Warning:** *Currently the registry returns `OrderedDict` objects when you're querying for regions or subregions. Expect that the next major release will preferrably return plain'ol' `dict` objects. If your scripts rely on the order of the objects returned, you'll have to sort them yourself.* +- Fix Denmark, remove observances (remove Palm Sunday, Constitution Day, Christmas Eve and New Year's Eve) (#387, #386) -## v5.2.1 (2019-07-05) +Incorporate changes from workalendar v5.2.1 (2019-07-05) - Refactored the package building procedure, now linked to `make package` ; added a note about this target in the PR template (#366). - Fixed United Kingom's 2020 holidays ; The Early May Bank Holiday has been moved to May 8th to commemorate the 75th anniversary of the end of WWII (#381). -## v5.2.0 (2019-07-04) +Incorporate changes from workalendar v5.2.0 (2019-07-04) -### New Calendar +- New Calendar -- Added JapanBank by @raybuhr (#379, #369). + - Added JapanBank by @raybuhr (#379, #369). -### Other changes +- Other changes -- Added adjustments to 2019-2020 Japan calendar due to the coronation of a new emperor (#379). -- Add a note about the fact that contributors should not change the version number in the changelog and/or the ``setup.py`` file (#380). + - Added adjustments to 2019-2020 Japan calendar due to the coronation of a new emperor (#379). + - Add a note about the fact that contributors should not change the version number in the changelog and/or the ``setup.py`` file (#380). -## v5.1.1 (2019-06-27) +Incorporate changes from workalendar v5.1.1 (2019-06-27) - Display missing lines in coverage report (#376). - Add "Europe Day" for Luxembourg (#377). -## v5.1.0 (2019-06-24) +Incorporate changes from workalendar v5.1.0 (2019-06-24) -### New Calendar +- New Calendar -- Added Turkey by @tayyipgoren (#371). + - Added Turkey by @tayyipgoren (#371). -### Other changes +- Other changes -- Change registry mechanism to avoid circular imports (#288). -- Internal: Added a "Release" section to the Pull Request template. -- Internal: Added advices on the Changelog entry in the Contributing document. -- Bugfix: Fixing North Carolina shift rules when Christmas Day happens on Saturday (#232). -- Documentation: rearrange country list in ``README.rst`` (sorting and fixing nested lists). -- Documentation: Renamed and changed format of the "Contributing guidelines" document, now in Markdown (GFM variant), with a few fixes (#368). -- Internal: remove coverage targets ; now coverage reports are displayed for each tox job, but they won't output classes with 100% coverage. + - Change registry mechanism to avoid circular imports (#288). + - Internal: Added a "Release" section to the Pull Request template. + - Internal: Added advices on the Changelog entry in the Contributing document. + - Bugfix: Fixing North Carolina shift rules when Christmas Day happens on Saturday (#232). + - Documentation: rearrange country list in ``README.rst`` (sorting and fixing nested lists). + - Documentation: Renamed and changed format of the "Contributing guidelines" document, now in Markdown (GFM variant), with a few fixes (#368). + - Internal: remove coverage targets ; now coverage reports are displayed for each tox job, but they won't output classes with 100% coverage. -## v5.0.3 (2019-06-07) +Incorporate changes from workalendar v5.0.3 (2019-06-07) - Bugfix: Panama - Fixed incorrect independence from Spain date, thanks to @chopanpma (#361). -## v5.0.2 (2019-06-03) +Incorporate changes from workalendar v5.0.2 (2019-06-03) - Bugfix: Israel - Fixed incorrect Purim/Shushan Purim dates in jewish leap years, thx @orzarchi. This fix cancels the last (5.0.1) version, that will be deleted from PyPI. -## v5.0.1 (2019-06-03) +Incorporate changes from workalendar v5.0.1 (2019-06-03) -**WARNING** This version contains known bugs on Israel calendar. Please do not use it in production. +- **WARNING** This version contains known bugs on Israel calendar. Please do not use it in production. - Bugfix: Israel - Fixed incorrect Purim/Shushan Purim dates in jewish leap years, thx @orzarchi. -## v5.0.0 (2019-05-24) - -### Major Changes & fixes +Incorporate changes from workalendar v5.0.0 (2019-05-24) -- Dropped Python 3.4 support (#352). -- Added Malaysia Thaipusam days for the year 2019 & 2020 - thx @burlak for the bug report (#354). -- Fixed Deepavali dates for the year 2018 ; confirmed fixed dates that were set in the past. +- Major Changes & fixes -### Added calendars + - Dropped Python 3.4 support (#352). + - Added Malaysia Thaipusam days for the year 2019 & 2020 - thx @burlak for the bug report (#354). + - Fixed Deepavali dates for the year 2018 ; confirmed fixed dates that were set in the past. -- Added Florida specific calendars: Florida Legal, Florida Circuit Courts, Miami-Dade (#216). +- Added calendars + - Added Florida specific calendars: Florida Legal, Florida Circuit Courts, Miami-Dade (#216). -## v4.4.0 (2019-05-17) +Incorporate changes from workalendar v4.4.0 (2019-05-17) -**WARNING**: This release will be the last one to support Python 3.4, which has [reached its End of Life and has been retired](https://www.python.org/dev/peps/pep-0429/#release-schedule). Please upgrade. +- **WARNING**: This release will be the last one to support Python 3.4, which has [reached its End of Life and has been retired](https://www.python.org/dev/peps/pep-0429/#release-schedule). Please upgrade. -### Added calendar +- Added calendar -- Added California specific calendars: California Education, Berkeley, San Francisco, West Hollywood (#215). + - Added California specific calendars: California Education, Berkeley, San Francisco, West Hollywood (#215). -### Fixes +- Fixes -- Added a few refactors and tests for Australia Capital Territory holiday named "Family & Community Day", that lasted from 2007 to 2017 (#25). -- Added South African 2019 National Elections as holiday (#350), by @RichardOB. + - Added a few refactors and tests for Australia Capital Territory holiday named "Family & Community Day", that lasted from 2007 to 2017 (#25). + - Added South African 2019 National Elections as holiday (#350), by @RichardOB. -## v4.3.1 (2019-05-03) +Incorporate changes from workalendar v4.3.1 (2019-05-03) - Bugfix: Update 2019 Labour Day Holidays for China as changed by government recently (2019-03-22), by @iamsk, and thanks to @ltyely for their patch (#345 & #347). -## v4.3.0 (2019-03-15) +Incorporate changes from workalendar v4.3.0 (2019-03-15) + +- New Calendar -### New Calendar + - Added Barbados by @ludsoft. -- Added Barbados by @ludsoft. +- Fixes -### Fixes + - Added isolated tests for shifting mechanics in USA calendars - previously untested (#335). + - Added Berlin specific holidays (#340). + - Added several one-off public holidays to UK calendar (#336). -- Added isolated tests for shifting mechanics in USA calendars - previously untested (#335). -- Added Berlin specific holidays (#340). -- Added several one-off public holidays to UK calendar (#336). +Incorporate changes from workalendar v4.2.0 (2019-02-21) -## v4.2.0 (2019-02-21) +- New calendars -### New calendars + - Added several US territories and other specific calendars: -- Added several US territories and other specific calendars: - - American Samoa territory (#218). - - Chicago, Illinois (#220). - - Guam territory (#219). - - Suffolk County, Massachusetts (#222). -- Added Cayman Islands, British Overseas Territory (#328) + - American Samoa territory (#218). + - Chicago, Illinois (#220). + - Guam territory (#219). + - Suffolk County, Massachusetts (#222). -## v4.1.0 (2019-02-07) + - Added Cayman Islands, British Overseas Territory (#328) -### New calendars +Incorporate changes from workalendar v4.1.0 (2019-02-07) -**WARNING** Scotland (sub)calendars are highly experimental and because of their very puzzling rules, may be false. Please use them with care. +- New calendars -- Added Scotland calendars, i.e. Scotland, Aberdeen, Angus, Arbroath, Ayr, Carnoustie & Monifieth, Clydebank, Dumfries & Galloway, Dundee, East Dunbartonshire, Edinburgh, Elgin, Falkirk, Fife, Galashiels, Glasgow, Hawick, Inverclyde, Inverness, Kilmarnock, Lochaber, Monifieth, North Lanarkshire, Paisley, Perth, Scottish Borders, South Lanarkshire, Stirling, and West Dunbartonshire (#31). +- **WARNING** Scotland (sub)calendars are highly experimental and because of their very puzzling rules, may be false. Please use them with care. -### Bugfixes + - Added Scotland calendars, i.e. Scotland, Aberdeen, Angus, Arbroath, Ayr, Carnoustie & Monifieth, Clydebank, Dumfries & Galloway, Dundee, East Dunbartonshire, Edinburgh, Elgin, Falkirk, Fife, Galashiels, Glasgow, Hawick, Inverclyde, Inverness, Kilmarnock, Lochaber, Monifieth, North Lanarkshire, Paisley, Perth, Scottish Borders, South Lanarkshire, Stirling, and West Dunbartonshire (#31). -- Fixed United Kingdom bank holiday for 2002 and 2012, thx @ludsoft (#315). -- Fix a small flake8 issue with wrong indentation (#319). -- Fix Russia "Day of Unity" date, set to November 4th, thx @alexitkes for the bug report (#317). +- Bugfixes -## v4.0.0 (2019-01-24) + - Fixed United Kingdom bank holiday for 2002 and 2012, thx @ludsoft (#315). + - Fix a small flake8 issue with wrong indentation (#319). + - Fix Russia "Day of Unity" date, set to November 4th, thx @alexitkes for the bug report (#317). + +Incorporate changes from workalendar v4.0.0 (2019-01-24) - Solved the incompatibility between `pandas` latest version and Python 3.4. Upgraded travis distro to Xenial/16.04 LTS (#307). - Added instructions about the usage of the `iso_register` decorator in the pull-request template (#309). -### New Calendars +- New Calendars -- Added New Zealand, by @johnguant (#306). -- Added Paraguay calendar, following the work of @reichert (#268). -- Added China calendar, by @iamsk (#304). -- Added Israel, by @armona, @tsehori (#281). + - Added New Zealand, by @johnguant (#306). + - Added Paraguay calendar, following the work of @reichert (#268). + - Added China calendar, by @iamsk (#304). + - Added Israel, by @armona, @tsehori (#281). -## v3.2.1 (2018-12-06) +3.0 +--- -### Bugfixes +Incorporate changes from workalendar 3.2.1: - Added DEEPAVALI days for 2019 and 2020, thx @pvalenti (#282). - Fixed Germany Reformation Day miscalculation. Some German states include Reformation Day since the "beginning" ; in 2017, all states included Reformation Day as a holiday (500th anniversary of the Reformation) ; starting of 2018, 4 states added Reformation Day (#295). -## v3.2.0 (2018-11-30) - -### Major changes +Incorporate changes from workalendar 3.2.0: - Removed dependency to `PyEphem`. This package was the "Python2-compatible" library to deal with the xephem system library. Now it's obsolete, so you don't need this dual-dependency handling, because `ephem` is compatible with Python 2 & Python 3 (#296). -- Raise an exception when trying to use unsupported date/datetime types. Workalendar now only supports stdlib `date` & `datetime` (sub)types. See the [basic documentation](https://workalendar.github.io/workalendar/basic.html#standard-datetime-types-only-please) for more details (#294). +- Raise an exception when trying to use unsupported date/datetime types. Workalendar now only supports stdlib `date` & `datetime` (sub)types. See the `basic documentation `_ for more details (#294). -## v3.1.1 (2018-11-17) - -### Bugfix +Incorporate changes from workalendar 3.1.1: - Fixed ISO 3166-1 code for the `Slovenia` calendar (#291, thx @john-sandall). -## v3.2.0 (2018-12-25) - -### New Calendars - -- Added China, by @iamsk. - -## v3.1.0 (2018-10-25) - -### Major changes +Incorporate changes from workalendar 3.1.0: - Added support for Python 3.7 (#283). - Fixed the `SouthAfrica` holidays calendar, taking into account the specs of holidays that vary over the periods. As a consequence, it cleaned up erroneous holidays that were duplicated in some years (#285). Thx to @surfer190 for his review & suggestions. - Bugfix for South Africa: disabled the possibility to compute holidays prior to the year 1910. - -### Minor changes - - Renamed Madagascar test class name into `MadagascarTest` (#286). - Separated the coverage jobs from the pure tests. Their report output was disturbing in development mode, you had to scroll your way up to find eventual failing tests (#289). -## v3.0.0 (2019-09-20) - -### Major changes +Incorporate changes from workalendar 3.0.0: -Large work on global registry: refs #13, #96, #257 & #284. +Large work on global registry: refs (#13), (#96), (#257) & (#284). - Added Tests for Europe registry. - Revamped and cleaned up Europe countries. - Added the United States of America + States, American countries & sub-regions, African countries, Asian countries, Oceanian countries. - The global registry usage is documented. - -### Breaking changes - - Changed Canada namespace to `workalendar.america.canada`. - You don't have to declare a `name` properties for Calendar classes. It will be deducted from the docstring. - Changed the `registry.items()` mandatory argument name to `region_codes` for more readability. -## v2.6.0 (2018-08-30) - -### New Calendars +Incorporate changes from workalendar 2.6.0: - Added Angola, by @dvdmgl (#276) - -### Bugfixes - - Portugal - removed carnival from Portuguese holidays, restored missing holidays (#275) - -### Other changes - - Added All Souls Day to common (#274) - Allow the `add_working_days()` function to be provided a datetime, and returning a `date` (#270). - Added a `keep_datetime` option to keep the original type of the input argument for both ``add_working_days()`` and ``sub_working_days()`` functions (#270). @@ -602,45 +620,35 @@ Large work on global registry: refs #13, #96, #257 & #284. - Added basic usage documentation, hosted by Github pages. - Added advanced usage documentation. -## v2.5.0 (2018-06-14) +Incorporate changes from workalendar 2.5.0: - Bugfix: deduplicate South Africa holidays that were emitted as duplicates (#265). - Add the `get_working_days_delta` method to the core calendar class (#260). -## v2.4.0 (2018-03-28) - -### New Calendars +Incorporate changes from workalendar 2.4.0: - Added Lithuania, by @landler (#254). - Added Russia, by @vanadium23 (#259). - -### Bugfixes - - Fixed shifting ANZAC day for Australia states (#249). - Renamed Australian state classes to actual state names(eg. AustraliaNewSouthWales to NewSouthWales). - Update ACT holidays (#251). - Fixing Federal Christmas Shift ; added a `include_veterans_day` flag to enable/disable Veteran's day on specific calendar - e.g. Mozilla's dedicated calendar (#242). - -### Other - - **Deprecation:** Dropped support for Python 3.3 (#245). - Fixed Travis-ci configuration for Python 3.5 and al (#252). -- Moved from `novafloss` to the `peopledoc` organization, the core People Doc Inc. organization (#255). - First step iteration on the "global registry" feature. European countries are now part of a registry loaded in the ``workalendar.registry`` module. Please use with care at the moment (#248). - Refactored Australia family and community day calculation (#244). -## v2.3.1 (2017-07-27) +2.0 +--- -I have done a terrible mistake with the 1.3.0 release, and uploaded a defunct 2.3.0 version... I tried to remove it from PyPI, but it failed at some point. In order to make sure that the latest version has the highest version number, I'm releasing this "dumb" version. It is equivalent to the 2.1.0 release, but at least, if you upgrade, you have all the 2.0+ niceties... - -## v2.1.0 (2017-07-27) +Incorporate changes from workalendar 2.1.0: - Added Hong Kong, by @nedlowe (#235). - Splitted `africa.py` file into an `africa/` module (#236). - Added Alabama Counties - Baldwin County, Mobile County, Perry County. Refactored UnitedStates classes to have a parameter to include the "Mardi Gras" day (#214). - Added brazilian calendar to consider working days for bank transactions, by @fvlima (#238). -## v2.0.0 (2017-06-23) +Incorporate changes from workalendar 2.0.0: - Major refactor in the USA module. Each State is now an independant module, all of the Mixins were removed, all the possible corrections have been made, following the main Wikipedia page, and cross-checking with official sources when it was possible (#171). - Added District of Columbia in the USA module (#217). @@ -649,7 +657,7 @@ I have done a terrible mistake with the 1.3.0 release, and uploaded a defunct 2. - Various refactors for the Asia module, essentially centered around a more convenient Chinese New Year computation toolset (#202). - Refactoring the USA tests: using inheritance to test federal and state-based holidays using only one "Don't Repeat Yourself" codebase (#213). -## v1.3.0 (2017-06-09) +Incorporate changes from workalendar 1.3.0: - Added Singapore calendar, initiated by @nedlowe (#194 + #195). - Added Malaysia, by @gregyhj (#201). @@ -658,7 +666,7 @@ I have done a terrible mistake with the 1.3.0 release, and uploaded a defunct 2. - Fixed a bug in Slovakia calendar, de-duplicated Christmas Day, that appeared twice (#205). - Fixed important bugs in the calendars of the following Brazilian cities: Vitória, Vila Velha, Cariacica, Guarapari and Serra - thx to Fernanda Gonçalves Rodrigues, who confirmed this issue raised by @Skippern (#199). -## v1.2.0 (2017-05-30) +Incorporate changes from workalendar 1.2.0: - Moved all the calendar of countries on the american continent in their own modules (#188). - Refactor base Calendar class get_weekend_days to use WEEKEND_DAYS more intelligently (#191 + #192). @@ -666,7 +674,7 @@ I have done a terrible mistake with the 1.3.0 release, and uploaded a defunct 2. - Added a ``good_friday_label`` class variable to ``ChristianMixin`` ; one can assign the right label to this holiday (#187). - Added a ``ash_wednesday_label`` class variable to ``ChristianMixin`` ; one can assign the right label to this holiday (#187). -## v1.1.0 (2017-02-28) +Incorporate changes from workalendar 1.1.0: - Added Cyprus. thx @gregn610 (#174). - Added Latvia. thx @gregn610 (#178). @@ -678,9 +686,7 @@ I have done a terrible mistake with the 1.3.0 release, and uploaded a defunct 2. - Fixed Historical and one-off holidays for South Africa. thx @gregn610 (#173). - Minor PEP8 fixes (#186). -## v1.0.0 (2016-12-12) - -After several years of development, we can now say that this library is production-ready, so we're releasing its 1.0.0 version. Millions of "thank you" to all the contributors involved. +Incorporate changes from workalendar 1.0.0: - Add Ireland. thx @gregn610 (#152). - Bugfix: New Year's Eve is not a holiday in Netherlands (#154). @@ -688,15 +694,21 @@ After several years of development, we can now say that this library is producti - Add Bulgaria. thx @gregn610 (#156) - Add Croatia. thx @gregn610 (#157) -## v0.8.1 (2016-11-08) +Incorporate changes from workalendar 0.8.1: - Reformation Day is a national holiday in Germany, but only in 2017 (#150). -## v0.8.0 (2016-08-25) +1.8 +--- + +Now tests are run using tox and releases are made automatically +using Travis-CI deployment framework. + +Incorporate changes from workalendar 0.8.0: - Fix Czech Republic calendar - as of 2016, Good Friday has become a holiday (#148). -## v0.7.0 (2016-08-02) +Incorporate changes from workalendar 0.7.0: - Easter Sunday is a Brandenburg federate state holiday (#143), thx @uvchik. - Added Catalonia (#145), thx @ferranp. @@ -704,7 +716,7 @@ After several years of development, we can now say that this library is producti - use py.test instead of nosetests for tests (#146). - cleanup: remove unused ``swiss.py`` file (#147). -## v0.6.1 (2016-06-29) +Incorporate changes from workalendar 0.6.1: - Added Estonia, thx to @landler (#134), - Europe-related modules being reorganized, thx to @Natim (#135), @@ -712,21 +724,35 @@ After several years of development, we can now say that this library is producti - Added a pull-request template (#125), - Added a Makefile for various dev-related tasks -- installs, running tests, uploading to PyPI... (#133). -*Note:* the 0.6.0 was erroneously uploaded ; this v0.6.1 cancels and replaces the v0.6.0. +1.7.1 +----- + +- #7: Avoid crashing on import when installed as zip package. -## v0.5.0 (2016-06-14) +1.7 +--- + +Incorporate changes from workalendar 0.5.0: -- Workalendar now being compatible with Python 3.4 and 3.5 (#128), - A new holiday has appeared in Japan as of 2016 (#131), thx @suhara for the report. -## v0.4.5 (2016-05-09) +Incorporate changes from workalendar 0.4.5: + +- Added Slovenia, thx to @ratek1 (#124). +- Added Switzerland, thx to @sykaeh (#127). + +1.6 +--- -- Added Slovenia, thx to @ratek1 (#124), -- Added Switzerland, thx to @sykaeh (#127), +- #6: Remove observance shift for Sweden. +- Use `jaraco skeleton `_ to + maintain the project structure, adding automatic releases + from continuous integration and bundled documentation. -## v0.4.3 & v0.4.4 (2016-01-19 & 2016-05-09) +1.5 +--- -**Sorry, I think I completely broke the 0.4.3 release by trying to delete a naughty file...** +Incorporate changes from workalendar 0.4.3: - Added Denmark (#117). - Tiny fixes in the ``usa.py`` module (flake8 + typo) (#122) @@ -735,63 +761,91 @@ After several years of development, we can now say that this library is producti - Moved from `novapost` to the `novafloss` organization, handling FLOSS projects in People Doc Inc. (#116) - Added Spain 2016 (#123) -## v0.4.2 (2015-12-23) +Incorporate changes from workalendar 0.4.2: - Added Luxembourg (#111) - Added Netherlands (#113) - Added Spain (#114) - Bugfix: fixed the name of the Pentecost for Sweden (#115) -## v0.4.1 (2015-08-05) +Incorporate changes from workalendar 0.4.1: - Added Portugal, thx to @borfast (#110). -## v0.4.0 (2015-06-11) +Incorporate changes from workalendar 0.4.0: - Added Colombia calendar, thx to @spalac24 - Added Slovakia calendar, thx to @Adman - Fixed the Boxing day & boxing day shift for Australia -## v0.3.0 (2015-01-30) +1.4 +--- + +``Calendar.get_observed_date`` now allows ``observance_shift`` to be +a callable accepting the holiday and calendar and returning the observed +date. ``Holiday`` supplies a ``get_nearest_weekday`` method suitable for +locating the nearest weekday. + +- #5: USA Independence Day now honors the nearest weekday model. + +1.3 +--- + +Incorporate these fixes from Workalendar 0.3: -- Germany calendar added, thx to @rndusr -- Support building on systems where LANG=C (Ubuntu) #92 -- little improvement to directly return a tested value. - ``delta`` argument for ``add_working_days()`` can be negative. added a ``sub_working_days()`` method that computes working days backwards. -- BUGFIX: shifting UK boxing day if Christmas day falls on a Friday (shit to - next Monday) #95 -- BUGFIX: Renaming the Finnish "Independance Day" #101 (thx to - @mikko-ahlroth-vincit) -- BUGFIX: Renaming Showa Day. "ō is not romji" #100 (thx @shinriyo) -- BUGFIX: Belgian National Day title #99 (thx @laulaz) - -## v0.2.0 (2014-07-15) - -- How to contribute documentation, -- Added Belgium, European Central Bank, Sweden, every specific calendar in the - United States of America, Canada. -- BUGFIX: fixed a corpus christi bug. This day used to be included in every - ChristianMixin calendar, except noticed otherwise. Now it's not included by - default and should be set to "True" when needed. - -## v0.1 (2014-02-17) - -- added LunarCalendar, including lunar month calculations -- added SouthKoreanCalendar, for a LunarCalendar proof of concept -- added Python3 support -- added Algeria, Australia, Brazil, Chile, Czech Republic, Finland, - France Alsace-Moselle, Greece, Hungary, Iceland, Italy, Ivory Coast, Japan, - Madagascar, Marshall Islands, Mexico, Northern Ireland, Norway, Panama, - Poland, Qatar, South Africa, São Tomé, Taiwan, United Kingdom calendars. -- BACKWARDS INCOMPATIBILITY: calendar suffix for class names are now obsolete. - e.g: to use the Japan calendar, simply import `workalendar.asia.Japan` instead - of JapanCalendar. - -## v0.0.1 (2013-11-21) - -- First released version -- Core calendar classes, Western (European and North American) - easter computations, -- United States federal days -- France legal holidays days +- BUGFIX: Renaming Showa Day. "ō is not romji" (#100) (thx @shinriyo) +- BUGFIX: Belgian National Day title (#99) (thx @laulaz) + +1.2.1 +----- + +Correct usage in example. + +1.2 +--- + +Fixed issue #4 where Finland holidays were shifted but shouldn't have been. +Calendars and Holidays may now specify observance_shift=None to signal no +shift. + +Package can now be tested with pytest-runner by invoking ``python setup.py +pytest``. + +1.1.3 +----- + +Fix name of Finnish Independence Day. + +1.1.2 +----- + +Fixed issues with packaging (disabled installation an zip egg and now use +setuptools always). + +1.1 +--- + +UnitedKingdom Calendar now uses indicated/observed Holidays. + +Includes these changes slated for workalendar 0.3: + +- BUGFIX: shifting UK boxing day if Christmas day falls on a Friday (shift to + next Monday) (#95) + +1.0 +--- + +Initial release of Calendra based on Workalendar 0.2. + +- Adds Holiday class per (#79). Adds support for giving + holidays a more rich description and better resolution of observed versus + indicated holidays. See the pull request for detail on the motivation and + implementation. See the usa.UnitedStates calendar for example usage. + +Includes these changes slated for workalendar 0.3: + +- Germany calendar added, thx to @rndusr +- Support building on systems where LANG=C (Ubuntu) (#92) +- little improvement to directly return a tested value. diff --git a/LICENSE b/LICENSE index 25e47c11..353924be 100644 --- a/LICENSE +++ b/LICENSE @@ -1,19 +1,19 @@ -Copyright (c) 2013-2021 Novapost/PeopleDoc, 2021 Workalendar Maintainers. +Copyright Jason R. Coombs Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. diff --git a/README.md b/README.md index 8c3f66b1..46d9ce2e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,12 @@ -# Workalendar +[![](https://img.shields.io/pypi/v/calendra.svg)][1] + +[![](https://img.shields.io/pypi/pyversions/calendra.svg)][1] + + [1]: https://pypi.org/project/calendra + +[![Automated Tests](https://github.com/jaraco/calendra/workflows/Automated%20Tests/badge.svg)](https://github.com/jaraco/calendra/actions?query=workflow%3A%22Automated+Tests%22) + +[![](https://readthedocs.org/projects/calendra/badge/?version=latest)](https://calendra.readthedocs.io/en/latest/?badge=latest) [![license](http://img.shields.io/pypi/l/workalendar.svg)](https://github.com/workalendar/workalendar/blob/master/LICENSE) [![pypi](http://img.shields.io/pypi/v/workalendar.svg)](https://pypi.python.org/pypi/workalendar) @@ -6,7 +14,23 @@ ## Overview -Workalendar is a Python module that offers classes able to handle calendars, list legal / religious holidays and gives working-day-related computation functions. +Calendra is a Python module that offers classes able to handle calendars, list legal / religious holidays and gives working-day-related computation functions. + +## History + +Calendra is a fork of [Workalendar](https://github.com/peopledoc/workalendar) +designed to be more extensible and introspectable, adding interfaces where +[Workalendar is philosophically opposed for the sake of simplicity](https://github.com/peopledoc/workalendar/pull/79). + +What can Calendra do that Workalendar cannot? + +- Provides descriptions for holidays for the "day indicated" for each + Holiday (such as '3rd Monday in August'). +- Keeps distinct the indicated and observed dates for Holidays, such + that it's possible to determine on what day a given holiday is observed. +- Allows the number of Holidays in a calendar year to be counted. +- Consolidates observance logic in the core code rather than requiring + each calendar implementation to implement its own. ## Installation @@ -38,15 +62,15 @@ If you had previously installed the `skyfield` and `skyfield-data` packages, the ## Status -This library is ready for production, although we may warn eventual users: some calendars may not be up-to-date, and this library doesn't cover all the existing countries on earth (yet). +The project is stable and in production use. Calendra follows the principles of [semver](https://semver.org) for released verisons. -If you spot any bug or wish to add a calendar, please refer to the [Contributing doc](docs/contributing.md). +If you spot any bug or wish to add a calendar, please refer to the [Contributing doc](https://peopledoc.github.io/workalendar/contributing.html). ## Usage sample -```python +```python-repl >>> from datetime import date ->>> from workalendar.europe import France +>>> from calendra.europe import France >>> cal = France() >>> cal.holidays(2012) [(datetime.date(2012, 1, 1), 'New year'), @@ -74,13 +98,7 @@ For a more complete documentation and advanced usage, go to [the official workal ## External dependencies -**Workalendar will require you to use Python 3.6+.** - -Workalendar is tested on Python 3.6, 3.7, 3.8, 3.9, and on Linux (Ubuntu), Mac OS and Windows using Github actions. - -### Conditional dependencies - -As of v15.0.0: +Calendra has been tested on the Python versions declared in setup.cfg. * If you're using \*Nix and Python 3.6, 3.7, 3.8, the package `backports.zoneinfo` is required * If you're using Windows and Python 3.6, 3.7, 3.8, the package `tzdata` is *also* a requirement (with the `backports.zoneinfo`). @@ -88,18 +106,10 @@ As of v15.0.0: ## Tests -CI status: - -[![Github action status](https://github.com/workalendar/workalendar/workflows/CI/badge.svg)](https://github.com/workalendar/workalendar/actions?query=workflow%3ACI) - -To run test, just install tox with `pip install tox` and run: - -``` -tox -``` - +To run test, just install tox with `pip install tox` and run `tox` from the command line. + ## Available Calendars ### Europe diff --git a/calendra/__init__.py b/calendra/__init__.py new file mode 100644 index 00000000..2081185e --- /dev/null +++ b/calendra/__init__.py @@ -0,0 +1,8 @@ +try: + import importlib.metadata as metadata +except ImportError: + import importlib_metadata as metadata + + +#: Module version, as defined in PEP-0396. +__version__ = metadata.version(__package__) diff --git a/workalendar/africa/__init__.py b/calendra/africa/__init__.py similarity index 100% rename from workalendar/africa/__init__.py rename to calendra/africa/__init__.py diff --git a/workalendar/africa/algeria.py b/calendra/africa/algeria.py similarity index 83% rename from workalendar/africa/algeria.py rename to calendra/africa/algeria.py index 680a3afa..fe8302d1 100644 --- a/workalendar/africa/algeria.py +++ b/calendra/africa/algeria.py @@ -14,9 +14,9 @@ class Algeria(IslamicCalendar): include_islamic_new_year = True FIXED_HOLIDAYS = IslamicCalendar.FIXED_HOLIDAYS + ( - (7, 5, "Independence Day"), - (11, 1, "Anniversary of the revolution"), - ) + (7, 5, "Independence Day"), + (11, 1, "Anniversary of the revolution"), + ) ISLAMIC_HOLIDAYS = IslamicCalendar.ISLAMIC_HOLIDAYS + ( (1, 10, "Ashura"), diff --git a/workalendar/africa/angola.py b/calendra/africa/angola.py similarity index 100% rename from workalendar/africa/angola.py rename to calendra/africa/angola.py diff --git a/workalendar/africa/benin.py b/calendra/africa/benin.py similarity index 100% rename from workalendar/africa/benin.py rename to calendra/africa/benin.py diff --git a/workalendar/africa/ivory_coast.py b/calendra/africa/ivory_coast.py similarity index 100% rename from workalendar/africa/ivory_coast.py rename to calendra/africa/ivory_coast.py diff --git a/workalendar/africa/kenya.py b/calendra/africa/kenya.py similarity index 100% rename from workalendar/africa/kenya.py rename to calendra/africa/kenya.py diff --git a/workalendar/africa/madagascar.py b/calendra/africa/madagascar.py similarity index 100% rename from workalendar/africa/madagascar.py rename to calendra/africa/madagascar.py diff --git a/workalendar/africa/mozambique.py b/calendra/africa/mozambique.py similarity index 100% rename from workalendar/africa/mozambique.py rename to calendra/africa/mozambique.py diff --git a/workalendar/africa/nigeria.py b/calendra/africa/nigeria.py similarity index 100% rename from workalendar/africa/nigeria.py rename to calendra/africa/nigeria.py diff --git a/workalendar/africa/sao_tome.py b/calendra/africa/sao_tome.py similarity index 100% rename from workalendar/africa/sao_tome.py rename to calendra/africa/sao_tome.py diff --git a/workalendar/africa/south_africa.py b/calendra/africa/south_africa.py similarity index 92% rename from workalendar/africa/south_africa.py rename to calendra/africa/south_africa.py index f422635c..3cb16eed 100644 --- a/workalendar/africa/south_africa.py +++ b/calendra/africa/south_africa.py @@ -1,6 +1,6 @@ -from datetime import timedelta, date +from datetime import date -from ..core import WesternCalendar, SUN, MON, FRI +from ..core import WesternCalendar, MON, FRI from ..exceptions import CalendarError from ..registry_tools import iso_register @@ -24,7 +24,7 @@ def get_easter_monday_or_family_day(self, year): label = "Family Day" return self.get_easter_monday(year), label - def get_fixed_holidays(self, year): + def get_fixed_holidays(self, year): # noqa: C901 days = super().get_fixed_holidays(year) if year >= 1990: days.append((date(year, 3, 21), 'Human Rights Day')) @@ -115,15 +115,8 @@ def get_variable_days(self, year): "Settlers' Day")) return days - def get_calendar_holidays(self, year): + def get_calendar_holidays(self, year): # noqa: C901 days = super().get_calendar_holidays(year) - # compute shifting days - for holiday, label in days: - if holiday.weekday() == SUN: - days.append(( - holiday + timedelta(days=1), - f"{label} substitute" - )) # Other one-offs. Don't shift these if year == 1999: diff --git a/workalendar/america/__init__.py b/calendra/america/__init__.py similarity index 100% rename from workalendar/america/__init__.py rename to calendra/america/__init__.py diff --git a/workalendar/america/argentina.py b/calendra/america/argentina.py similarity index 100% rename from workalendar/america/argentina.py rename to calendra/america/argentina.py diff --git a/workalendar/america/barbados.py b/calendra/america/barbados.py similarity index 100% rename from workalendar/america/barbados.py rename to calendra/america/barbados.py diff --git a/workalendar/america/brazil.py b/calendra/america/brazil.py similarity index 100% rename from workalendar/america/brazil.py rename to calendra/america/brazil.py diff --git a/workalendar/america/canada.py b/calendra/america/canada.py similarity index 100% rename from workalendar/america/canada.py rename to calendra/america/canada.py diff --git a/workalendar/america/chile.py b/calendra/america/chile.py similarity index 98% rename from workalendar/america/chile.py rename to calendra/america/chile.py index 1ad7b36d..97c492ce 100644 --- a/workalendar/america/chile.py +++ b/calendra/america/chile.py @@ -32,7 +32,7 @@ class Chile(WesternCalendar): # Immaculate Conception (Law 2.977) include_immaculate_conception = True - def get_variable_days(self, year): + def get_variable_days(self, year): # noqa: C901 days = super().get_variable_days(year) # Indigenous Peoples (Law 21.357) diff --git a/workalendar/america/colombia.py b/calendra/america/colombia.py similarity index 100% rename from workalendar/america/colombia.py rename to calendra/america/colombia.py diff --git a/workalendar/america/mexico.py b/calendra/america/mexico.py similarity index 57% rename from workalendar/america/mexico.py rename to calendra/america/mexico.py index ee771cde..68ab73c4 100644 --- a/workalendar/america/mexico.py +++ b/calendra/america/mexico.py @@ -1,31 +1,43 @@ from datetime import date, timedelta -from ..core import WesternCalendar, SUN, MON, SAT +from dateutil import relativedelta as rd + +from ..core import WesternCalendar, ChristianMixin +from ..core import SUN, SAT +from ..core import Holiday from ..registry_tools import iso_register @iso_register('MX') -class Mexico(WesternCalendar): +class Mexico(WesternCalendar, ChristianMixin): "Mexico" FIXED_HOLIDAYS = WesternCalendar.FIXED_HOLIDAYS + ( + (5, 1, "Labour Day"), (9, 16, "Independence Day"), ) - # Civil holidays - include_labour_day = True - def get_variable_days(self, year): - days = super().get_variable_days(year) - days.append( - (Mexico.get_nth_weekday_in_month(year, 2, MON), - "Constitution Day")) + shift_new_years_day = True - days.append( - (Mexico.get_nth_weekday_in_month(year, 3, MON, 3), - "Benito Juárez's birthday")) + @property + def observance_shift(self): + return Holiday.nearest_weekday - days.append( - (Mexico.get_nth_weekday_in_month(year, 11, MON, 3), - "Revolution Day")) + def get_variable_days(self, year): + days = super().get_variable_days(year) + days.append(Holiday( + date(year, 2, 1) + rd.relativedelta(weekday=rd.MO(1)), + "Constitution Day", + )) + + days.append(Holiday( + date(year, 3, 1) + rd.relativedelta(weekday=rd.MO(3)), + "Benito Juárez's birthday", + )) + + days.append(Holiday( + date(year, 11, 1) + rd.relativedelta(weekday=rd.MO(3)), + "Revolution Day", + )) return days diff --git a/workalendar/america/panama.py b/calendra/america/panama.py similarity index 100% rename from workalendar/america/panama.py rename to calendra/america/panama.py diff --git a/workalendar/america/paraguay.py b/calendra/america/paraguay.py similarity index 100% rename from workalendar/america/paraguay.py rename to calendra/america/paraguay.py diff --git a/workalendar/asia/__init__.py b/calendra/asia/__init__.py similarity index 100% rename from workalendar/asia/__init__.py rename to calendra/asia/__init__.py diff --git a/workalendar/asia/china.py b/calendra/asia/china.py similarity index 100% rename from workalendar/asia/china.py rename to calendra/asia/china.py diff --git a/workalendar/asia/hong_kong.py b/calendra/asia/hong_kong.py similarity index 99% rename from workalendar/asia/hong_kong.py rename to calendra/asia/hong_kong.py index 52964f59..d073c3c7 100644 --- a/workalendar/asia/hong_kong.py +++ b/calendra/asia/hong_kong.py @@ -33,6 +33,7 @@ class HongKong(WesternMixin, ChineseNewYearCalendar): chinese_third_day_label = "Third day of Chinese Lunar New Year" shift_sunday_holidays = True # Except CNY which rolls to Saturday shift_start_cny_sunday = False # Prior to 2011 this was True + shift_new_years_day = True def get_variable_days(self, year): """ diff --git a/workalendar/asia/israel.py b/calendra/asia/israel.py similarity index 100% rename from workalendar/asia/israel.py rename to calendra/asia/israel.py diff --git a/workalendar/asia/japan.py b/calendra/asia/japan.py similarity index 100% rename from workalendar/asia/japan.py rename to calendra/asia/japan.py diff --git a/workalendar/asia/kazakhstan.py b/calendra/asia/kazakhstan.py similarity index 100% rename from workalendar/asia/kazakhstan.py rename to calendra/asia/kazakhstan.py diff --git a/workalendar/asia/malaysia.py b/calendra/asia/malaysia.py similarity index 100% rename from workalendar/asia/malaysia.py rename to calendra/asia/malaysia.py diff --git a/workalendar/asia/philippines.py b/calendra/asia/philippines.py similarity index 100% rename from workalendar/asia/philippines.py rename to calendra/asia/philippines.py diff --git a/workalendar/asia/qatar.py b/calendra/asia/qatar.py similarity index 100% rename from workalendar/asia/qatar.py rename to calendra/asia/qatar.py diff --git a/workalendar/asia/singapore.py b/calendra/asia/singapore.py similarity index 100% rename from workalendar/asia/singapore.py rename to calendra/asia/singapore.py diff --git a/workalendar/asia/south_korea.py b/calendra/asia/south_korea.py similarity index 100% rename from workalendar/asia/south_korea.py rename to calendra/asia/south_korea.py diff --git a/workalendar/asia/taiwan.py b/calendra/asia/taiwan.py similarity index 100% rename from workalendar/asia/taiwan.py rename to calendra/asia/taiwan.py diff --git a/workalendar/astronomy.py b/calendra/astronomy.py similarity index 100% rename from workalendar/astronomy.py rename to calendra/astronomy.py diff --git a/workalendar/core.py b/calendra/core.py similarity index 89% rename from workalendar/core.py rename to calendra/core.py index 15171e10..97f6ce24 100644 --- a/workalendar/core.py +++ b/calendra/core.py @@ -1,22 +1,25 @@ """ Working day tools """ -from copy import copy import warnings + from calendar import monthrange from datetime import date, timedelta, datetime from pathlib import Path import sys +import functools import convertdate from dateutil import easter from lunardate import LunarDate +from dateutil import relativedelta as rd from .exceptions import ( UnsupportedDateType, CalendarError, ICalExportRangeError, ICalExportTargetPathError ) from . import __version__ +from .holiday import Holiday, SeriesShiftMixin MON, TUE, WED, THU, FRI, SAT, SUN = range(7) ISO_MON, ISO_TUE, ISO_WED, ISO_THU, ISO_FRI, ISO_SAT, ISO_SUN = range(1, 8) @@ -89,7 +92,6 @@ class ChristianMixin: include_immaculate_conception = False immaculate_conception_label = "Immaculate Conception" include_christmas = True - christmas_day_label = "Christmas Day" include_christmas_eve = False include_ascension = False include_assumption = False @@ -168,7 +170,7 @@ def shift_christmas_boxing_days(self, year): """ christmas = date(year, 12, 25) boxing_day = date(year, 12, 26) - boxing_day_label = f"{self.boxing_day_label} Shift" + boxing_day_label = "{} Shift".format(self.boxing_day_label) results = [] if christmas.weekday() in self.get_weekend_days(): shift = self.find_following_working_day(christmas) @@ -220,12 +222,19 @@ def get_variable_days(self, year): # noqa days.append((date(year, 11, 2), "All Souls Day")) if self.include_immaculate_conception: days.append((date(year, 12, 8), self.immaculate_conception_label)) + christmas = None if self.include_christmas: - days.append((date(year, 12, 25), self.christmas_day_label)) + christmas = Holiday(date(year, 12, 25), "Christmas Day") + days.append(christmas) if self.include_christmas_eve: days.append((date(year, 12, 24), "Christmas Eve")) if self.include_boxing_day: - days.append((date(year, 12, 26), self.boxing_day_label)) + boxing_day = Holiday( + date(year, 12, 26), + self.boxing_day_label, + indication="Day after Christmas", + ) + days.append(boxing_day) if self.include_ascension: days.append(( self.get_ascension_thursday(year), "Ascension Thursday")) @@ -247,12 +256,23 @@ class WesternMixin(ChristianMixin): """ EASTER_METHOD = easter.EASTER_WESTERN WEEKEND_DAYS = (SAT, SUN) + include_new_years_day = False + shift_new_years_day = False + + def get_variable_days(self, year): + days = super().get_variable_days(year) + new_years = Holiday( + date(year, 1, 1), 'New year', indication='First day in January', + ) + if not self.shift_new_years_day: + new_years.observance_shift = None + days.append(new_years) + return days class OrthodoxMixin(ChristianMixin): EASTER_METHOD = easter.EASTER_ORTHODOX WEEKEND_DAYS = (SAT, SUN) - include_orthodox_christmas = True # This label should be de-duplicated if needed orthodox_christmas_day_label = "Christmas" @@ -367,30 +387,13 @@ def get_variable_days(self, year): days.extend(self.get_chinese_new_year(year)) return days - def get_shifted_holidays(self, dates): - """ - Taking a list of existing holidays, yield a list of 'shifted' days if - the holiday falls on SUN. - """ - for holiday, label in dates: - if holiday.weekday() == SUN: - yield ( - holiday + timedelta(days=1), - f'{label} shift' - ) - - def get_calendar_holidays(self, year): + @staticmethod + def observance_shift(holiday, calendar): """ - Take into account the eventual shift to the next MON if any holiday - falls on SUN. + Taking an existing holiday, return a 'shifted' day to skip on from SUN. """ - # Unshifted days are here: - days = super().get_calendar_holidays(year) - if self.shift_sunday_holidays: - days_to_inspect = copy(days) - for day_shifted in self.get_shifted_holidays(days_to_inspect): - days.append(day_shifted) - return days + do_shift = calendar.shift_sunday_holidays and holiday.weekday() == SUN + return holiday + timedelta(days=1) * do_shift class CalverterMixin: @@ -472,7 +475,7 @@ class IslamicMixin(CalverterMixin): include_laylat_al_qadr = False include_nuzul_al_quran = False - def get_islamic_holidays(self): + def get_islamic_holidays(self): # noqa: C901 """Return a list of Islamic (month, day, label) for islamic holidays. Please take note that these dates must be expressed using the Islamic Calendar""" @@ -507,6 +510,12 @@ class CoreCalendar: FIXED_HOLIDAYS = () WEEKEND_DAYS = () + observance_shift = dict(weekday=rd.MO(1)) + """ + The shift for the observance of a holiday defined as keyword parameters to + a rd.relativedelta instance. + By default, holidays are shifted to the Monday following the weekend. + """ def __init__(self): self._holidays = {} @@ -523,10 +532,11 @@ def name(cls): def get_fixed_holidays(self, year): """Return the fixed days according to the FIXED_HOLIDAYS class property """ - days = [] - for month, day, label in self.FIXED_HOLIDAYS: - days.append((date(year, month, day), label)) - return days + fixed_holidays = map( + Holiday._from_fixed_definition, + self.FIXED_HOLIDAYS, + ) + return [day.replace(year=year) for day in fixed_holidays] def get_variable_days(self, year): return [] @@ -547,7 +557,9 @@ def holidays(self, year=None): return self._holidays[year] # Here we process the holiday specific calendar - temp_calendar = tuple(self.get_calendar_holidays(year)) + days = self.get_calendar_holidays(year) + days = map(Holiday._from_resolved_definition, days) + temp_calendar = tuple(days) # it is sorted self._holidays[year] = sorted(temp_calendar) @@ -558,9 +570,38 @@ def get_holiday_label(self, day): day = cleaned_date(day) return {day: label for day, label in self.holidays(day.year)}.get(day) + @functools.lru_cache() + def get_observed_date(self, holiday): + """ + The date the holiday is observed for this calendar. If the holiday + occurs on a weekend, it may be observed on another day as indicated by + the observance_shift. + + The holiday may also specify an 'observe_after' such that it is always + shifted after a preceding holiday. For example, Boxing day is always + observed after Christmas Day is observed. + """ + # observance_shift may be overridden in the holiday itself + shift = getattr(holiday, 'observance_shift', self.observance_shift) + if callable(shift): + shifted = shift(holiday, self) + else: + shift = shift or {} + delta = rd.relativedelta(**shift) + try: + weekend_days = self.get_weekend_days() + except NotImplementedError: + weekend_days = () + should_shift = holiday.weekday() in weekend_days + shifted = holiday + delta if should_shift else holiday + precedent = getattr(holiday, 'observe_after', None) + while precedent and shifted <= self.get_observed_date(precedent): + shifted += timedelta(days=1) + return shifted + def holidays_set(self, year=None): "Return a quick date index (set)" - return {day for day, label in self.holidays(year)} + return set(self.holidays(year)) def get_weekend_days(self): """Return a list (or a tuple) of weekdays that are *not* working days. @@ -603,8 +644,9 @@ def is_working_day(self, day, # Regular rules if day.weekday() in self.get_weekend_days(): return False - - return not self.is_holiday(day, extra_holidays=extra_holidays) + if extra_holidays and day in extra_holidays: + return False + return not self.is_observed_holiday(day) def is_holiday(self, day, extra_holidays=None): """Return True if it's an holiday. @@ -624,6 +666,12 @@ def is_holiday(self, day, extra_holidays=None): return day in self.holidays_set(day.year) + def is_observed_holiday(self, day): + """Return True if it's an observed holiday. + """ + observed = set(map(self.get_observed_date, self.holidays(day.year))) + return day in observed + def add_working_days(self, day, delta, extra_working_days=None, extra_holidays=None, keep_datetime=False): @@ -770,14 +818,14 @@ def get_iso_week_date(year, week_nb, weekday=ISO_MON): datetime.date(2021, 11, 1) For your convenience, the ISO weekdays are available via the - ``workalendar.core`` module, like this: + ``calendra.core`` module, like this: - from workalendar.core import ISO_MON, ISO_TUE # etc. + from calendra.core import ISO_MON, ISO_TUE # etc. i.e.: if you need to get the FRI of the week 44 of the year 2020, you'll have to use: - from workalendar.core import ISO_FRI + from calendra.core import ISO_FRI Calendar.get_iso_week_date(2020, 44, ISO_FRI) """ @@ -827,6 +875,7 @@ def get_working_days_delta(self, start, end, include_start=False, In France, April 1st 2018 is a holiday because it's Easter monday: + >>> from calendra.europe import France >>> cal = France() >>> cal.get_working_days_delta(day1, day2) 4 @@ -920,9 +969,10 @@ def _get_ical_target_path(self, target_path): None. Examples -------- + >>> from calendra.europe import Austria >>> cal = Austria() - >>> cal._get_ical_target_path('austria') # -> austria.ics - + >>> str(cal._get_ical_target_path('austria')) + 'austria.ics' """ if not target_path: @@ -975,13 +1025,14 @@ def export_to_ical(self, period=[2000, 2030], target_path=None): dtstamp = f'DTSTAMP;VALUE=DATE-TIME:{datetime.utcnow():%Y%m%dT%H%M%SZ}' # add an event for each holiday - for date_, name in holidays: + for holiday in holidays: + date_ = self.get_observed_date(holiday) ics.extend([ 'BEGIN:VEVENT', - f'SUMMARY:{name}', + f'SUMMARY:{holiday.name}', f'DTSTART;VALUE=DATE:{date_:%Y%m%d}', dtstamp, - f'UID:{date_}{name}@peopledoc.github.io/workalendar', + f'UID:{date_}{holiday.name}@peopledoc.github.io/workalendar', 'END:VEVENT', ]) @@ -1000,7 +1051,7 @@ def export_to_ical(self, period=[2000, 2030], target_path=None): return ics -class Calendar(CoreCalendar): +class Calendar(SeriesShiftMixin, CoreCalendar): """ The cornerstone of Earth calendars. diff --git a/calendra/create-astronomical-data.py b/calendra/create-astronomical-data.py new file mode 100755 index 00000000..3756fe62 --- /dev/null +++ b/calendra/create-astronomical-data.py @@ -0,0 +1,6 @@ +from tqdm import tqdm + +from .precomputed_astronomy import create_astronomical_data + +if __name__ == '__main__': + create_astronomical_data(tqdm) diff --git a/workalendar/equinoxes.json.gz b/calendra/equinoxes.json.gz similarity index 100% rename from workalendar/equinoxes.json.gz rename to calendra/equinoxes.json.gz diff --git a/workalendar/europe/__init__.py b/calendra/europe/__init__.py similarity index 100% rename from workalendar/europe/__init__.py rename to calendra/europe/__init__.py diff --git a/workalendar/europe/austria.py b/calendra/europe/austria.py similarity index 100% rename from workalendar/europe/austria.py rename to calendra/europe/austria.py diff --git a/workalendar/europe/belarus.py b/calendra/europe/belarus.py similarity index 100% rename from workalendar/europe/belarus.py rename to calendra/europe/belarus.py diff --git a/workalendar/europe/belgium.py b/calendra/europe/belgium.py similarity index 100% rename from workalendar/europe/belgium.py rename to calendra/europe/belgium.py diff --git a/workalendar/europe/bulgaria.py b/calendra/europe/bulgaria.py similarity index 100% rename from workalendar/europe/bulgaria.py rename to calendra/europe/bulgaria.py diff --git a/workalendar/europe/cayman_islands.py b/calendra/europe/cayman_islands.py similarity index 99% rename from workalendar/europe/cayman_islands.py rename to calendra/europe/cayman_islands.py index b1d11183..e6575256 100644 --- a/workalendar/europe/cayman_islands.py +++ b/calendra/europe/cayman_islands.py @@ -3,7 +3,6 @@ from ..core import WesternCalendar, MON, SAT from ..registry_tools import iso_register - QUEENS_BIRTHDAY_EXCEPTIONS = { 2013: date(2013, 6, 17), 2017: date(2017, 6, 19), diff --git a/workalendar/europe/croatia.py b/calendra/europe/croatia.py similarity index 100% rename from workalendar/europe/croatia.py rename to calendra/europe/croatia.py diff --git a/workalendar/europe/cyprus.py b/calendra/europe/cyprus.py similarity index 100% rename from workalendar/europe/cyprus.py rename to calendra/europe/cyprus.py diff --git a/workalendar/europe/czech_republic.py b/calendra/europe/czech_republic.py similarity index 100% rename from workalendar/europe/czech_republic.py rename to calendra/europe/czech_republic.py diff --git a/workalendar/europe/denmark.py b/calendra/europe/denmark.py similarity index 100% rename from workalendar/europe/denmark.py rename to calendra/europe/denmark.py diff --git a/workalendar/europe/estonia.py b/calendra/europe/estonia.py similarity index 100% rename from workalendar/europe/estonia.py rename to calendra/europe/estonia.py diff --git a/workalendar/europe/european_central_bank.py b/calendra/europe/european_central_bank.py similarity index 100% rename from workalendar/europe/european_central_bank.py rename to calendra/europe/european_central_bank.py diff --git a/workalendar/europe/finland.py b/calendra/europe/finland.py similarity index 98% rename from workalendar/europe/finland.py rename to calendra/europe/finland.py index ff3d7e0d..1a868758 100644 --- a/workalendar/europe/finland.py +++ b/calendra/europe/finland.py @@ -18,6 +18,7 @@ class Finland(WesternCalendar): include_christmas_eve = True include_boxing_day = True boxing_day_label = "St. Stephen's Day" + observance_shift = None # Civil holidays include_labour_day = True diff --git a/workalendar/europe/france.py b/calendra/europe/france.py similarity index 100% rename from workalendar/europe/france.py rename to calendra/europe/france.py diff --git a/workalendar/europe/georgia.py b/calendra/europe/georgia.py similarity index 100% rename from workalendar/europe/georgia.py rename to calendra/europe/georgia.py diff --git a/workalendar/europe/germany.py b/calendra/europe/germany.py similarity index 100% rename from workalendar/europe/germany.py rename to calendra/europe/germany.py diff --git a/workalendar/europe/greece.py b/calendra/europe/greece.py similarity index 100% rename from workalendar/europe/greece.py rename to calendra/europe/greece.py diff --git a/workalendar/europe/guernsey.py b/calendra/europe/guernsey.py similarity index 100% rename from workalendar/europe/guernsey.py rename to calendra/europe/guernsey.py diff --git a/workalendar/europe/hungary.py b/calendra/europe/hungary.py similarity index 100% rename from workalendar/europe/hungary.py rename to calendra/europe/hungary.py diff --git a/workalendar/europe/iceland.py b/calendra/europe/iceland.py similarity index 100% rename from workalendar/europe/iceland.py rename to calendra/europe/iceland.py diff --git a/workalendar/europe/ireland.py b/calendra/europe/ireland.py similarity index 100% rename from workalendar/europe/ireland.py rename to calendra/europe/ireland.py diff --git a/workalendar/europe/italy.py b/calendra/europe/italy.py similarity index 100% rename from workalendar/europe/italy.py rename to calendra/europe/italy.py diff --git a/workalendar/europe/latvia.py b/calendra/europe/latvia.py similarity index 100% rename from workalendar/europe/latvia.py rename to calendra/europe/latvia.py diff --git a/workalendar/europe/lithuania.py b/calendra/europe/lithuania.py similarity index 100% rename from workalendar/europe/lithuania.py rename to calendra/europe/lithuania.py diff --git a/workalendar/europe/luxembourg.py b/calendra/europe/luxembourg.py similarity index 100% rename from workalendar/europe/luxembourg.py rename to calendra/europe/luxembourg.py diff --git a/workalendar/europe/malta.py b/calendra/europe/malta.py similarity index 100% rename from workalendar/europe/malta.py rename to calendra/europe/malta.py diff --git a/workalendar/europe/monaco.py b/calendra/europe/monaco.py similarity index 100% rename from workalendar/europe/monaco.py rename to calendra/europe/monaco.py diff --git a/workalendar/europe/netherlands.py b/calendra/europe/netherlands.py similarity index 99% rename from workalendar/europe/netherlands.py rename to calendra/europe/netherlands.py index 73919507..1806de1b 100644 --- a/workalendar/europe/netherlands.py +++ b/calendra/europe/netherlands.py @@ -15,6 +15,8 @@ class Netherlands(WesternCalendar): include_whit_monday = True include_boxing_day = True + observance_shift = None + FIXED_HOLIDAYS = WesternCalendar.FIXED_HOLIDAYS + ( (5, 5, "Liberation Day"), ) diff --git a/workalendar/europe/norway.py b/calendra/europe/norway.py similarity index 100% rename from workalendar/europe/norway.py rename to calendra/europe/norway.py diff --git a/workalendar/europe/poland.py b/calendra/europe/poland.py similarity index 100% rename from workalendar/europe/poland.py rename to calendra/europe/poland.py diff --git a/workalendar/europe/portugal.py b/calendra/europe/portugal.py similarity index 100% rename from workalendar/europe/portugal.py rename to calendra/europe/portugal.py diff --git a/workalendar/europe/romania.py b/calendra/europe/romania.py similarity index 100% rename from workalendar/europe/romania.py rename to calendra/europe/romania.py diff --git a/workalendar/europe/russia.py b/calendra/europe/russia.py similarity index 100% rename from workalendar/europe/russia.py rename to calendra/europe/russia.py diff --git a/workalendar/europe/scotland/__init__.py b/calendra/europe/scotland/__init__.py similarity index 100% rename from workalendar/europe/scotland/__init__.py rename to calendra/europe/scotland/__init__.py diff --git a/workalendar/europe/scotland/mixins/__init__.py b/calendra/europe/scotland/mixins/__init__.py similarity index 100% rename from workalendar/europe/scotland/mixins/__init__.py rename to calendra/europe/scotland/mixins/__init__.py diff --git a/workalendar/europe/scotland/mixins/autumn_holiday.py b/calendra/europe/scotland/mixins/autumn_holiday.py similarity index 100% rename from workalendar/europe/scotland/mixins/autumn_holiday.py rename to calendra/europe/scotland/mixins/autumn_holiday.py diff --git a/workalendar/europe/scotland/mixins/fair_holiday.py b/calendra/europe/scotland/mixins/fair_holiday.py similarity index 100% rename from workalendar/europe/scotland/mixins/fair_holiday.py rename to calendra/europe/scotland/mixins/fair_holiday.py diff --git a/workalendar/europe/scotland/mixins/spring_holiday.py b/calendra/europe/scotland/mixins/spring_holiday.py similarity index 100% rename from workalendar/europe/scotland/mixins/spring_holiday.py rename to calendra/europe/scotland/mixins/spring_holiday.py diff --git a/workalendar/europe/scotland/mixins/victoria_day.py b/calendra/europe/scotland/mixins/victoria_day.py similarity index 100% rename from workalendar/europe/scotland/mixins/victoria_day.py rename to calendra/europe/scotland/mixins/victoria_day.py diff --git a/workalendar/europe/serbia.py b/calendra/europe/serbia.py similarity index 100% rename from workalendar/europe/serbia.py rename to calendra/europe/serbia.py diff --git a/workalendar/europe/slovakia.py b/calendra/europe/slovakia.py similarity index 100% rename from workalendar/europe/slovakia.py rename to calendra/europe/slovakia.py diff --git a/workalendar/europe/slovenia.py b/calendra/europe/slovenia.py similarity index 100% rename from workalendar/europe/slovenia.py rename to calendra/europe/slovenia.py diff --git a/workalendar/europe/spain.py b/calendra/europe/spain.py similarity index 100% rename from workalendar/europe/spain.py rename to calendra/europe/spain.py diff --git a/workalendar/europe/sweden.py b/calendra/europe/sweden.py similarity index 98% rename from workalendar/europe/sweden.py rename to calendra/europe/sweden.py index 2bf89acc..4b074409 100644 --- a/workalendar/europe/sweden.py +++ b/calendra/europe/sweden.py @@ -19,6 +19,7 @@ class Sweden(WesternCalendar): include_christmas_eve = True include_boxing_day = True boxing_day_label = "Second Day of Christmas" + observance_shift = None # Civil holidays include_labour_day = True diff --git a/workalendar/europe/switzerland.py b/calendra/europe/switzerland.py similarity index 100% rename from workalendar/europe/switzerland.py rename to calendra/europe/switzerland.py diff --git a/workalendar/europe/turkey.py b/calendra/europe/turkey.py similarity index 93% rename from workalendar/europe/turkey.py rename to calendra/europe/turkey.py index 78e8a84a..a8c729f5 100644 --- a/workalendar/europe/turkey.py +++ b/calendra/europe/turkey.py @@ -6,7 +6,8 @@ @iso_register('TR') class Turkey(IslamicCalendar): 'Turkey' - shift_new_years_day = True + include_new_years_day = True + shift_new_years_day = False # Even though they're using an islamic calendar, the work week is MON->FRI WEEKEND_DAYS = (SAT, SUN) diff --git a/workalendar/europe/ukraine.py b/calendra/europe/ukraine.py similarity index 100% rename from workalendar/europe/ukraine.py rename to calendra/europe/ukraine.py diff --git a/workalendar/europe/united_kingdom.py b/calendra/europe/united_kingdom.py similarity index 71% rename from workalendar/europe/united_kingdom.py rename to calendra/europe/united_kingdom.py index 7e044c94..61d9ed20 100644 --- a/workalendar/europe/united_kingdom.py +++ b/calendra/europe/united_kingdom.py @@ -1,5 +1,9 @@ from datetime import date -from ..core import WesternCalendar, MON + +from dateutil import relativedelta as rd + +from ..core import Holiday +from ..core import WesternCalendar from ..registry_tools import iso_register @@ -27,39 +31,36 @@ def get_early_may_bank_holiday(self, year): """ Return Early May bank holiday """ + day = date(year, 5, 1) + rd.relativedelta(weekday=rd.MO(1)) + desc = "Early May Bank Holiday" + indication = "1st Monday in May" + # Special case in 2020, for the 75th anniversary of the end of WWII. if year == 2020: - return ( - date(year, 5, 8), - "Early May bank holiday (VE day)" - ) - - return ( - UnitedKingdom.get_nth_weekday_in_month(year, 5, MON), - "Early May Bank Holiday" - ) + day = date(year, 5, 8) + desc += " (VE day)" + indication = "VE day" + return Holiday(day, desc, indication=indication) def get_spring_bank_holiday(self, year): - if year == 2012: - spring_bank_holiday = date(2012, 6, 4) - elif year == 1977: - spring_bank_holiday = date(1977, 6, 6) - elif year == 2002: - spring_bank_holiday = date(2002, 6, 4) - elif year == 2022: - spring_bank_holiday = date(2022, 6, 2) - else: - spring_bank_holiday = UnitedKingdom \ - .get_last_weekday_in_month(year, 5, MON) - return ( - spring_bank_holiday, - "Spring Bank Holiday" - ) + day = date(year, 5, 30) + rd.relativedelta(weekday=rd.MO(-1)) + if year in (2012, 2002): + day = date(year, 6, 4) + if year in (1977,): + day = date(year, 6, 6) + if year == 2022: + day = date(year, 6, 2) + return Holiday( + day, + "Spring Bank Holiday", + indication="Last Monday in May", + ), def get_late_summer_bank_holiday(self, year): - return ( - UnitedKingdom.get_last_weekday_in_month(year, 8, MON), - "Late Summer Bank Holiday" + return Holiday( + date(year, 8, 31) + rd.relativedelta(weekday=rd.MO(-1)), + "Late Summer Bank Holiday", + indication="Last Monday in August", ) def non_computable_holiday(self, year): @@ -71,9 +72,6 @@ def get_variable_days(self, year): days.append(self.get_early_may_bank_holiday(year)) days.append(self.get_spring_bank_holiday(year)) days.append(self.get_late_summer_bank_holiday(year)) - # Boxing day & XMas shift - shifts = self.shift_christmas_boxing_days(year=year) - days.extend(shifts) non_computable = self.non_computable_holiday(year) if non_computable: days.extend(non_computable) diff --git a/workalendar/exceptions.py b/calendra/exceptions.py similarity index 100% rename from workalendar/exceptions.py rename to calendra/exceptions.py diff --git a/calendra/holiday.py b/calendra/holiday.py new file mode 100644 index 00000000..f16e0ef1 --- /dev/null +++ b/calendra/holiday.py @@ -0,0 +1,119 @@ +import itertools +from datetime import date, timedelta + +from more_itertools import recipes + + +class Holiday(date): + """ + A named holiday with an indicated date, name, and additional keyword + attributes. + + >>> nyd = Holiday(date(2014, 1, 1), "New year") + + But if New Year's Eve is also a holiday, and it too falls on a weekend, + many calendars will have that holiday fall back to the previous friday: + + >>> from dateutil import relativedelta as rd + >>> nye = Holiday(date(2014, 12, 31), "New year's eve", + ... observance_shift=dict(weekday=rd.FR(-1))) + + For compatibility, a Holiday may be treated like a tuple of (date, label) + + >>> nyd[0] == date(2014, 1, 1) + True + >>> nyd[1] + 'New year' + >>> d, label = nyd + """ + + def __new__(cls, date, *args, **kwargs): + return super().__new__( + cls, date.year, date.month, date.day) + + def __init__(self, date, name='Holiday', **kwargs): + self.name = name + vars(self).update(kwargs) + + def __getitem__(self, n): + """ + for compatibility as a two-tuple + """ + tp = self, self.name + return tp[n] + + def __iter__(self): + """ + for compatibility as a two-tuple + """ + tp = self, self.name + return iter(tp) + + def replace(self, *args, **kwargs): + replaced = super().replace(*args, **kwargs) + vars(replaced).update(vars(self)) + return replaced + + def __add__(self, other): + orig = date(self.year, self.month, self.day) + return Holiday(orig + other, **vars(self)) + + def __sub__(self, other): + orig = date(self.year, self.month, self.day) + return Holiday(orig - other, **vars(self)) + + def nearest_weekday(self, calendar): + """ + Return the nearest weekday to self. + """ + weekend_days = calendar.get_weekend_days() + deltas = (timedelta(n) for n in itertools.count()) + candidates = recipes.flatten( + (self - delta, self + delta) + for delta in deltas + ) + matches = ( + day for day in candidates + if day.weekday() not in weekend_days + ) + return next(matches) + + @classmethod + def _from_fixed_definition(cls, item): + """For backward compatibility, load Holiday object from an item of + FIXED_HOLIDAYS class property, which might be just a tuple of + month, day, label. + """ + if isinstance(item, tuple): + month, day, label = item + any_year = 2000 + item = Holiday(date(any_year, month, day), label) + return item + + @classmethod + def _from_resolved_definition(cls, item, **kwargs): + """For backward compatibility, load Holiday object from a two-tuple + or existing Holiday instance. + """ + if isinstance(item, tuple): + item = Holiday(*item, **kwargs) + return item + + +class SeriesShiftMixin: + """ + "Series" holidays like the two Islamic Eid's or Chinese Spring Festival span + multiple days. If one of these days encounters a non-zero observance_shift, + apply that shift to all subsequent members of the series. + """ + + def get_calendar_holidays(self, year): + """ + Ensure that all events are observed in the order indicated. + """ + days = super().get_calendar_holidays(year) + holidays = sorted(map(Holiday._from_resolved_definition, days)) + from more_itertools import pairwise + for a, b in pairwise(holidays): + b.observe_after = a + return holidays diff --git a/workalendar/oceania/__init__.py b/calendra/oceania/__init__.py similarity index 100% rename from workalendar/oceania/__init__.py rename to calendra/oceania/__init__.py diff --git a/workalendar/oceania/australia.py b/calendra/oceania/australia.py similarity index 100% rename from workalendar/oceania/australia.py rename to calendra/oceania/australia.py diff --git a/workalendar/oceania/marshall_islands.py b/calendra/oceania/marshall_islands.py similarity index 100% rename from workalendar/oceania/marshall_islands.py rename to calendra/oceania/marshall_islands.py diff --git a/calendra/oceania/new_zealand.py b/calendra/oceania/new_zealand.py new file mode 100644 index 00000000..43163d59 --- /dev/null +++ b/calendra/oceania/new_zealand.py @@ -0,0 +1,46 @@ +from datetime import date + +from dateutil import relativedelta as rd + +from ..core import WesternCalendar, ChristianMixin +from ..core import Holiday +from ..registry_tools import iso_register + + +@iso_register("NZ") +class NewZealand(WesternCalendar, ChristianMixin): + "New Zealand" + include_good_friday = True + include_easter_monday = True + include_boxing_day = True + shift_new_years_day = True + + FIXED_HOLIDAYS = WesternCalendar.FIXED_HOLIDAYS + ( + Holiday(date(2000, 2, 6), "Waitangi Day"), + Holiday(date(2000, 4, 25), "ANZAC Day"), + ) + + def get_queens_birthday(self, year): + return Holiday( + date(year, 6, 1) + rd.relativedelta(weekday=rd.MO(1)), + "Queen's Birthday", + indication="First Monday in June", + ) + + def get_labour_day(self, year): + return Holiday( + date(year, 10, 1) + rd.relativedelta(weekday=rd.MO(4)), + "Labour Day", + indication="Fourth Monday in October", + ) + + def get_variable_days(self, year): + # usual variable days + days = super().get_variable_days(year) + days.append(Holiday( + date(year, 1, 2), + "Day after New Year's Day", + )) + days.append(self.get_queens_birthday(year)) + days.append(self.get_labour_day(year)) + return days diff --git a/workalendar/precomputed_astronomy.py b/calendra/precomputed_astronomy.py similarity index 100% rename from workalendar/precomputed_astronomy.py rename to calendra/precomputed_astronomy.py diff --git a/workalendar/registry.py b/calendra/registry.py similarity index 86% rename from workalendar/registry.py rename to calendra/registry.py index dd6d8895..6d6baa1d 100644 --- a/workalendar/registry.py +++ b/calendra/registry.py @@ -28,11 +28,11 @@ class IsoRegistry: 'oceania', ) - def __init__(self, load_standard_modules=True): + def __init__(self, load_standard_modules=False): self.region_registry = dict() if load_standard_modules: for module_name in self.STANDARD_MODULES: - module = f'workalendar.{module_name}' + module = f'calendra.{module_name}' all_classes = getattr(import_module(module), '__all__') self.load_module_from_items(module, all_classes) @@ -77,9 +77,9 @@ def get_subregions(self, iso_code): >>> registry = IsoRegistry() >>> # assuming calendars registered are: DE, DE-HH, DE-BE - >>> registry.get_subregions('DE') - {'DE-HH': , - 'DE-BE': } + >>> registry.get_subregions('DE') # doctest: +SKIP + {'DE-HH': , + 'DE-BE': } :rtype dict :return dict where keys are ISO codes strings and values are calendar classes @@ -123,3 +123,14 @@ def get_calendars(self, region_codes=None, include_subregions=False): registry = IsoRegistry() + +# Europe Countries +from calendra.europe import * # noqa +# United States of America +from calendra.usa import * # noqa +# American continent outside of USA +from calendra.america import * # noqa +# African continent +from calendra.africa import * # noqa +from calendra.asia import * # noqa +from calendra.oceania import * # noqa diff --git a/workalendar/registry_tools.py b/calendra/registry_tools.py similarity index 58% rename from workalendar/registry_tools.py rename to calendra/registry_tools.py index 614ccddf..1f47496e 100644 --- a/workalendar/registry_tools.py +++ b/calendra/registry_tools.py @@ -9,16 +9,20 @@ def iso_register(iso_code): Registered country must set class variables ``iso`` using this decorator. - >>> from workalendar.core import Calendar + >>> from calendra.core import Calendar + >>> from calendra.registry import registry + >>> from calendra.registry_tools import iso_register >>> @iso_register('MC-MR') - >>> class MyRegion(Calendar): - >>> 'My Region' + ... class MyRegion(Calendar): + ... 'My Region' Region calendar is then retrievable from registry: >>> calendar = registry.get('MC-MR') """ + def wrapper(cls): - cls.__iso_code = (iso_code, cls.__name__) + from calendra.registry import registry + registry.register(iso_code, cls) return cls return wrapper diff --git a/workalendar/skyfield_astronomy.py b/calendra/skyfield_astronomy.py similarity index 100% rename from workalendar/skyfield_astronomy.py rename to calendra/skyfield_astronomy.py diff --git a/workalendar/solar_terms.json.gz b/calendra/solar_terms.json.gz similarity index 100% rename from workalendar/solar_terms.json.gz rename to calendra/solar_terms.json.gz diff --git a/workalendar/tests/__init__.py b/calendra/tests/__init__.py similarity index 95% rename from workalendar/tests/__init__.py rename to calendra/tests/__init__.py index 7c77580f..29cae303 100644 --- a/workalendar/tests/__init__.py +++ b/calendra/tests/__init__.py @@ -7,6 +7,7 @@ from platform import system from freezegun import freeze_time +import pytest from ..core import Calendar from .. import __version__ @@ -48,6 +49,9 @@ def test_january_1st(self): else: self.assertNotIn(date(self.year, 1, 1), holidays) + @pytest.mark.xfail( + "platform.system() == 'Windows'", + reason="https://github.com/peopledoc/workalendar/issues/607") def test_ical_export(self): """Check that an iCal file can be created according to iCal spec.""" class_name = self.cal_class.__name__ @@ -99,7 +103,7 @@ def test_ical_export(self): # check that UIDs are unique within the calendar assert len(uid_lines) == len(set(uid_lines)) # check that final year is included - assert remaining_lines[-3].startswith('UID:2020') + assert remaining_lines[-3].startswith(('UID:2020', 'UID:2021')) # check last few lines of file assert remaining_lines[-2] == 'END:VEVENT\n' assert remaining_lines[-1] == 'END:VCALENDAR\n' diff --git a/workalendar/tests/test_africa.py b/calendra/tests/test_africa.py similarity index 97% rename from workalendar/tests/test_africa.py rename to calendra/tests/test_africa.py index 71a36391..55fe7e6a 100644 --- a/workalendar/tests/test_africa.py +++ b/calendra/tests/test_africa.py @@ -62,6 +62,7 @@ def test_before_1910(self): def test_year_2013(self): holidays = self.cal.holidays_set(2013) + self.assertIn(date(2013, 1, 1), holidays) # new year self.assertIn(date(2013, 3, 21), holidays) # human rights day self.assertIn(date(2013, 3, 29), holidays) # good friday @@ -69,18 +70,28 @@ def test_year_2013(self): self.assertIn(date(2013, 4, 27), holidays) # freedom day self.assertIn(date(2013, 5, 1), holidays) # labour day self.assertIn(date(2013, 6, 16), holidays) # youth day - self.assertIn(date(2013, 6, 17), holidays) # youth day - shift + self.assertNotIn(date(2013, 6, 17), holidays) # youth day - observed self.assertIn(date(2013, 8, 9), holidays) # national women's day self.assertIn(date(2013, 9, 24), holidays) # heritage day self.assertIn(date(2013, 12, 16), holidays) # day of reconciliation self.assertIn(date(2013, 12, 25), holidays) # christmas self.assertIn(date(2013, 12, 26), holidays) # day of goodwill + # test that Youth Day is observed on 17-Jun and is not a working day + observed = set(map(self.cal.get_observed_date, holidays)) + self.assertIn(date(2013, 6, 17), observed) + self.assertFalse(self.cal.is_working_day(date(2013, 6, 17))) + def test_year_2014(self): # test shifting holidays = self.cal.holidays_set(2014) self.assertIn(date(2014, 4, 27), holidays) # freedom day - self.assertIn(date(2014, 4, 28), holidays) # freedom day sub + self.assertNotIn(date(2014, 4, 28), holidays) # freedom day - observ + + # test that Freedom Day is observed on 28-Apr and is not a working day + observed = set(map(self.cal.get_observed_date, holidays)) + self.assertIn(date(2014, 4, 28), observed) + self.assertFalse(self.cal.is_working_day(date(2014, 4, 28))) def test_easter_monday(self): # 1910-1979, Easter Monday label was "Easter Monday" diff --git a/workalendar/tests/test_america.py b/calendra/tests/test_america.py similarity index 97% rename from workalendar/tests/test_america.py rename to calendra/tests/test_america.py index 4d6d0c44..69a30bfb 100644 --- a/workalendar/tests/test_america.py +++ b/calendra/tests/test_america.py @@ -432,24 +432,26 @@ def test_holidays_2013(self): self.assertIn(date(2013, 12, 25), holidays) # XMas def test_shift_to_monday(self): - holidays = self.cal.holidays_set(2017) + observed = set(map(self.cal.get_observed_date, self.cal.holidays_set(2017))) # New year on Sunday -> shift - self.assertIn(date(2017, 1, 2), holidays) - holidays = self.cal.holidays_set(2016) + assert date(2017, 1, 2) in observed + observed = set(map(self.cal.get_observed_date, self.cal.holidays_set(2016))) # XMas on sunday -> shift to monday - self.assertIn(date(2016, 12, 26), holidays) + assert date(2016, 12, 26) in observed # Same for Labour day - self.assertIn(date(2016, 5, 2), holidays) + assert date(2016, 5, 2) in observed def test_shift_to_friday(self): - holidays = self.cal.holidays_set(2021) + holidays = self.cal.holidays_set(2021) | self.cal.holidays_set(2022) + observed = set(map(self.cal.get_observed_date, holidays)) # January 1st 2022 is a saturday, so we shift to friday - self.assertIn(date(2021, 12, 31), holidays) + assert date(2021, 12, 31) in observed # Same for Labour day - self.assertIn(date(2021, 4, 30), holidays) + assert date(2021, 4, 30) in observed holidays = self.cal.holidays_set(2021) + observed = set(map(self.cal.get_observed_date, holidays)) # December 25th, 2022 is a saturday, so we shift to friday - self.assertIn(date(2021, 12, 24), holidays) + assert date(2021, 12, 24) in observed class PanamaTest(GenericCalendarTest): diff --git a/workalendar/tests/test_asia.py b/calendra/tests/test_asia.py similarity index 97% rename from workalendar/tests/test_asia.py rename to calendra/tests/test_asia.py index 884425a6..4f173fa2 100644 --- a/workalendar/tests/test_asia.py +++ b/calendra/tests/test_asia.py @@ -178,7 +178,8 @@ def test_year_2010(self): self.assertIn(date(2010, 10, 1), holidays) # National Day self.assertIn(date(2010, 10, 16), holidays) # Chung Yeung Festival self.assertIn(date(2010, 12, 25), holidays) # Christmas Day - self.assertIn(date(2010, 12, 27), holidays) # Boxing Day (shifted) + observed = set(map(self.cal.get_observed_date, holidays)) + self.assertIn(date(2010, 12, 27), observed) # Boxing Day (shifted) def test_year_2013(self): # https://www.gov.hk/en/about/abouthk/holiday/2013.htm @@ -197,7 +198,8 @@ def test_year_2013(self): self.assertIn(date(2013, 7, 1), holidays) # HK SAR Establishment Day self.assertIn(date(2013, 9, 20), holidays) # Day after Mid-Autumn self.assertIn(date(2013, 10, 1), holidays) # National Day - self.assertIn(date(2013, 10, 14), holidays) # Chung Yeung Festival + observed = set(map(self.cal.get_observed_date, holidays)) + self.assertIn(date(2013, 10, 14), observed) # Chung Yeung Festival self.assertIn(date(2013, 12, 25), holidays) # Christmas Day self.assertIn(date(2013, 12, 26), holidays) # Boxing Day @@ -213,13 +215,14 @@ def test_year_2016(self): self.assertIn(date(2016, 3, 28), holidays) # Easter Monday self.assertIn(date(2016, 4, 4), holidays) # Ching Ming self.assertIn(date(2016, 5, 1), holidays) # Labour Day (SUN) - self.assertIn(date(2016, 5, 2), holidays) # Labour Day (shifted) + observed = set(map(self.cal.get_observed_date, holidays)) + self.assertIn(date(2016, 5, 2), observed) # Labour Day (shifted) self.assertIn(date(2016, 5, 14), holidays) # Buddha's Birthday self.assertIn(date(2016, 6, 9), holidays) # Tuen Ng Festival self.assertIn(date(2016, 7, 1), holidays) # HK SAR Establishment Day self.assertIn(date(2016, 9, 16), holidays) # Day after Mid-Autumn self.assertIn(date(2016, 10, 1), holidays) # National Day - self.assertIn(date(2016, 10, 10), holidays) # Chung Yeung Festival + self.assertIn(date(2016, 10, 10), observed) # Chung Yeung Festival self.assertIn(date(2016, 12, 25), holidays) # Christmas Day self.assertIn(date(2016, 12, 26), holidays) # Christmas + Boxing Day self.assertIn(date(2016, 12, 27), holidays) # Second weekday @@ -227,7 +230,8 @@ def test_year_2016(self): def test_year_2017(self): # https://www.gov.hk/en/about/abouthk/holiday/2017.htm holidays = self.cal.holidays_set(2017) - self.assertIn(date(2017, 1, 2), holidays) # New Year (shifted) + observed = set(map(self.cal.get_observed_date, holidays)) + self.assertIn(date(2017, 1, 2), observed) # New Year (shifted) self.assertIn(date(2017, 1, 28), holidays) # Chinese new year self.assertIn(date(2017, 1, 30), holidays) # Chinese new year self.assertIn(date(2017, 1, 31), holidays) # Chinese new year @@ -239,7 +243,7 @@ def test_year_2017(self): self.assertIn(date(2017, 5, 3), holidays) # Buddha's Birthday self.assertIn(date(2017, 5, 30), holidays) # Tuen Ng Festival self.assertIn(date(2017, 7, 1), holidays) # HK SAR Establishment Day - self.assertIn(date(2017, 10, 2), holidays) # National Day (shifted) + self.assertIn(date(2017, 10, 2), observed) # National Day (shifted) self.assertIn(date(2017, 10, 5), holidays) # Day after Mid-Autumn self.assertIn(date(2017, 10, 28), holidays) # Chung Yeung Festival self.assertIn(date(2017, 12, 25), holidays) # Christmas Day @@ -279,7 +283,8 @@ def test_holidays_2020(self): self.assertIn(date(2020, 7, 1), holidays) # HK SAR Establishment Day self.assertIn(date(2020, 10, 1), holidays) # National Day self.assertIn(date(2020, 10, 2), holidays) # Day after Mid-Autumn - self.assertIn(date(2020, 10, 26), holidays) # Chung Yeung Festival + observed = set(map(self.cal.get_observed_date, holidays)) + self.assertIn(date(2020, 10, 26), observed) # Chung Yeung Festival self.assertIn(date(2020, 12, 25), holidays) # Christmas Day self.assertIn(date(2020, 12, 26), holidays) # Boxing Day # Special: Boxing day is not shifted, because it's not a SUN @@ -292,7 +297,8 @@ def test_holidays_2021(self): self.assertIn(date(2021, 2, 12), holidays) # Chinese new year self.assertIn(date(2021, 2, 13), holidays) # Chinese new year self.assertIn(date(2021, 2, 14), holidays) # Chinese new year (SUN) - self.assertIn(date(2021, 2, 15), holidays) # Chinese new year + observed = set(map(self.cal.get_observed_date, holidays)) + self.assertIn(date(2021, 2, 15), observed) # Chinese new year self.assertIn(date(2021, 4, 2), holidays) # Good Friday self.assertIn(date(2021, 4, 3), holidays) # Day after Good Friday self.assertIn(date(2021, 4, 4), holidays) # Ching Ming @@ -304,9 +310,9 @@ def test_holidays_2021(self): self.assertIn(date(2021, 7, 1), holidays) # HK SAR Establishment Day self.assertIn(date(2021, 9, 22), holidays) # Day after Mid-Autumn self.assertIn(date(2021, 10, 1), holidays) # National Day - self.assertIn(date(2021, 10, 14), holidays) # Chung Yeung Festival + self.assertIn(date(2021, 10, 14), observed) # Chung Yeung Festival self.assertIn(date(2021, 12, 25), holidays) # Christmas Day - self.assertIn(date(2021, 12, 27), holidays) # First weekday after Xmas + self.assertIn(date(2021, 12, 27), observed) # First weekday after Xmas def test_are_saturdays_working_days(self): # Let's start with february 6th. @@ -560,7 +566,8 @@ def test_year_2013(self): self.assertIn(date(2013, 8, 9), holidays) # National Day self.assertIn(date(2013, 10, 15), holidays) # Hari Raya Haji self.assertIn(date(2013, 11, 3), holidays) # Deepavali - self.assertIn(date(2013, 11, 4), holidays) # Deepavali shift + observed = set(map(self.cal.get_observed_date, holidays)) + self.assertIn(date(2013, 11, 4), observed) # Deepavali shift self.assertIn(date(2013, 12, 25), holidays) # Christmas Day def test_year_2018(self): @@ -583,7 +590,8 @@ def test_fixed_holiday_shift(self): # Labour Day (sunday) self.assertIn(date(2016, 5, 1), holidays) # Shifted day (Monday) - self.assertIn(date(2016, 5, 2), holidays) + observed = set(map(self.cal.get_observed_date, holidays)) + self.assertIn(date(2016, 5, 2), observed) def test_deepavali(self): # At the moment, we have values for deepavali only until year 2021 diff --git a/calendra/tests/test_astronomy.py b/calendra/tests/test_astronomy.py new file mode 100644 index 00000000..c7107814 --- /dev/null +++ b/calendra/tests/test_astronomy.py @@ -0,0 +1,98 @@ +import pytest +from datetime import date +from math import pi +from unittest.mock import patch + +from skyfield.api import Loader +from skyfield_data import get_skyfield_data_path + +from ..skyfield_astronomy import ( + calculate_equinoxes, + solar_term, + newton_angle_function, +) + + +def test_calculate_some_equinoxes(): + assert calculate_equinoxes(2010) == (date(2010, 3, 20), date(2010, 9, 23)) + assert calculate_equinoxes(2010, 'Asia/Taipei') == ( + date(2010, 3, 21), date(2010, 9, 23) + ) + assert calculate_equinoxes(2013) == (date(2013, 3, 20), date(2013, 9, 22)) + assert calculate_equinoxes(2014) == (date(2014, 3, 20), date(2014, 9, 23)) + assert calculate_equinoxes(2020) == (date(2020, 3, 20), date(2020, 9, 22)) + + +def test_qingming_festivals(): + assert solar_term(2001, 15) == date(2001, 4, 4) + assert solar_term(2001, 15, 'Asia/Taipei') == date(2001, 4, 5) + assert solar_term(2011, 15) == date(2011, 4, 5) + assert solar_term(2014, 15) == date(2014, 4, 4) + assert solar_term(2016, 15) == date(2016, 4, 4) + assert solar_term(2017, 15) == date(2017, 4, 4) + + +def test_qingming_festivals_hk(): + assert solar_term(2018, 15, 'Asia/Hong_Kong') == date(2018, 4, 5) + assert solar_term(2019, 15, 'Asia/Hong_Kong') == date(2019, 4, 5) + assert solar_term(2020, 15, 'Asia/Hong_Kong') == date(2020, 4, 4) + assert solar_term(2021, 15, 'Asia/Hong_Kong') == date(2021, 4, 4) + + +@pytest.fixture(scope='session') +def params_newton_angle(): + """ + Session-scoped fixture to "cache" the newton angle func parameters + """ + load = Loader(get_skyfield_data_path()) + ts = load.timescale() + planets = load('de421.bsp') + earth = planets['earth'] + sun = planets['sun'] + + jan_first = ts.utc(date(2021, 1, 1)) + t0 = ts.tt_jd(jan_first.tt).tt + return t0, ts, earth, sun + + +def test_newton_angle_function_normal_range(params_newton_angle): + """ + Test the newton angle func when the longitude is in the range [-pi, +pi]. + """ + t0, ts, earth, sun = params_newton_angle + + with patch('calendra.skyfield_astronomy.get_current_longitude') as patched: + patched.return_value = pi + assert newton_angle_function(t0, ts, 0, earth, sun) == -pi + + +def test_newton_angle_function_above_pi(params_newton_angle): + """ + Test the newton angle function when the longitude is > +pi. + + This should not happen, but it was implemented in the function, so in order + to make sure that the resulting angle is always in the range [-pi, +pi], + we added these tests with those out of range values. + """ + t0, ts, earth, sun = params_newton_angle + + with patch('calendra.skyfield_astronomy.get_current_longitude') as patched: + patched.return_value = pi + 0.1 + expected = pytest.approx(-.1) + assert newton_angle_function(t0, ts, 0, earth, sun) == expected + + +def test_newton_angle_function_below_minus_pi(params_newton_angle): + """ + Test the newton angle function when the longitude is < -pi. + + This should not happen, but it was implemented in the function, so in order + to make sure that the resulting angle is always in the range [-pi, +pi], + we added these tests with those out of range values. + """ + t0, ts, earth, sun = params_newton_angle + + with patch('calendra.skyfield_astronomy.get_current_longitude') as patched: + patched.return_value = -pi - 0.1 + expected = pytest.approx(.1) + assert newton_angle_function(t0, ts, 0, earth, sun) == expected diff --git a/workalendar/tests/test_brazil.py b/calendra/tests/test_brazil.py similarity index 100% rename from workalendar/tests/test_brazil.py rename to calendra/tests/test_brazil.py diff --git a/workalendar/tests/test_canada.py b/calendra/tests/test_canada.py similarity index 88% rename from workalendar/tests/test_canada.py rename to calendra/tests/test_canada.py index 328649d1..0985873f 100644 --- a/workalendar/tests/test_canada.py +++ b/calendra/tests/test_canada.py @@ -13,15 +13,17 @@ class CanadaTest(GenericCalendarTest): def test_holidays_2011(self): holidays = self.cal.holidays_set(2011) - self.assertIn(date(2011, 1, 3), holidays) + observed = set(map(self.cal.get_observed_date, holidays)) + self.assertIn(date(2011, 1, 3), observed) self.assertIn(date(2011, 7, 1), holidays) self.assertIn(date(2011, 9, 5), holidays) self.assertIn(date(2011, 12, 26), holidays) def test_holidays_2012(self): holidays = self.cal.holidays_set(2012) - self.assertIn(date(2012, 1, 2), holidays) # New years shift - self.assertIn(date(2012, 7, 2), holidays) # Canada day shift + observed = set(map(self.cal.get_observed_date, holidays)) + self.assertIn(date(2012, 1, 2), observed) # New years shift + self.assertIn(date(2012, 7, 2), observed) # Canada day shift self.assertIn(date(2012, 9, 3), holidays) # Labour day self.assertIn(date(2012, 12, 25), holidays) @@ -36,7 +38,8 @@ def test_holidays_2013(self): def test_holidays_2017(self): holidays = self.cal.holidays_set(2017) - self.assertIn(date(2017, 1, 2), holidays) + observed = set(map(self.cal.get_observed_date, holidays)) + self.assertIn(date(2017, 1, 2), observed) class OntarioTest(GenericCalendarTest): @@ -49,7 +52,8 @@ def test_holidays_2010(self): def test_holidays_2011(self): holidays = self.cal.holidays_set(2011) - self.assertIn(date(2011, 1, 3), holidays) + observed = set(map(self.cal.get_observed_date, holidays)) + self.assertIn(date(2011, 1, 3), observed) self.assertIn(date(2011, 2, 21), holidays) # Family Day Ontario self.assertIn(date(2011, 4, 22), holidays) # Good Friday self.assertNotIn(date(2011, 4, 25), holidays) # Easter Monday @@ -63,7 +67,8 @@ def test_holidays_2011(self): def test_holidays_2012(self): holidays = self.cal.holidays_set(2012) - self.assertIn(date(2012, 1, 2), holidays) + observed = set(map(self.cal.get_observed_date, holidays)) + self.assertIn(date(2012, 1, 2), observed) self.assertIn(date(2012, 2, 20), holidays) # Family Day Ontario self.assertIn(date(2012, 4, 6), holidays) # Good Friday self.assertNotIn(date(2012, 4, 9), holidays) # Easter Monday @@ -81,7 +86,8 @@ class QuebecTest(GenericCalendarTest): def test_holidays_2012(self): holidays = self.cal.holidays_set(2012) - self.assertIn(date(2012, 1, 2), holidays) + observed = set(map(self.cal.get_observed_date, holidays)) + self.assertIn(date(2012, 1, 2), observed) self.assertNotIn(date(2012, 4, 6), holidays) # Good Friday self.assertIn(date(2012, 4, 9), holidays) # Easter Monday self.assertIn(date(2012, 5, 21), holidays) # Victoria Day @@ -97,7 +103,8 @@ class BritishColumbiaTest(GenericCalendarTest): def test_holidays_2012(self): holidays = self.cal.holidays_set(2012) - self.assertIn(date(2012, 1, 2), holidays) + observed = set(map(self.cal.get_observed_date, holidays)) + self.assertIn(date(2012, 1, 2), observed) # Family Day BC was not set in 2012 self.assertNotIn(date(2012, 2, 13), holidays) @@ -132,7 +139,8 @@ class AlbertaTest(GenericCalendarTest): def test_holidays_2012(self): holidays = self.cal.holidays_set(2012) - self.assertIn(date(2012, 1, 2), holidays) + observed = set(map(self.cal.get_observed_date, holidays)) + self.assertIn(date(2012, 1, 2), observed) self.assertIn(date(2012, 2, 20), holidays) # Family Day self.assertIn(date(2012, 4, 6), holidays) # Good Friday self.assertNotIn(date(2012, 4, 9), holidays) # Easter Monday @@ -150,7 +158,8 @@ class SaskatchewanTest(GenericCalendarTest): def test_holidays_2012(self): holidays = self.cal.holidays_set(2012) - self.assertIn(date(2012, 1, 2), holidays) + observed = set(map(self.cal.get_observed_date, holidays)) + self.assertIn(date(2012, 1, 2), observed) self.assertIn(date(2012, 2, 20), holidays) # Family Day self.assertIn(date(2012, 4, 6), holidays) # Good Friday self.assertNotIn(date(2012, 4, 9), holidays) # Easter Monday @@ -169,7 +178,8 @@ class ManitobaTest(GenericCalendarTest): def test_holidays_2012(self): holidays = self.cal.holidays_set(2012) - self.assertIn(date(2012, 1, 2), holidays) + observed = set(map(self.cal.get_observed_date, holidays)) + self.assertIn(date(2012, 1, 2), observed) self.assertIn(date(2012, 2, 20), holidays) # Louis Riel Day self.assertIn(date(2012, 4, 6), holidays) # Good Friday self.assertNotIn(date(2012, 4, 9), holidays) # Easter Monday @@ -189,7 +199,8 @@ class NewBrunswickTest(GenericCalendarTest): def test_holidays_2012(self): holidays = self.cal.holidays_set(2012) - self.assertIn(date(2012, 1, 2), holidays) + observed = set(map(self.cal.get_observed_date, holidays)) + self.assertIn(date(2012, 1, 2), observed) self.assertNotIn(date(2012, 2, 20), holidays) # Family Day self.assertIn(date(2012, 4, 6), holidays) # Good Friday self.assertNotIn(date(2012, 4, 9), holidays) # Easter Monday @@ -209,7 +220,8 @@ class NovaScotiaTest(GenericCalendarTest): def test_holidays_2012(self): holidays = self.cal.holidays_set(2012) - self.assertIn(date(2012, 1, 2), holidays) + observed = set(map(self.cal.get_observed_date, holidays)) + self.assertIn(date(2012, 1, 2), observed) self.assertNotIn(date(2012, 2, 20), holidays) # Family Day self.assertIn(date(2012, 4, 6), holidays) # Good Friday self.assertNotIn(date(2012, 4, 9), holidays) # Easter Monday @@ -233,7 +245,8 @@ class PrinceEdwardIslandTest(GenericCalendarTest): def test_holidays_2012(self): holidays = self.cal.holidays_set(2012) - self.assertIn(date(2012, 1, 2), holidays) + observed = set(map(self.cal.get_observed_date, holidays)) + self.assertIn(date(2012, 1, 2), observed) self.assertIn(date(2012, 2, 20), holidays) # Islander Day self.assertIn(date(2012, 4, 6), holidays) # Good Friday self.assertNotIn(date(2012, 4, 9), holidays) # Easter Monday @@ -266,7 +279,8 @@ class YukonTest(GenericCalendarTest): def test_holidays_2012(self): holidays = self.cal.holidays_set(2012) - self.assertIn(date(2012, 1, 2), holidays) + observed = set(map(self.cal.get_observed_date, holidays)) + self.assertIn(date(2012, 1, 2), observed) self.assertNotIn(date(2012, 2, 20), holidays) # Family Day self.assertIn(date(2012, 4, 6), holidays) # Good Friday self.assertNotIn(date(2012, 4, 9), holidays) # Easter Monday @@ -287,7 +301,8 @@ class NorthwestTerritoriesTest(GenericCalendarTest): def test_holidays_2012(self): holidays = self.cal.holidays_set(2012) - self.assertIn(date(2012, 1, 2), holidays) + observed = set(map(self.cal.get_observed_date, holidays)) + self.assertIn(date(2012, 1, 2), observed) self.assertNotIn(date(2012, 2, 20), holidays) # Family Day self.assertIn(date(2012, 4, 6), holidays) # Good Friday self.assertNotIn(date(2012, 4, 9), holidays) # Easter Monday @@ -308,7 +323,8 @@ class NunavutTests(GenericCalendarTest): def test_holidays_2012(self): holidays = self.cal.holidays_set(2012) - self.assertIn(date(2012, 1, 2), holidays) + observed = set(map(self.cal.get_observed_date, holidays)) + self.assertIn(date(2012, 1, 2), observed) self.assertNotIn(date(2012, 2, 20), holidays) # Family Day self.assertIn(date(2012, 4, 6), holidays) # Good Friday self.assertNotIn(date(2012, 4, 9), holidays) # Easter Monday diff --git a/workalendar/tests/test_core.py b/calendra/tests/test_core.py similarity index 95% rename from workalendar/tests/test_core.py rename to calendra/tests/test_core.py index d28d2d61..b5bb83d8 100644 --- a/workalendar/tests/test_core.py +++ b/calendra/tests/test_core.py @@ -1,11 +1,13 @@ -from unittest.mock import patch from datetime import date from datetime import datetime from unittest import TestCase +from unittest.mock import patch +import dateutil.relativedelta as rd import pandas from . import CoreCalendarTest, GenericCalendarTest +from ..core import Holiday from ..core import ( MON, TUE, THU, FRI, WED, SAT, SUN, ISO_TUE, ISO_FRI, @@ -125,7 +127,7 @@ def test_get_iso_week_date(self): ) # Remove this test when dropping support for Python 3.7 - @patch('workalendar.core.sys') + @patch('calendra.core.sys') def test_get_iso_week_date_patched(self, mock_sys): # The Python 3.6-3.7 backport should always work mock_sys.version_info = (3, 6, 0) @@ -147,10 +149,10 @@ def test_lunar_new_year(self): class MockCalendar(Calendar): def holidays(self, year=None): - return tuple(( - (date(year, 12, 25), 'Christmas'), - (date(year, 1, 1), 'New year'), - )) + return ( + Holiday(date(year, 12, 25), 'Christmas'), + Holiday(date(year, 1, 1), 'New year'), + ) def get_weekend_days(self): return [] # no week-end, yes, it's sad @@ -269,6 +271,46 @@ def test_add_working_days_backwards(self): ) +class SimpleObservanceCalendar(Calendar): + """ + A simple calendar with a couple of holidays with typical observance rules: + If a holiday falls on a weekend, then its observance is shifted to a + nearby weekday. + """ + include_new_years_day = False + + FIXED_HOLIDAYS = ( + Holiday( + date(2000, 12, 24), 'Christmas Eve', indication='December 24th', + observance_shift=dict(weekday=rd.FR(-1)), + ), + Holiday(date(2000, 12, 25), 'Christmas', indication='December 25th'), + ) + + def get_weekend_days(self): + return SAT, SUN + + +class ObservanceCalendarTest(CoreCalendarTest): + """ + A simple calendar with days shifted for observance. + """ + cal_class = SimpleObservanceCalendar + + def test_observance(self): + """ + Each Holiday returned by the calendar should be aware of its indicated + date and observance date. + """ + holidays = list(self.cal.holidays(2011)) + assert len(holidays) == 2 + xmas_eve, xmas_day = holidays + assert xmas_eve == date(2011, 12, 24) + assert self.cal.get_observed_date(xmas_eve) == date(2011, 12, 23) + assert xmas_day == date(2011, 12, 25) + assert self.cal.get_observed_date(xmas_day) == date(2011, 12, 26) + + class IslamicMixinTest(CoreCalendarTest): cal_class = IslamicMixin diff --git a/workalendar/tests/test_europe.py b/calendra/tests/test_europe.py similarity index 97% rename from workalendar/tests/test_europe.py rename to calendra/tests/test_europe.py index 7e90a590..8217e1a5 100644 --- a/workalendar/tests/test_europe.py +++ b/calendra/tests/test_europe.py @@ -4,6 +4,7 @@ from . import GenericCalendarTest from ..core import daterange, ISO_SAT +from ..core import Holiday from ..europe import ( Austria, Bulgaria, @@ -274,7 +275,7 @@ def test_holidays_2016(self): def test_holidays_2017(self): holidays = self.cal.holidays_set(2017) self.assertIn(date(2017, 1, 1), holidays) - self.assertIn(date(2017, 1, 2), holidays) # New Year boxing + self.assertNotIn(date(2017, 1, 2), holidays) # New Year boxing self.assertIn(date(2017, 1, 23), holidays) # National Heroes Day self.assertIn(date(2017, 3, 1), holidays) # Ash Wednesday self.assertIn(date(2017, 4, 14), holidays) # good friday @@ -567,8 +568,13 @@ def test_year_2015(self): self.assertIn(date(2015, 12, 31), holidays) # new year's eve def test_pentecost(self): - holidays = self.cal.holidays(2015) - self.assertIn((date(2015, 5, 24), 'Pentecost'), holidays) + "Pentecost is designated on May 24, 2015" + pentecost, = ( + holiday + for holiday in self.cal.holidays_set(2015) + if holiday.name == 'Pentecost' + ) + assert pentecost == date(2015, 5, 24) class FinlandTest(GenericCalendarTest): @@ -599,6 +605,15 @@ def test_year_2014(self): self.assertIn(date(2014, 6, 21), holidays) # midsummer day self.assertIn(date(2014, 11, 1), holidays) # all saints (special) + def test_holidays_not_shifted(self): + """ + Holidays should not be shifted for Finland. + """ + holidays = self.cal.holidays_set(2014) + observed = set(map(self.cal.get_observed_date, holidays)) + self.assertIn(date(2014, 12, 6), observed) + self.assertTrue(self.cal.is_working_day(date(2014, 12, 8))) + class FranceTest(GenericCalendarTest): @@ -936,7 +951,8 @@ def test_year_2013(self): def test_shift_2012(self): holidays = self.cal.holidays_set(2012) self.assertIn(date(2012, 1, 1), holidays) # new year day - self.assertIn(date(2012, 1, 2), holidays) # new year day shift + observed = set(map(self.cal.get_observed_date, holidays)) + self.assertIn(date(2012, 1, 2), observed) # new year day shift def test_shift_2011(self): holidays = self.cal.holidays_set(2011) @@ -1729,7 +1745,7 @@ def test_holidays_2021(self): (date(2021, 1, 5), "Fifth Day after New Year"), (date(2021, 1, 6), "Sixth Day after New Year"), (date(2021, 1, 7), "Christmas"), - (date(2021, 1, 8), "Eighth Day after New Year"), + (date(2021, 1, 8), "Eighth Day after New Year"), (date(2021, 2, 22), "Day Before Defendence of the Fatherland"), (date(2021, 2, 23), "Defendence of the Fatherland"), (date(2021, 3, 8), "International Women's Day"), @@ -1743,6 +1759,7 @@ def test_holidays_2021(self): (date(2021, 11, 5), "Day After Day of Unity"), (date(2021, 12, 31), "New Year's Eve") ] + holidays_2021 = [Holiday(*h) for h in holidays_2021] self.assertEqual(holidays, holidays_2021) def test_year_2021_extra_working_day(self): @@ -1810,7 +1827,8 @@ def test_year_2016(self): def test_year_2017(self): holidays = self.cal.holidays_set(2017) - self.assertIn(date(2017, 1, 2), holidays) # New Year's Day (postponed) + # New Year's observed + assert self.cal.is_observed_holiday(date(2017, 1, 2)) self.assertIn(date(2017, 1, 9), holidays) # Christmas Orthodox (moved) self.assertIn(date(2017, 12, 25), holidays) # Christmas Day @@ -1864,7 +1882,7 @@ def test_year_2011(self): def test_year_2012(self): holidays = self.cal.holidays_set(2012) self.assertIn(date(2012, 1, 1), holidays) # new year day - self.assertIn(date(2012, 1, 2), holidays) # new year shift + self.assertNotIn(date(2012, 1, 2), holidays) # new year shift self.assertIn(date(2012, 4, 6), holidays) # good friday self.assertIn(date(2012, 4, 8), holidays) # easter sunday self.assertIn(date(2012, 4, 9), holidays) # easter monday @@ -1890,25 +1908,32 @@ def test_year_2013(self): def test_shift_2012(self): holidays = self.cal.holidays_set(2012) self.assertIn(date(2012, 1, 1), holidays) # new year day - self.assertIn(date(2012, 1, 2), holidays) # new year day shift def test_shift_2011(self): holidays = self.cal.holidays_set(2011) - self.assertIn(date(2011, 12, 25), holidays) # Christmas it's sunday - self.assertIn(date(2011, 12, 26), holidays) # XMas day shift - self.assertIn(date(2011, 12, 27), holidays) # Boxing day shift + self.assertIn(date(2011, 12, 25), holidays) # XMas day indicated + self.assertIn(date(2011, 12, 26), holidays) # Boxing day + # XMas observed + assert self.cal.is_observed_holiday(date(2011, 12, 26)) + # Boxing observed + assert self.cal.is_observed_holiday(date(2011, 12, 27)) def test_shift_2015(self): - holidays = self.cal.holidays_set(2015) - self.assertIn(date(2015, 12, 25), holidays) # Christmas it's friday - self.assertIn(date(2015, 12, 26), holidays) # Boxing day it's saturday - self.assertIn(date(2015, 12, 28), holidays) # Boxing day shift + """ + Christmas is on a Friday and Boxing Day on a Saturday. Only Boxing Day + should be shifted. + """ + # XMas observed + assert self.cal.is_observed_holiday(date(2015, 12, 25)) + # Boxing observed + assert self.cal.is_observed_holiday(date(2015, 12, 28)) def test_shift_2016(self): holidays = self.cal.holidays_set(2016) + observed = set(map(self.cal.get_observed_date, holidays)) self.assertIn(date(2016, 12, 25), holidays) # Christmas - Sunday - self.assertIn(date(2016, 12, 26), holidays) # Boxing day - Monday - self.assertIn(date(2016, 12, 27), holidays) # Christmas - shift to Tue + self.assertIn(date(2016, 12, 26), observed) # Christmas - observed + self.assertIn(date(2016, 12, 27), observed) # Boxing day - observed def test_2020(self): holidays = self.cal.holidays_set(2020) @@ -1923,21 +1948,24 @@ def test_2020(self): self.assertIn(date(2020, 5, 25), holidays) # Spring Bank Holiday self.assertIn(date(2020, 8, 31), holidays) # Late Summer Bank Holiday self.assertIn(date(2020, 12, 25), holidays) # Christmas Day - self.assertIn(date(2020, 12, 26), holidays) # 'Boxing Day - self.assertIn(date(2020, 12, 28), holidays) # Boxing Day Shift + self.assertIn(date(2020, 12, 26), holidays) # Boxing Day + + # Boxing observed + assert self.cal.is_observed_holiday(date(2020, 12, 28)) # May the 8th is VE day holidays = self.cal.holidays(2020) holidays = dict(holidays) self.assertIn(date(2020, 5, 8), holidays) self.assertEqual( - holidays[date(2020, 5, 8)], "Early May bank holiday (VE day)" + holidays[date(2020, 5, 8)], "Early May Bank Holiday (VE day)" ) def test_2022(self): holidays = self.cal.holidays_set(2022) print(holidays) - self.assertIn(date(2022, 1, 3), holidays) # New Year + observed = set(map(self.cal.get_observed_date, holidays)) + self.assertIn(date(2022, 1, 3), observed) # New Year self.assertIn(date(2022, 4, 15), holidays) # Good Friday self.assertIn(date(2022, 4, 18), holidays) # Easter Monday self.assertIn(date(2022, 5, 2), holidays) # Early May Bank Holiday @@ -1946,8 +1974,8 @@ def test_2022(self): # Platinum Jubilee Bank Holiday self.assertIn(date(2022, 6, 3), holidays) self.assertIn(date(2022, 8, 29), holidays) # Summer bank holiday - self.assertIn(date(2022, 12, 26), holidays) # Boxing Day - self.assertIn(date(2022, 12, 27), holidays) # Christmas Day + self.assertIn(date(2022, 12, 26), observed) # Boxing Day + self.assertIn(date(2022, 12, 27), observed) # Christmas Day class UnitedKingdomNorthernIrelandTest(UnitedKingdomTest): @@ -2005,15 +2033,16 @@ def test_2021(self): def test_2022(self): holidays = self.cal.holidays_set(2022) - self.assertIn(date(2022, 1, 3), holidays) # New Year + observed = set(map(self.cal.get_observed_date, holidays)) + self.assertIn(date(2022, 1, 3), observed) # New Year self.assertIn(date(2022, 4, 15), holidays) # Good Friday self.assertIn(date(2022, 4, 18), holidays) # Easter Monday self.assertIn(date(2022, 5, 2), holidays) # May Day bank Holiday self.assertIn(date(2022, 5, 9), holidays) # Liberation Day self.assertIn(date(2022, 5, 30), holidays) # Spring Bank Holiday self.assertIn(date(2022, 8, 29), holidays) # Late Summer Bank Holiday - self.assertIn(date(2022, 12, 26), holidays) # 'Boxing Day - self.assertIn(date(2022, 12, 27), holidays) # Christmas Day + self.assertIn(date(2022, 12, 26), observed) # 'Boxing Day + self.assertIn(date(2022, 12, 27), observed) # Christmas Day class EuropeanCentralBankTest(GenericCalendarTest): diff --git a/workalendar/tests/test_germany.py b/calendra/tests/test_germany.py similarity index 100% rename from workalendar/tests/test_germany.py rename to calendra/tests/test_germany.py diff --git a/workalendar/tests/test_global_registry.py b/calendra/tests/test_global_registry.py similarity index 100% rename from workalendar/tests/test_global_registry.py rename to calendra/tests/test_global_registry.py diff --git a/workalendar/tests/test_ical_export.py b/calendra/tests/test_ical_export.py similarity index 100% rename from workalendar/tests/test_ical_export.py rename to calendra/tests/test_ical_export.py diff --git a/workalendar/tests/test_mozambique.py b/calendra/tests/test_mozambique.py similarity index 100% rename from workalendar/tests/test_mozambique.py rename to calendra/tests/test_mozambique.py diff --git a/workalendar/tests/test_oceania.py b/calendra/tests/test_oceania.py similarity index 94% rename from workalendar/tests/test_oceania.py rename to calendra/tests/test_oceania.py index 6cfd72d1..6278fdd7 100644 --- a/workalendar/tests/test_oceania.py +++ b/calendra/tests/test_oceania.py @@ -290,8 +290,8 @@ def test_new_year_shift_on_sunday(self): # New Years was on a SUN # Day After New Years is on the 2nd self.assertIn(date(2012, 1, 2), holidays) - # New Years Shift is on the 3rd - self.assertIn(date(2012, 1, 3), holidays) + # New Years observed on the 3rd + assert self.cal.is_observed_holiday(date(2012, 1, 3)) # No shift of a shift self.assertNotIn(date(2012, 1, 4), holidays) @@ -301,38 +301,40 @@ def test_new_year_shift_on_saturday(self): # The day after is still a holiday self.assertIn(date(2022, 1, 2), holidays) # The shift is on the MON, 3rd - self.assertIn(date(2022, 1, 3), holidays) + observed = set(map(self.cal.get_observed_date, holidays)) + self.assertIn(date(2022, 1, 3), observed) # And there's a shift of the shift - self.assertIn(date(2022, 1, 4), holidays) + self.assertIn(date(2022, 1, 4), observed) def test_anzac_shift(self): holidays = self.cal.holidays_set(2010) # 25th was a sunday # ANZAC Day is on 25th self.assertIn(date(2010, 4, 25), holidays) - # ANZAC Day Shift is on 26th - self.assertIn(date(2010, 4, 26), holidays) + # ANZAC Day observed on 26th + assert self.cal.is_observed_holiday(date(2010, 4, 26)) def test_waitangi_shift(self): holidays = self.cal.holidays_set(2016) # 6th was a saturday # Waitangi Day is on 6th self.assertIn(date(2016, 2, 6), holidays) - # Waitangi Day Shift is on 7th - self.assertIn(date(2016, 2, 8), holidays) + # Waitangi Day observed on 8th + assert self.cal.is_observed_holiday(date(2016, 2, 8)) def test_oceania_shift_2016(self): holidays = self.cal.holidays_set(2016) # Christmas day is on sunday in 2016 # Boxing day is on 26th self.assertIn(date(2016, 12, 26), holidays) - # Christmas day shift on 27th - self.assertIn(date(2016, 12, 27), holidays) + # Christmas day observed on 26th and boxing day on 27th + assert self.cal.is_observed_holiday(date(2016, 12, 26)) + assert self.cal.is_observed_holiday(date(2016, 12, 27)) def test_oceania_shift_2009(self): holidays = self.cal.holidays_set(2009) # Boxing day is on saturday in 2009 # Boxing day is on 26th self.assertIn(date(2009, 12, 26), holidays) - # Boxing day shift on 28th - self.assertIn(date(2009, 12, 28), holidays) + # Boxing day is observed on 28th + assert self.cal.is_observed_holiday(date(2009, 12, 28)) diff --git a/workalendar/tests/test_precomputed_astronomy.py b/calendra/tests/test_precomputed_astronomy.py similarity index 91% rename from workalendar/tests/test_precomputed_astronomy.py rename to calendra/tests/test_precomputed_astronomy.py index fa3e11ca..795bcc37 100644 --- a/workalendar/tests/test_precomputed_astronomy.py +++ b/calendra/tests/test_precomputed_astronomy.py @@ -31,11 +31,11 @@ def test_pre_computed_pathes(self): pathlib.Path(__file__).parent.parent / 'solar_terms.json.gz', ) - @patch('workalendar.skyfield_astronomy.solar_term') - @patch('workalendar.skyfield_astronomy.calculate_equinoxes') - @patch('workalendar.precomputed_astronomy.gzip.open') - @patch('workalendar.precomputed_astronomy.YEAR_INTERVAL', 1) - @patch('workalendar.precomputed_astronomy.TIME_ZONES', ('Europe/Paris',)) + @patch('calendra.skyfield_astronomy.solar_term') + @patch('calendra.skyfield_astronomy.calculate_equinoxes') + @patch('calendra.precomputed_astronomy.gzip.open') + @patch('calendra.precomputed_astronomy.YEAR_INTERVAL', 1) + @patch('calendra.precomputed_astronomy.TIME_ZONES', ('Europe/Paris',)) @freeze_time('2022-01-01') def test_create_astronomical_data(self, gzipopen, @@ -78,8 +78,8 @@ def test_create_astronomical_data(self, json.dumps(expected_solar_terms_dict).encode('utf-8') ) - @patch('workalendar.precomputed_astronomy.gzip.decompress') - @patch('workalendar.precomputed_astronomy.pre_computed_equinoxes_path') + @patch('calendra.precomputed_astronomy.gzip.decompress') + @patch('calendra.precomputed_astronomy.pre_computed_equinoxes_path') def test_calculate_equinoxes(self, pre_computed_equinoxes_path, decompress): @@ -113,8 +113,8 @@ def test_calculate_equinoxes(self, decompress.assert_called_once_with(sentinel.some_equinoxes_bytes) decompress.reset_mock() - @patch('workalendar.precomputed_astronomy.gzip.decompress') - @patch('workalendar.precomputed_astronomy.pre_computed_solar_terms_path') + @patch('calendra.precomputed_astronomy.gzip.decompress') + @patch('calendra.precomputed_astronomy.pre_computed_solar_terms_path') def test_sorted_term(self, pre_computed_solar_terms_path, decompress): diff --git a/workalendar/tests/test_registry.py b/calendra/tests/test_registry.py similarity index 100% rename from workalendar/tests/test_registry.py rename to calendra/tests/test_registry.py diff --git a/workalendar/tests/test_registry_africa.py b/calendra/tests/test_registry_africa.py similarity index 100% rename from workalendar/tests/test_registry_africa.py rename to calendra/tests/test_registry_africa.py diff --git a/workalendar/tests/test_registry_america.py b/calendra/tests/test_registry_america.py similarity index 100% rename from workalendar/tests/test_registry_america.py rename to calendra/tests/test_registry_america.py diff --git a/workalendar/tests/test_registry_asia.py b/calendra/tests/test_registry_asia.py similarity index 100% rename from workalendar/tests/test_registry_asia.py rename to calendra/tests/test_registry_asia.py diff --git a/workalendar/tests/test_registry_europe.py b/calendra/tests/test_registry_europe.py similarity index 98% rename from workalendar/tests/test_registry_europe.py rename to calendra/tests/test_registry_europe.py index 29f7c774..b831e294 100644 --- a/workalendar/tests/test_registry_europe.py +++ b/calendra/tests/test_registry_europe.py @@ -72,7 +72,7 @@ def test_europe(self): self.assertIn(Estonia, classes) self.assertIn(Denmark, classes) self.assertIn(Finland, classes) - self.assertIn(France, classes) + self.assertIn(France, classes) self.assertIn(Greece, classes) self.assertIn(Hungary, classes) self.assertIn(Iceland, classes) @@ -91,7 +91,7 @@ def test_europe(self): self.assertIn(Russia, classes) self.assertIn(Slovakia, classes) self.assertIn(Slovenia, classes) - self.assertIn(Spain, classes) + self.assertIn(Spain, classes) self.assertIn(Sweden, classes) self.assertIn(Switzerland, classes) self.assertIn(Vaud, classes) diff --git a/workalendar/tests/test_registry_oceania.py b/calendra/tests/test_registry_oceania.py similarity index 100% rename from workalendar/tests/test_registry_oceania.py rename to calendra/tests/test_registry_oceania.py diff --git a/workalendar/tests/test_registry_usa.py b/calendra/tests/test_registry_usa.py similarity index 100% rename from workalendar/tests/test_registry_usa.py rename to calendra/tests/test_registry_usa.py diff --git a/workalendar/tests/test_scotland.py b/calendra/tests/test_scotland.py similarity index 97% rename from workalendar/tests/test_scotland.py rename to calendra/tests/test_scotland.py index 3a4ed544..0e6c8306 100644 --- a/workalendar/tests/test_scotland.py +++ b/calendra/tests/test_scotland.py @@ -1,6 +1,7 @@ from datetime import date from unittest import TestCase -import warnings + +import pytest from . import GenericCalendarTest from ..europe import ( @@ -12,6 +13,9 @@ ) +pytestmark = pytest.mark.filterwarnings('ignore:::calendra.europe.scotland') + + class GoodFridayTestMixin: def test_good_friday(self): holidays = self.cal.holidays_set(2018) @@ -272,17 +276,8 @@ class ScotlandTest(GenericCalendarTest): cal_class = Scotland def test_init_warning(self): - warnings.simplefilter("always") - with warnings.catch_warnings(record=True) as w: - # Cause all warnings to always be triggered. - # Trigger a warning. + with pytest.warns(UserWarning, match="experimental"): self.cal_class() - # Verify some things - assert len(w) == 1 - assert issubclass(w[-1].category, UserWarning) - assert "experimental" in str(w[-1].message) - # Back to normal filtering - warnings.simplefilter("ignore") def test_year_2018(self): holidays = self.cal.holidays_set(2018) diff --git a/workalendar/tests/test_skyfield_astronomy.py b/calendra/tests/test_skyfield_astronomy.py similarity index 91% rename from workalendar/tests/test_skyfield_astronomy.py rename to calendra/tests/test_skyfield_astronomy.py index 47fcb927..c7107814 100644 --- a/workalendar/tests/test_skyfield_astronomy.py +++ b/calendra/tests/test_skyfield_astronomy.py @@ -61,8 +61,7 @@ def test_newton_angle_function_normal_range(params_newton_angle): """ t0, ts, earth, sun = params_newton_angle - with patch('workalendar.skyfield_astronomy.get_current_longitude') \ - as patched: + with patch('calendra.skyfield_astronomy.get_current_longitude') as patched: patched.return_value = pi assert newton_angle_function(t0, ts, 0, earth, sun) == -pi @@ -77,8 +76,7 @@ def test_newton_angle_function_above_pi(params_newton_angle): """ t0, ts, earth, sun = params_newton_angle - with patch('workalendar.skyfield_astronomy.get_current_longitude') \ - as patched: + with patch('calendra.skyfield_astronomy.get_current_longitude') as patched: patched.return_value = pi + 0.1 expected = pytest.approx(-.1) assert newton_angle_function(t0, ts, 0, earth, sun) == expected @@ -94,8 +92,7 @@ def test_newton_angle_function_below_minus_pi(params_newton_angle): """ t0, ts, earth, sun = params_newton_angle - with patch('workalendar.skyfield_astronomy.get_current_longitude') \ - as patched: + with patch('calendra.skyfield_astronomy.get_current_longitude') as patched: patched.return_value = -pi - 0.1 expected = pytest.approx(.1) assert newton_angle_function(t0, ts, 0, earth, sun) == expected diff --git a/workalendar/tests/test_spain.py b/calendra/tests/test_spain.py similarity index 100% rename from workalendar/tests/test_spain.py rename to calendra/tests/test_spain.py diff --git a/workalendar/tests/test_switzerland.py b/calendra/tests/test_switzerland.py similarity index 100% rename from workalendar/tests/test_switzerland.py rename to calendra/tests/test_switzerland.py diff --git a/workalendar/tests/test_turkey.py b/calendra/tests/test_turkey.py similarity index 98% rename from workalendar/tests/test_turkey.py rename to calendra/tests/test_turkey.py index 36d58d1c..52921f7a 100644 --- a/workalendar/tests/test_turkey.py +++ b/calendra/tests/test_turkey.py @@ -10,7 +10,7 @@ class TurkeyTest(GenericCalendarTest): def test_year_new_year_shift(self): holidays = self.cal.holidays_set(2012) self.assertIn(date(2012, 1, 1), holidays) - self.assertIn(date(2012, 1, 2), holidays) + self.assertNotIn(date(2012, 1, 2), holidays) holidays = self.cal.holidays_set(2013) self.assertIn(date(2013, 1, 1), holidays) self.assertNotIn(date(2013, 1, 2), holidays) diff --git a/workalendar/tests/test_usa.py b/calendra/tests/test_usa.py similarity index 96% rename from workalendar/tests/test_usa.py rename to calendra/tests/test_usa.py index 12326889..e182d9ae 100644 --- a/workalendar/tests/test_usa.py +++ b/calendra/tests/test_usa.py @@ -1,6 +1,7 @@ from unittest import skip from datetime import date -import warnings + +import pytest from . import GenericCalendarTest from ..usa import ( @@ -78,6 +79,22 @@ def test_federal_year_2013(self): self.assertIn(date(2013, 11, 28), holidays) # Thanskgiving self.assertIn(date(2013, 12, 25), holidays) # Christmas + def test_independence_day_nearest_weekday(self): + """ + Independence Day should shift to the nearest weekday. + """ + holidays = self.cal.holidays_set(2010) + observed = set(map(self.cal.get_observed_date, holidays)) + self.assertIn(date(2010, 7, 5), observed) + + holidays = self.cal.holidays_set(2011) + observed = set(map(self.cal.get_observed_date, holidays)) + self.assertIn(date(2011, 7, 4), observed) + + holidays = self.cal.holidays_set(2015) + observed = set(map(self.cal.get_observed_date, holidays)) + self.assertIn(date(2015, 7, 3), observed) + def test_presidential_year(self): self.assertTrue(UnitedStates.is_presidential_year(2012)) self.assertFalse(UnitedStates.is_presidential_year(2013)) @@ -381,23 +398,26 @@ def test_state_year_2014(self): holidays = self.cal.holidays_set(2014) self.assertIn(date(2014, 3, 31), holidays) # Seward's Day self.assertIn(date(2014, 10, 18), holidays) # Alaska Day + observed = set(map(self.cal.get_observed_date, holidays)) # Alaska Day is on SAT, shift to FRI - self.assertIn(date(2014, 10, 17), holidays) + self.assertIn(date(2014, 10, 17), observed) def test_state_year_2015(self): holidays = self.cal.holidays_set(2015) self.assertIn(date(2015, 3, 30), holidays) # Seward's Day self.assertIn(date(2015, 10, 18), holidays) # Alaska Day + observed = set(map(self.cal.get_observed_date, holidays)) # Alaska day is on SUN: shifted to MON - self.assertIn(date(2015, 10, 19), holidays) + self.assertIn(date(2015, 10, 19), observed) def test_state_year_2017(self): holidays = self.cal.holidays_set(2017) self.assertIn(date(2017, 3, 27), holidays) # Seward's Day self.assertIn(date(2017, 10, 18), holidays) # Alaska Day + observed = set(map(self.cal.get_observed_date, holidays)) # Alaska day is on WED: no shift - self.assertNotIn(date(2017, 10, 19), holidays) - self.assertNotIn(date(2017, 10, 17), holidays) + self.assertNotIn(date(2017, 10, 19), observed) + self.assertNotIn(date(2017, 10, 17), observed) class ArizonaTest(UnitedStatesTest): @@ -430,7 +450,8 @@ def test_state_year_2015(self): def test_christmas_2016(self): holidays = self.cal.holidays_set(2016) self.assertIn(date(2016, 12, 24), holidays) # XMas Eve - self.assertIn(date(2016, 12, 23), holidays) # XMas Eve shifted + observed = set(map(self.cal.get_observed_date, holidays)) + self.assertIn(date(2016, 12, 23), observed) # XMas Eve shifted def test_president_day_label(self): # Overwrite UnitedStatesTest.test_president_day_label @@ -645,7 +666,8 @@ def test_state_year_2014(self): def test_state_year_2015(self): holidays = self.cal.holidays_set(2015) - self.assertIn(date(2015, 7, 3), holidays) + observed = set(map(self.cal.get_observed_date, holidays)) + self.assertIn(date(2015, 7, 3), observed) self.assertIn(date(2015, 11, 27), holidays) # Thanksgiving Friday def test_thanksgiving_friday_label(self): @@ -684,16 +706,12 @@ class FloridaLegalTest(IncludeMardiGras, ElectionDayEveryYear, cal_class = FloridaLegal def test_init_warning(self): - warnings.simplefilter("always") - with warnings.catch_warnings(record=True) as w: - # Cause all warnings to always be triggered. - # Trigger a warning. + msg = ( + "Florida's laws separate the definitions between " + "paid versus legal holidays." + ) + with pytest.warns(UserWarning, match=msg): self.cal_class() - # Verify some things - assert len(w) == 1 - assert issubclass(w[-1].category, UserWarning) - assert "Florida's laws separate the definitions between paid versus legal holidays." in str(w[-1].message) # noqa - warnings.simplefilter("ignore") def test_specific_lincoln_birthday(self): holidays = self.cal.holidays_set(2014) @@ -866,7 +884,9 @@ def test_year_2020(self): self.assertIn(date(2020, 4, 10), holidays) self.assertIn(date(2020, 5, 25), holidays) # memorial day - self.assertIn(date(2020, 7, 3), holidays) # Independance day (OBS) + observed = set(map(self.cal.get_observed_date, holidays)) + # Independance day (OBS) + self.assertIn(date(2020, 7, 3), observed) self.assertIn(date(2020, 7, 4), holidays) # Independance day self.assertIn(date(2020, 9, 7), holidays) # Labor day self.assertIn(date(2020, 10, 12), holidays) # Columbus @@ -905,13 +925,14 @@ class HawaiiTest(ElectionDayEvenYears, NoColumbus, UnitedStatesTest): def test_state_year_2017(self): holidays = self.cal.holidays_set(2017) + observed = set(map(self.cal.get_observed_date, holidays)) self.assertIn(date(2017, 3, 26), holidays) # Prince Jonah Kuhio Kalanianaole self.assertIn(date(2017, 3, 27), - holidays) # Prince Jonah Kuhio Kalanianaole (shifted) + observed) # Prince Jonah Kuhio Kalanianaole (shifted) self.assertIn(date(2017, 4, 14), holidays) # Good Friday self.assertIn(date(2017, 6, 11), holidays) # Kamehameha - self.assertIn(date(2017, 6, 12), holidays) # Kamehameha (shifted) + self.assertIn(date(2017, 6, 12), observed) # Kamehameha (shifted) self.assertIn(date(2017, 8, 18), holidays) # Statehood day def test_state_year_2018(self): @@ -1065,7 +1086,8 @@ def test_state_year_2014(self): def test_state_year_2015(self): holidays = self.cal.holidays_set(2015) - self.assertIn(date(2015, 7, 3), holidays) + observed = set(map(self.cal.get_observed_date, holidays)) + self.assertIn(date(2015, 7, 3), observed) self.assertIn(date(2015, 11, 27), holidays) # Thanksgiving Friday @@ -1515,14 +1537,15 @@ class SouthCarolinaTest(NoColumbus, UnitedStatesTest): def test_state_year_2014(self): holidays = self.cal.holidays_set(2014) + observed = set(map(self.cal.get_observed_date, holidays)) # Confederate Memorial Day self.assertIn(date(2014, 5, 10), holidays) # Observed here, it falls on SAT - self.assertIn(date(2014, 5, 9), holidays) + self.assertIn(date(2014, 5, 9), observed) self.assertIn(date(2014, 11, 28), holidays) # Thanksgiving Friday - self.assertIn(date(2014, 12, 24), holidays) # XMas Eve - self.assertIn(date(2014, 12, 26), holidays) # Boxing day + self.assertIn(date(2014, 12, 24), observed) # XMas Eve + self.assertIn(date(2014, 12, 26), observed) # Boxing day def test_state_year_2015(self): holidays = self.cal.holidays_set(2015) @@ -1618,15 +1641,17 @@ class VermontTest(NoColumbus, UnitedStatesTest): def test_state_year_2014(self): holidays = self.cal.holidays_set(2014) + observed = set(map(self.cal.get_observed_date, holidays)) self.assertIn(date(2014, 3, 4), holidays) # Town Meeting Day self.assertIn(date(2014, 8, 16), holidays) # Bennington Battle Day - self.assertIn(date(2014, 8, 15), holidays) # Shifted to FRI + self.assertIn(date(2014, 8, 15), observed) # Shifted to FRI def test_state_year_2015(self): holidays = self.cal.holidays_set(2015) + observed = set(map(self.cal.get_observed_date, holidays)) self.assertIn(date(2015, 3, 3), holidays) # Town Meeting Day self.assertIn(date(2015, 8, 16), holidays) # Bennington Battle Day - self.assertIn(date(2015, 8, 17), holidays) # Shifted to MON + self.assertIn(date(2015, 8, 17), observed) # Shifted to MON class VirginiaTest(UnitedStatesTest): @@ -1811,8 +1836,7 @@ def test_shift_2015(self): observed = date(2015, 7, 3) self.assertIn(fourth_july, holiday_dict) self.assertEqual(holiday_dict[fourth_july], "Independence Day") - self.assertIn(observed, holiday_dict) - self.assertEqual(holiday_dict[observed], "Independence Day (Observed)") + self.assertNotIn(observed, holiday_dict) def test_shift_2010(self): # Test a normal shift on 4th of July. @@ -1823,19 +1847,14 @@ def test_shift_2010(self): observed = date(2010, 7, 5) self.assertIn(fourth_july, holiday_dict) self.assertEqual(holiday_dict[fourth_july], "Independence Day") - self.assertIn(observed, holiday_dict) - self.assertEqual(holiday_dict[observed], "Independence Day (Observed)") + self.assertNotIn(observed, holiday_dict) def test_new_years_shift(self): # If January, 1st *of the year after* happens on SAT, add New Years Eve holidays = self.cal.holidays(2010) holiday_dict = dict(holidays) new_years_eve = date(2010, 12, 31) - self.assertIn(new_years_eve, holiday_dict) - self.assertEqual( - holiday_dict[new_years_eve], - "New Years Day (Observed)" - ) + self.assertNotIn(new_years_eve, holiday_dict) # The year after, it's not shifted holidays = self.cal.holidays_set(2011) new_years_eve = date(2011, 12, 31) @@ -1849,12 +1868,10 @@ def test_christmas_extra_shift_2010(self): # * 23rd (XMas Eve shifted on THU) holidays = self.cal.holidays(2010) holiday_dict = dict(holidays) - dec_23rd = date(2010, 12, 23) dec_24th = date(2010, 12, 24) dec_25th = date(2010, 12, 25) - for day in (dec_23rd, dec_24th, dec_25th): + for day in (dec_24th, dec_25th): self.assertIn(day, holiday_dict) - self.assertEqual(holiday_dict[dec_23rd], "Christmas Eve (Observed)") self.assertEqual(holiday_dict[dec_24th], "Christmas Eve") self.assertEqual(holiday_dict[dec_25th], "Christmas Day") @@ -1867,12 +1884,10 @@ def test_christmas_extra_shift_2006(self): holiday_dict = dict(holidays) dec_24th = date(2006, 12, 24) dec_25th = date(2006, 12, 25) - dec_26th = date(2006, 12, 26) - for day in (dec_24th, dec_25th, dec_26th): + for day in (dec_24th, dec_25th): self.assertIn(day, holiday_dict) self.assertEqual(holiday_dict[dec_24th], "Christmas Eve") self.assertEqual(holiday_dict[dec_25th], "Christmas Day") - self.assertEqual(holiday_dict[dec_26th], "Christmas Day (Observed)") class ShiftExceptionsTestCase(UnitedStatesTest): @@ -1941,16 +1956,18 @@ class FederalReserveSystemTest(UnitedStatesTest): def test_juneteenth_day(self): holidays = self.cal.holidays_set(2021) + observed = set(map(self.cal.get_observed_date, holidays)) juneteenth, _ = self.cal.get_juneteenth_day(2021) self.assertEqual(date(2021, 6, 19), juneteenth) # 2021-06-19 is a Saturday so holiday is shifted - self.assertIn(date(2021, 6, 18), holidays) + self.assertIn(date(2021, 6, 18), observed) holidays = self.cal.holidays_set(2022) + observed = set(map(self.cal.get_observed_date, holidays)) juneteenth, _ = self.cal.get_juneteenth_day(2022) self.assertEqual(date(2022, 6, 19), juneteenth) # 2022-06-19 is a Sunday so holiday is shifted - self.assertIn(date(2022, 6, 20), holidays) + self.assertIn(date(2022, 6, 20), observed) holidays = self.cal.holidays_set(2023) juneteenth, _ = self.cal.get_juneteenth_day(2023) diff --git a/workalendar/usa/__init__.py b/calendra/usa/__init__.py similarity index 100% rename from workalendar/usa/__init__.py rename to calendra/usa/__init__.py diff --git a/workalendar/usa/alabama.py b/calendra/usa/alabama.py similarity index 100% rename from workalendar/usa/alabama.py rename to calendra/usa/alabama.py diff --git a/workalendar/usa/alaska.py b/calendra/usa/alaska.py similarity index 77% rename from workalendar/usa/alaska.py rename to calendra/usa/alaska.py index 81eb71f4..a0bd29f1 100644 --- a/workalendar/usa/alaska.py +++ b/calendra/usa/alaska.py @@ -1,5 +1,8 @@ +import datetime + from .core import UnitedStates from ..core import MON +from ..core import Holiday from ..registry_tools import iso_register @@ -7,7 +10,10 @@ class Alaska(UnitedStates): """Alaska""" FIXED_HOLIDAYS = UnitedStates.FIXED_HOLIDAYS + ( - (10, 18, 'Alaska Day'), + Holiday( + datetime.date(2000, 10, 18), + 'Alaska Day', + ), ) include_columbus_day = False diff --git a/workalendar/usa/american_samoa.py b/calendra/usa/american_samoa.py similarity index 100% rename from workalendar/usa/american_samoa.py rename to calendra/usa/american_samoa.py diff --git a/workalendar/usa/arizona.py b/calendra/usa/arizona.py similarity index 100% rename from workalendar/usa/arizona.py rename to calendra/usa/arizona.py diff --git a/workalendar/usa/arkansas.py b/calendra/usa/arkansas.py similarity index 100% rename from workalendar/usa/arkansas.py rename to calendra/usa/arkansas.py diff --git a/workalendar/usa/california.py b/calendra/usa/california.py similarity index 100% rename from workalendar/usa/california.py rename to calendra/usa/california.py diff --git a/workalendar/usa/colorado.py b/calendra/usa/colorado.py similarity index 100% rename from workalendar/usa/colorado.py rename to calendra/usa/colorado.py diff --git a/workalendar/usa/connecticut.py b/calendra/usa/connecticut.py similarity index 100% rename from workalendar/usa/connecticut.py rename to calendra/usa/connecticut.py diff --git a/workalendar/usa/core.py b/calendra/usa/core.py similarity index 66% rename from workalendar/usa/core.py rename to calendra/usa/core.py index 3f8d33c9..5a98a3a5 100644 --- a/workalendar/usa/core.py +++ b/calendra/usa/core.py @@ -1,6 +1,9 @@ from datetime import date, timedelta -from ..core import MON, SAT, SUN, THU, TUE, WesternCalendar +from dateutil import relativedelta as rd + +from ..core import MON, SUN, THU, TUE, WesternCalendar +from ..core import Holiday from ..registry_tools import iso_register @@ -9,10 +12,19 @@ class UnitedStates(WesternCalendar): "United States of America" FIXED_HOLIDAYS = WesternCalendar.FIXED_HOLIDAYS + ( - (7, 4, 'Independence Day'), + Holiday( + date(2000, 7, 4), + 'Independence Day', + indication='July 4', + ), ) + + @property + def observance_shift(self): + return Holiday.nearest_weekday + # Veterans day label - include_veterans_day = True + include_veterans_day = False veterans_day_label = 'Veterans Day' # MLK @@ -58,6 +70,7 @@ class UnitedStates(WesternCalendar): national_memorial_day_label = "Memorial Day" # Some regional variants + include_mardi_gras = False include_fat_tuesday = False fat_tuesday_label = "Mardi Gras" @@ -71,64 +84,6 @@ class UnitedStates(WesternCalendar): # (11, 11), # Veterans day won't be shifted ) - def shift(self, holidays, year): - """ - Shift all holidays of the year, according to the shifting rules. - """ - new_holidays = [] - holiday_lookup = [x[0] for x in holidays] - exceptions = [] - if self.shift_exceptions: - exceptions = [ - *[date(year - 1, m, d) for m, d in self.shift_exceptions], - *[date(year, m, d) for m, d in self.shift_exceptions], - *[date(year + 1, m, d) for m, d in self.shift_exceptions] - ] - - # For each holiday available: - # * if it falls on SUN, add the observed on MON - # * if it falls on SAT, add the observed on FRI - for day, label in holidays: - # ... except if it's been explicitely excepted. - if day in exceptions: - continue - if day.weekday() == SAT: - new_holidays.append( - (day - timedelta(days=1), f"{label} (Observed)")) - elif day.weekday() == SUN: - new_holidays.append( - (day + timedelta(days=1), f"{label} (Observed)")) - - # If year+1 January the 1st is on SAT, add the FRI before to observed - next_year_jan_1st = date(year + 1, 1, 1) - if next_year_jan_1st.weekday() == SAT and \ - next_year_jan_1st not in exceptions: - new_holidays.append( - (date(year, 12, 31,), "New Years Day (Observed)")) - - # Special rules for XMas and XMas Eve - christmas = date(year, 12, 25) - christmas_eve = date(year, 12, 24) - # Is XMas eve in your calendar? - if christmas_eve in holiday_lookup: - # You are observing the THU before, as an extra XMas Eve - if christmas.weekday() == SAT: - # Remove the "fake" XMAS Day shift, the one done before. - new_holidays.remove( - (christmas_eve, "Christmas Day (Observed)") - ) - new_holidays.append( - (date(year, 12, 23), "Christmas Eve (Observed)")) - # You are observing the 26th (TUE) - elif christmas.weekday() == MON: - # Remove the "fake" XMAS Eve shift, done before - new_holidays.remove( - (christmas, "Christmas Eve (Observed)") - ) - new_holidays.append( - (date(year, 12, 26), "Christmas Day (Observed)")) - return holidays + new_holidays - @staticmethod def is_presidential_year(year): return (year % 4) == 0 @@ -176,22 +131,21 @@ def get_jefferson_davis_birthday(self, year): ) def get_martin_luther_king_date(self, year): - """ - Martin Luther King is on 3rd MON of January, starting of 1985. - - """ if year < 1985: raise ValueError( "Martin Luther King Day became a holiday in 1985" ) - return UnitedStates.get_nth_weekday_in_month(year, 1, MON, 3) + return date(year, 1, 1) + rd.relativedelta(weekday=rd.MO(3)) def get_martin_luther_king_day(self, year): """ Return holiday record for Martin Luther King Jr. Day. """ - day = self.get_martin_luther_king_date(year) - return day, self.martin_luther_king_label + return Holiday( + self.get_martin_luther_king_date(year), + self.martin_luther_king_label, + indication="3rd Monday in January", + ) def get_presidents_day(self, year): """ @@ -199,8 +153,11 @@ def get_presidents_day(self, year): May be called Washington's or Lincoln's birthday """ - day = UnitedStates.get_nth_weekday_in_month(year, 2, MON, 3) - return day, self.presidents_day_label + return Holiday( + date(year, 2, 1) + rd.relativedelta(weekday=rd.MO(3)), + self.presidents_day_label, + indication="3rd Monday in February", + ) def get_cesar_chavez_days(self, year): """ @@ -222,8 +179,11 @@ def get_columbus_day(self, year): Only half of the states recognize it. """ - day = UnitedStates.get_nth_weekday_in_month(year, 10, MON, 2) - return day, self.columbus_day_label + return Holiday( + date(year, 10, 1) + rd.relativedelta(weekday=rd.MO(2)), + self.columbus_day_label, + indication="2nd Monday in October", + ) def get_lincoln_birthday(self, year): """ @@ -253,12 +213,10 @@ def get_inauguration_date(self, year): return inauguration_day def get_national_memorial_day(self, year): - """ - Return National Memorial Day - """ - return ( - UnitedStates.get_last_weekday_in_month(year, 5, MON), - self.national_memorial_day_label + return Holiday( + date(year, 5, 31) + rd.relativedelta(weekday=rd.MO(-1)), + self.national_memorial_day_label, + indication="Last Monday in May", ) def get_juneteenth_day(self, year): @@ -268,23 +226,38 @@ def get_juneteenth_day(self, year): # Juneteenth started to be a federal holiday in 2021 if year < 2021: raise ValueError("Juneteenth became a federal holiday in 2021") - return (date(year, 6, 19), "Juneteenth National Independence Day") + return Holiday( + date(year, 6, 19), + "Juneteenth National Independence Day", + indication="June 19th", + ) - def get_variable_days(self, year): + def get_variable_days(self, year): # noqa: C901 # usual variable days days = super().get_variable_days(year) + days += [ + self.get_veterans_day(year), + self.get_national_memorial_day(year), + Holiday( + date(year, 9, 1) + rd.relativedelta(weekday=rd.MO(1)), + "Labor Day", + indication="1st Monday in September", + ), + + Holiday( + date(year, 11, 1) + rd.relativedelta(weekday=rd.TH(4)), + "Thanksgiving Day", + indication="4th Thursday in November", + ), + ] + # Martin Luther King's Day started only in 1985 if year >= 1985: days.append(self.get_martin_luther_king_day(year)) - days.extend([ - self.get_national_memorial_day(year), - (UnitedStates.get_nth_weekday_in_month(year, 9, MON), - "Labor Day"), - (UnitedStates.get_nth_weekday_in_month(year, 11, THU, 4), - "Thanksgiving Day"), - ]) + if self.include_mardi_gras: + days.append(self.get_mardi_gras(year)) if self.include_federal_presidents_day: days.append(self.get_presidents_day(year)) @@ -307,11 +280,16 @@ def get_variable_days(self, year): if self.include_jefferson_davis_birthday: days.append(self.get_jefferson_davis_birthday(year)) + ind = "January 20 (or 21st if Sunday) following an election year" if self.include_inauguration_day: # Is it a "Inauguration year"? if UnitedStates.is_presidential_year(year - 1): days.append( - (self.get_inauguration_date(year), "Inauguration Day") + Holiday( + self.get_inauguration_date(year), + "Inauguration Day", + indication=ind, + ), ) if self.include_election_day_every_year: @@ -336,7 +314,11 @@ def get_veterans_day(self, year): Placed here because some States are renaming it. """ - return date(year, 11, 11), self.veterans_day_label + return Holiday( + date(year, 11, 11), + self.veterans_day_label, + indication='Nov 11', + ) def get_fixed_holidays(self, year): days = super().get_fixed_holidays(year) @@ -344,14 +326,6 @@ def get_fixed_holidays(self, year): days.append(self.get_veterans_day(year)) return days - def get_calendar_holidays(self, year): - """ - Will return holidays and their shifted days - """ - days = super().get_calendar_holidays(year) - days = self.shift(days, year) - return days - class FederalReserveSystem(UnitedStates): "Board of Governors of the Federal Reserve System of the USA" diff --git a/workalendar/usa/delaware.py b/calendra/usa/delaware.py similarity index 100% rename from workalendar/usa/delaware.py rename to calendra/usa/delaware.py diff --git a/workalendar/usa/district_columbia.py b/calendra/usa/district_columbia.py similarity index 100% rename from workalendar/usa/district_columbia.py rename to calendra/usa/district_columbia.py diff --git a/workalendar/usa/florida.py b/calendra/usa/florida.py similarity index 100% rename from workalendar/usa/florida.py rename to calendra/usa/florida.py diff --git a/workalendar/usa/georgia.py b/calendra/usa/georgia.py similarity index 100% rename from workalendar/usa/georgia.py rename to calendra/usa/georgia.py diff --git a/workalendar/usa/guam.py b/calendra/usa/guam.py similarity index 100% rename from workalendar/usa/guam.py rename to calendra/usa/guam.py diff --git a/workalendar/usa/hawaii.py b/calendra/usa/hawaii.py similarity index 100% rename from workalendar/usa/hawaii.py rename to calendra/usa/hawaii.py diff --git a/workalendar/usa/idaho.py b/calendra/usa/idaho.py similarity index 100% rename from workalendar/usa/idaho.py rename to calendra/usa/idaho.py diff --git a/workalendar/usa/illinois.py b/calendra/usa/illinois.py similarity index 100% rename from workalendar/usa/illinois.py rename to calendra/usa/illinois.py diff --git a/workalendar/usa/indiana.py b/calendra/usa/indiana.py similarity index 100% rename from workalendar/usa/indiana.py rename to calendra/usa/indiana.py diff --git a/workalendar/usa/iowa.py b/calendra/usa/iowa.py similarity index 100% rename from workalendar/usa/iowa.py rename to calendra/usa/iowa.py diff --git a/workalendar/usa/kansas.py b/calendra/usa/kansas.py similarity index 99% rename from workalendar/usa/kansas.py rename to calendra/usa/kansas.py index ae1225ba..018d636e 100644 --- a/workalendar/usa/kansas.py +++ b/calendra/usa/kansas.py @@ -1,6 +1,7 @@ from .core import UnitedStates from ..registry_tools import iso_register + # FIXME: According to wikipedia, Kansas only has all federal holidays, except # the Columbus Day and Washington's Birthday. # Unfortunately, other sources mention XMas Eve for 2018, but not for other diff --git a/workalendar/usa/kentucky.py b/calendra/usa/kentucky.py similarity index 100% rename from workalendar/usa/kentucky.py rename to calendra/usa/kentucky.py diff --git a/workalendar/usa/louisiana.py b/calendra/usa/louisiana.py similarity index 100% rename from workalendar/usa/louisiana.py rename to calendra/usa/louisiana.py diff --git a/workalendar/usa/maine.py b/calendra/usa/maine.py similarity index 100% rename from workalendar/usa/maine.py rename to calendra/usa/maine.py diff --git a/workalendar/usa/maryland.py b/calendra/usa/maryland.py similarity index 100% rename from workalendar/usa/maryland.py rename to calendra/usa/maryland.py diff --git a/workalendar/usa/massachusetts.py b/calendra/usa/massachusetts.py similarity index 100% rename from workalendar/usa/massachusetts.py rename to calendra/usa/massachusetts.py diff --git a/workalendar/usa/michigan.py b/calendra/usa/michigan.py similarity index 100% rename from workalendar/usa/michigan.py rename to calendra/usa/michigan.py diff --git a/workalendar/usa/minnesota.py b/calendra/usa/minnesota.py similarity index 100% rename from workalendar/usa/minnesota.py rename to calendra/usa/minnesota.py diff --git a/workalendar/usa/mississippi.py b/calendra/usa/mississippi.py similarity index 100% rename from workalendar/usa/mississippi.py rename to calendra/usa/mississippi.py diff --git a/workalendar/usa/missouri.py b/calendra/usa/missouri.py similarity index 100% rename from workalendar/usa/missouri.py rename to calendra/usa/missouri.py diff --git a/workalendar/usa/montana.py b/calendra/usa/montana.py similarity index 100% rename from workalendar/usa/montana.py rename to calendra/usa/montana.py diff --git a/workalendar/usa/nebraska.py b/calendra/usa/nebraska.py similarity index 100% rename from workalendar/usa/nebraska.py rename to calendra/usa/nebraska.py diff --git a/workalendar/usa/nevada.py b/calendra/usa/nevada.py similarity index 100% rename from workalendar/usa/nevada.py rename to calendra/usa/nevada.py diff --git a/workalendar/usa/new_hampshire.py b/calendra/usa/new_hampshire.py similarity index 100% rename from workalendar/usa/new_hampshire.py rename to calendra/usa/new_hampshire.py diff --git a/workalendar/usa/new_jersey.py b/calendra/usa/new_jersey.py similarity index 100% rename from workalendar/usa/new_jersey.py rename to calendra/usa/new_jersey.py diff --git a/workalendar/usa/new_mexico.py b/calendra/usa/new_mexico.py similarity index 100% rename from workalendar/usa/new_mexico.py rename to calendra/usa/new_mexico.py diff --git a/workalendar/usa/new_york.py b/calendra/usa/new_york.py similarity index 100% rename from workalendar/usa/new_york.py rename to calendra/usa/new_york.py diff --git a/workalendar/usa/north_carolina.py b/calendra/usa/north_carolina.py similarity index 100% rename from workalendar/usa/north_carolina.py rename to calendra/usa/north_carolina.py diff --git a/workalendar/usa/north_dakota.py b/calendra/usa/north_dakota.py similarity index 100% rename from workalendar/usa/north_dakota.py rename to calendra/usa/north_dakota.py diff --git a/workalendar/usa/ohio.py b/calendra/usa/ohio.py similarity index 100% rename from workalendar/usa/ohio.py rename to calendra/usa/ohio.py diff --git a/workalendar/usa/oklahoma.py b/calendra/usa/oklahoma.py similarity index 100% rename from workalendar/usa/oklahoma.py rename to calendra/usa/oklahoma.py diff --git a/workalendar/usa/oregon.py b/calendra/usa/oregon.py similarity index 100% rename from workalendar/usa/oregon.py rename to calendra/usa/oregon.py diff --git a/workalendar/usa/pennsylvania.py b/calendra/usa/pennsylvania.py similarity index 100% rename from workalendar/usa/pennsylvania.py rename to calendra/usa/pennsylvania.py diff --git a/workalendar/usa/rhode_island.py b/calendra/usa/rhode_island.py similarity index 100% rename from workalendar/usa/rhode_island.py rename to calendra/usa/rhode_island.py diff --git a/workalendar/usa/south_carolina.py b/calendra/usa/south_carolina.py similarity index 100% rename from workalendar/usa/south_carolina.py rename to calendra/usa/south_carolina.py diff --git a/workalendar/usa/south_dakota.py b/calendra/usa/south_dakota.py similarity index 100% rename from workalendar/usa/south_dakota.py rename to calendra/usa/south_dakota.py diff --git a/workalendar/usa/tennessee.py b/calendra/usa/tennessee.py similarity index 100% rename from workalendar/usa/tennessee.py rename to calendra/usa/tennessee.py diff --git a/workalendar/usa/texas.py b/calendra/usa/texas.py similarity index 100% rename from workalendar/usa/texas.py rename to calendra/usa/texas.py diff --git a/workalendar/usa/utah.py b/calendra/usa/utah.py similarity index 100% rename from workalendar/usa/utah.py rename to calendra/usa/utah.py diff --git a/workalendar/usa/vermont.py b/calendra/usa/vermont.py similarity index 100% rename from workalendar/usa/vermont.py rename to calendra/usa/vermont.py diff --git a/workalendar/usa/virginia.py b/calendra/usa/virginia.py similarity index 100% rename from workalendar/usa/virginia.py rename to calendra/usa/virginia.py diff --git a/workalendar/usa/washington.py b/calendra/usa/washington.py similarity index 100% rename from workalendar/usa/washington.py rename to calendra/usa/washington.py diff --git a/workalendar/usa/west_virginia.py b/calendra/usa/west_virginia.py similarity index 100% rename from workalendar/usa/west_virginia.py rename to calendra/usa/west_virginia.py diff --git a/workalendar/usa/wisconsin.py b/calendra/usa/wisconsin.py similarity index 100% rename from workalendar/usa/wisconsin.py rename to calendra/usa/wisconsin.py diff --git a/workalendar/usa/wyoming.py b/calendra/usa/wyoming.py similarity index 100% rename from workalendar/usa/wyoming.py rename to calendra/usa/wyoming.py diff --git a/create-astronomical-data b/create-astronomical-data deleted file mode 100755 index 9fb4f29d..00000000 --- a/create-astronomical-data +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env python3 -from tqdm import tqdm - -from workalendar.precomputed_astronomy import create_astronomical_data - -if __name__ == '__main__': - create_astronomical_data(tqdm) diff --git a/docs/_config.yml b/docs/_config.yml deleted file mode 100644 index 6952c416..00000000 --- a/docs/_config.yml +++ /dev/null @@ -1,3 +0,0 @@ -theme: jekyll-theme-hacker -include: - - contributing.md diff --git a/docs/advanced.md b/docs/advanced.md index 0b0cb9c7..afba0b44 100644 --- a/docs/advanced.md +++ b/docs/advanced.md @@ -2,7 +2,7 @@ [Home](index.md) / [Basic usage](basic.md) / [Class options](class-options.md) / [ISO Registry](iso-registry.md) / [iCal Export](ical.md) / [Contributing](contributing.md) -The following examples will better suit people willing to contribute to Workalendar or building their custom calendars. They use primitives and methods attached to the core Calendar class to enable computation of complex holidays, that is to say dates that are not fixed, not related to religious calendars (Christmas always happens on December 25th, right?). +The following examples will better suit people willing to contribute to Calendra or building their custom calendars. They use primitives and methods attached to the core Calendar class to enable computation of complex holidays, that is to say dates that are not fixed, not related to religious calendars (Christmas always happens on December 25th, right?). ## Find the following working day after a date @@ -12,7 +12,7 @@ Please note that if the day you're entering is already a working day, that'll be ```python >>> from datetime import date, datetime ->>> from workalendar.europe import France +>>> from calendra.europe import France >>> cal = France() >>> cal.find_following_working_day(date(2018, 7, 6)) # It's FRI datetime.date(2018, 7, 6) @@ -35,7 +35,7 @@ Or even better for Election day, which is: We've got you covered with static methods in the core ``Calendar`` class. ```python ->>> from workalendar.core import Calendar, THU +>>> from calendra.core import Calendar, THU >>> Calendar.get_nth_weekday_in_month(2018, 11, THU) # by default, find the first datetime.date(2018, 11, 2) >>> Calendar.get_nth_weekday_in_month(2018, 11, THU, 4) # Thanksgiving @@ -46,7 +46,7 @@ If you want to find the 2nd Monday after the 4th of July, you can use the ``star ```python >>> from datetime import date ->>> from workalendar.core import Calendar, MON +>>> from calendra.core import Calendar, MON >>> Calendar.get_nth_weekday_in_month( 2018, # Year 7, # Month @@ -63,7 +63,7 @@ This one was a bit trickier, because it may happen that you have 4 or 5 `weekday Same as above, there's a static method in the ``Calendar`` class: ```python ->>> from workalendar.core import Calendar, MON, FRI +>>> from calendra.core import Calendar, MON, FRI >>> Calendar.get_last_weekday_in_month(2018, 7, MON) datetime.date(2018, 7, 30) >>> Calendar.get_last_weekday_in_month(2018, 7, FRI) @@ -75,7 +75,7 @@ datetime.date(2018, 7, 27) Colombia, as an example, states that the Epiphany Day is set to the "First Monday after the 6th of January". You may use the following function to find this specific formula: ```python ->>> from workalendar.core import Calendar, MON +>>> from calendra.core import Calendar, MON >>> from datetime import date >>> jan_6th = date(2018, 1, 6) # It's a Saturday >>> Calendar.get_first_weekday_after(jan_6th, MON) diff --git a/docs/basic.md b/docs/basic.md index fe9a9c08..fff0e5ac 100644 --- a/docs/basic.md +++ b/docs/basic.md @@ -2,12 +2,12 @@ [Home](index.md) / [Advanced usage](advanced.md) / [Class options](class-options.md) / [ISO Registry](iso-registry.md) / [iCal Export](ical.md) / [Contributing](contributing.md) -Here are basic examples of what Workalendar can do for you. As an integrator or a simple Workalendar user, you will use these methods to retrieve calendars, and get basic outputs for a given date. +Here are basic examples of what Calendra can do for you. As an integrator or a simple Calendra user, you will use these methods to retrieve calendars, and get basic outputs for a given date. ## Get holidays for a given country and year ```python ->>> from workalendar.europe import France +>>> from calendra.europe import France >>> cal = France() >>> cal.holidays(2012) [(datetime.date(2012, 1, 1), 'New year'), @@ -29,7 +29,7 @@ As you can see, the output is a simple list of tuples, with the first member bei ```python >>> from datetime import date ->>> from workalendar.europe import France +>>> from calendra.europe import France >>> cal = France() >>> cal.is_working_day(date(2012, 12, 25)) # it's Christmas False @@ -61,7 +61,7 @@ In the following example, we want to calculate 5 working days after December the ```python >>> from datetime import date ->>> from workalendar.europe import France +>>> from calendra.europe import France >>> cal = France() >>> cal.add_working_days(date(2012, 12, 23), 5) datetime.date(2012, 12, 31) @@ -75,7 +75,7 @@ Let's say you want to know how many working days there are between April 2nd and ```python >>> from datetime import date ->>> from workalendar.europe import France +>>> from calendra.europe import France >>> cal = France() >>> cal.get_working_days_delta(date(2018, 4, 2), date(2018, 6, 17)) 50 @@ -91,7 +91,7 @@ Example: ```python >>> from datetime import date, datetime ->>> from workalendar.europe import France +>>> from calendra.europe import France >>> cal = France() >>> cal.is_working_day(datetime(2012, 12, 25, 14, 0, 39)) False @@ -103,7 +103,7 @@ You see that the default result type is ``datetime.date``. But if you really nee ```python >>> from datetime import date, datetime ->>> from workalendar.europe import France +>>> from calendra.europe import France >>> cal = France() >>> cal.add_working_days(datetime(2012, 12, 23, 14, 0, 39), 5, keep_datetime=True) datetime.datetime(2012, 12, 31, 14, 0, 39) diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 00000000..84ff1c32 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +extensions = ['sphinx.ext.autodoc', 'jaraco.packaging.sphinx', 'rst.linker'] + +master_doc = "index" + +link_files = { + '../CHANGES.rst': dict( + using=dict( + GH='https://github.com', + workalendar='https://github.com/peopledoc/workalendar/', + ), + replace=[ + dict( + pattern=r'(Issue #|\B#)(?P\d+)', + url='{package_url}/issues/{issue}', + ), + dict( + pattern=r'(?m:^((?Pv?\d+(\.\d+){1,2}))\n[-=]+\n)', + with_scm='{text}\n{rev[timestamp]:%d %b %Y}\n', + ), + dict( + pattern=r'PEP[- ](?P\d+)', + url='https://peps.python.org/pep-{pep_number:0>4}/', + ), + dict( + pattern=r'\(#(?P\d+)(.*?)\)', + url='{workalendar}issues/{wk_issue}', + ), + dict( + pattern=r'(?P[Ww]orkalendar \d+\.\d+(\.\d+)?)', + url='{workalendar}blob/master/Changelog.md', + ), + ], + ) +} + +# Be strict about any broken references: +nitpicky = True + +# Include Python intersphinx mapping to prevent failures +# jaraco/skeleton#51 +extensions += ['sphinx.ext.intersphinx'] +intersphinx_mapping = { + 'python': ('https://docs.python.org/3', None), +} diff --git a/docs/contributing.md b/docs/contributing.md deleted file mode 100644 index 685aeefc..00000000 --- a/docs/contributing.md +++ /dev/null @@ -1,227 +0,0 @@ -# Contribute to Workalendar - -[Home](index.md) / [Basic usage](basic.md) / [Advanced usage](advanced.md) / [Class options](class-options.md) / [ISO Registry](iso-registry.md) / [iCal Export](ical.md) - -## Use it (and test it) - -If you are using `workalendar`, you are already contributing to it. As long as you are able to check its result, compare the designated working days and holidays to the reality, and make sure these are right, you're helping. - -If any of the computed holidays for the country / area your are using is **wrong**, please report [it using the Github issues](https://github.com/workalendar/workalendar/issues). - -## Report an issue - -If you think you've found a bug you can report an issue. In order to help us sort this out, please follow the guidelines: - -* Tell us which `workalendar` version (master, PyPI release) you are using. -* Tell us which Python version you are using, and your platform. -* Give us extensive details on the country / area Calendar, the exact date(s) that was (were) computed and the one(s) that should have been the correct result. -* If possible, please provide us a reliable source about the designated country / area calendar, where we could effectively check that we were wrong about a date, and giving us a way to patch our code properly so we can fix the bug. - -## Adding new calendars - -Since `workalendar` is mainly built around configuration variables and generic methods, it's not that difficult to add a calendar to our codebase. A few **mandatory** steps should be observed: - -1. Fork the repository and create a new branch named after the calendar you want to implement, -2. Add a test class to the workalendar test suite that checks holidays, -3. Implement the class using the core class APIs as much as possible. Test it until all tests pass. -4. Make a nice pull-request we'll be glad to review and merge when it's perfect. - -Please respect the PEP8 convention, otherwise your PR won't be accepted. - -## Maintain Equinoxes and Solar terms - -To reduce dependencies and computation time, equinoxes and solar terms are precomputed for the 30 previous and next years. -From time to time and before each release, please run the `create-astronomical-data` data file to update those files. - -You also need to regenerate the `.json.gz` files if you plan to add a timezone to the astronomical data. - -### Example - -Let's assume you want to include the holidays of the magic (fictional) kingdom of *"Zhraa"*, which has a few holidays of different kind. - -For the sake of the example, it has the following specs: - -* it's a Gregorian-based Calendar (i.e. Western Europe / America), -* even if the King is not versed into religions, the kingdom includes a few Christian holidays, -* even if you never knew about it, it is set in Europe, - -Here is a list of the holidays in *Zhraa*: - -* January 1st, New year's Day, -* May 1st, Labour day, -* Easter Monday, which is variable (from March to May), -* The first monday in June, to celebrate the birth of the Founder of the Kingdom, Zhraa (nobody knows the exact day he was born, so this day was chosen as a convention), -* The birthday of the King, August 2nd. -* Christmas Day, Dec 25th. - -#### Getting ready - -You'll need to install `workalendar` dependencies beforehand. What's great is that you'll use virtualenv to set it up. Or even better: `virtualenvwrapper`. Just go in your working copy (cloned from github) of workalendar and type, for example: - -```sh -mkvirtualenv WORKALENDAR -pip install -e ./ -``` - -#### Coding standards - -When you provide a patch for workalendar, whether it would be a new calendar or a fix to an existing one, or else, you will have to make sure that your contribution follows these basic requirements: - -* Your code should pass the `flake8` test ; that is to say that it follows the [PEP8](https://www.python.org/dev/peps/pep-0008/) guidelines. You can check using the `tox -e flake8` command. If you can't, our CI jobs will do it, and you'll be able to know where are your mistakes, if any. -* Your code should be compatible with our supported Python version. Currently: Python 3.6, 3.7, 3.8 and 3.9. Again, the CI (powered by Github Actions) will check your code against all those versions so you won't have to. -* If you encounter a failure in the `pyupgrade` tox job, you can fix it by running the followinf command: `tox -r pyupgrade`. It'll modify your code so it will pass this test. **Warning:** sometimes, this change can break the `flake8` standards, so you'll have to make sure that both linters will pass. - -#### Test-driven start - -Let's prepare the `Zhraa` class. Edit the `workalendar/europe/zhraa.py` file and add a class like this: - -```python -from ..core import WesternCalendar -# NOTE: You may use absolute imports if your code lives outside of our codebase. - -class Zhraa(WesternCalendar): - "Kingdom of Zhraa" -``` - -The docstring is not mandatory, but if you omit it, the `name` property of your class will be the name of your class. For example, using upper CamelCase, `KingdomOfZhraa`. For a more human-readable label, use your docstring. - -Meanwhile, in the `workalendar/europe/__init__.py` file, add these snippets where needed: - -```python -from .zhraa import Zhraa -# ... -__all__ = ( - 'Belgium', - 'CzechRepublic', - # ... - 'Zhraa', -) -``` - -Now, we're building a test class. Edit the `workalendar/tests/test_europe.py` file and add the following code: - -```python -from ..europe import Zhraa -# snip... - -class ZhraaTest(GenericCalendarTest): - cal_class = Zhraa - - def test_year_2014(self): - holidays = self.cal.holidays_set(2014) - self.assertIn(date(2014, 1, 1), holidays) # new year - self.assertIn(date(2014, 5, 1), holidays) # labour day - self.assertIn(date(2014, 8, 2), holidays) # king birthday - self.assertIn(date(2014, 12, 25), holidays) # Xmas - # variable days - self.assertIn(date(2014, 4, 21), holidays) # easter monday - self.assertIn(date(2014, 6, 2), holidays) # First MON in June -``` - -of course, if you run the test using the `tox` or `py.test` command, this will fail, since we haven't implemented anything yet. - -Install tox using the following command: - -```sh -workon WORKALENDAR -pip install tox -``` - -With the `WesternCalendar` base class you have at least one holiday as a bonus: the New Year's day, which is almost a worldwide holiday. - -#### Add fixed days - -```python -class Zhraa(WesternCalendar): - - include_labour_day = True - FIXED_HOLIDAYS = WesternCalendar.FIXED_HOLIDAYS + ( - (8, 2, "King Birthday"), - ) -``` - -The `include_labour_day` is a flag common to all subclasses of `workalendar.core.Calendar`. Setting it to `True` activates the Labour Day for this calendar. Please note that there's a way to overwrite its label, using the `labour_day_label` class property. - -Now we've got 3 holidays out of 6. - - -#### Add religious holidays - -Since we're using `WesternCalendar` (it inherits from `ChristianMixin`) as a base to our `Zhraa` class, it automatically adds Christmas Day as a holiday. Now we can add Easter monday just by switching the correct flag. - -```python -from ..core import WesternCalendar - -class Zhraa(WesternCalendar): - include_easter_monday = True - include_labour_day = True - FIXED_HOLIDAYS = WesternCalendar.FIXED_HOLIDAYS + ( - (8, 2, "King Birthday"), - ) -``` -Almost there, 5 holidays out of 6. - -#### Add variable "non-usual" holidays - -There are many static methods that will grant you a clean access to variable days computation. It's very easy to add days like the "Birthday of the Founder": - -```python -class Zhraa(WesternCalendar): - include_easter_monday = True - include_labour_day = True - FIXED_HOLIDAYS = WesternCalendar.FIXED_HOLIDAYS + ( - (8, 2, "King Birthday"), - ) - - def get_variable_days(self, year): - # usual variable days - days = super().get_variable_days(year) - - days.append( - (Zhraa.get_nth_weekday_in_month(year, 6, MON), - 'Day of the Founder'), - ) - return days -``` - -Please mind that the returned `days` is a list of tuples. The first item being a date object (in the Python `datetime.date` sense) and the second one is the label string. - -#### Add you calendar to the global registry - -If you're adding a Country calendar that has an ISO code, you may want to add it to our global registry. - -Workalendar is providing a registry that you can use to query and fetch calendar based on their ISO code. For the current example, let's pretend that the Zhraa Kingdom ISO code is `ZK`. - -To register, add the following: - -```python -from ..registry_tools import iso_register - -@iso_register('ZK') -class Zhraa(WesternCalendar): - # The rest of your code... -``` - -#### You're done for the code! - -There you are. Commit with a nice commit message, test, make sure it works for the other years as well - there might be exceptions to the common rules - and you're almost there. - -#### The final steps - -Do not forget to: - -1. put the appropriate doctring in the Calendar class. -2. add your calendar in the `README.md` file, included in the appropriate continent. -3. add your calendar to the `Changelog.md` file. - -**Note** *Please, do NOT change the version number in the changelog or in the ``setup.py`` file. It's the project maintainers' duty to decide when to release and how to increment the version number, according to the impact of the changes.* - -We're planning to build a complete documentation for the other cases (special holiday rules, other calendar types, other religions, etc). But with this tutorial you're sorted for a lot of other calendars. - -## Other code contributions - -There are dozens of calendars all over the world. We'd appreciate you to contribute to the core of the library by adding some new Mixins or Calendars. - -Bear in mind that the code you'd provide **must** be tested using unittests before you submit your pull-request. - -[Home](index.md) / [Basic usage](basic.md) / [Advanced usage](advanced.md) / [Class options](class-options.md) / [ISO Registry](iso-registry.md) / [iCal Export](ical.md) diff --git a/docs/history.rst b/docs/history.rst new file mode 100644 index 00000000..8e217503 --- /dev/null +++ b/docs/history.rst @@ -0,0 +1,8 @@ +:tocdepth: 2 + +.. _changes: + +History +******* + +.. include:: ../CHANGES (links).rst diff --git a/docs/index.md b/docs/index.md index f0bca7bb..50917e45 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,8 +1,8 @@ -# Workalendar documentation +# Calendra documentation ## Overview -Workalendar is a Python module that offers classes able to handle calendars, list legal / religious holidays and gives working-day-related computation functions. +Calendra is a Python module that offers classes able to handle calendars, list legal / religious holidays and gives working-day-related computation functions. ## Status @@ -10,7 +10,7 @@ This library is ready for production, although we may warn eventual users: some ## Available Calendars -See [the repository README](https://github.com/workalendar/workalendar#available-calendars) for the most up-to-date catalog of the available calendars. +See [the repository README](https://github.com/jaraco/calendra#available-calendars) for the most up-to-date catalog of the available calendars. ## Usage examples diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 00000000..ff51c8f0 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,24 @@ +Welcome to |project| documentation! +=================================== + +.. toctree:: + :maxdepth: 1 + + history + +See the `workalendar docs `_ +for more details. + +.. automodule:: calendra + :members: + :undoc-members: + :show-inheritance: + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 00000000..976ba029 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,2 @@ +[mypy] +ignore_missing_imports = True diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..60de2424 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,20 @@ +[build-system] +requires = ["setuptools>=56", "setuptools_scm[toml]>=3.4.1"] +build-backend = "setuptools.build_meta" + +[tool.black] +skip-string-normalization = true + +[tool.setuptools_scm] + +[tool.pytest-enabler.black] +addopts = "--black" + +[tool.pytest-enabler.mypy] +addopts = "--mypy" + +[tool.pytest-enabler.flake8] +addopts = "--flake8" + +[tool.pytest-enabler.cov] +addopts = "--cov" diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000..80e98cc9 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,17 @@ +[pytest] +norecursedirs=dist build .tox .eggs +addopts=--doctest-modules +doctest_optionflags=ALLOW_UNICODE ELLIPSIS +filterwarnings= + # Suppress deprecation warning in flake8 + ignore:SelectableGroups dict interface is deprecated::flake8 + + # shopkeep/pytest-black#55 + ignore: is not using a cooperative constructor:pytest.PytestDeprecationWarning + ignore:The \(fspath. py.path.local\) argument to BlackItem is deprecated.:pytest.PytestDeprecationWarning + ignore:BlackItem is an Item subclass and should not be a collector:pytest.PytestWarning + + # tholo/pytest-flake8#83 + ignore: is not using a cooperative constructor:pytest.PytestDeprecationWarning + ignore:The \(fspath. py.path.local\) argument to Flake8Item is deprecated.:pytest.PytestDeprecationWarning + ignore:Flake8Item is an Item subclass and should not be a collector:pytest.PytestWarning diff --git a/requirements.astronomy.txt b/requirements.astronomy.txt deleted file mode 100644 index efd461af..00000000 --- a/requirements.astronomy.txt +++ /dev/null @@ -1,3 +0,0 @@ -skyfield -skyfield-data -tqdm diff --git a/setup.cfg b/setup.cfg index eed05a8f..f5c38e95 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,40 +1,70 @@ [metadata] -name = workalendar -version = 16.3.0 +name = calendra +author = Jason R. Coombs +author_email = jaraco@jaraco.com description = Worldwide holidays and working days helper and toolkit. -author = Bruno Bord -author_email = bruno@jehaisleprintemps.net -url = https://github.com/workalendar/workalendar -long_description = file: README.md +long_description = file:README.md long_description_content_type = text/markdown -license = MIT License +url = https://github.com/jaraco/calendra classifiers = - Development Status :: 5 - Production/Stable - Intended Audience :: Developers - License :: OSI Approved :: MIT License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - + Development Status :: 5 - Production/Stable + Intended Audience :: Developers + License :: OSI Approved :: MIT License + Programming Language :: Python :: 3 + Programming Language :: Python :: 3 :: Only [options] -python_requires = >=3.6 -zip_safe = False -include_package_data = True -packages = find: +packages = find_namespace: +include_package_data = true +python_requires = >=3.7 install_requires = - python-dateutil - lunardate + python-dateutil + lunardate backports.zoneinfo;python_version<"3.9" tzdata;platform_system=="Windows" - convertdate - pyluach - importlib-metadata;python_version < "3.8" + convertdate + pyluach + importlib_metadata; python_version < "3.8" + more_itertools [options.extras_require] +testing = + # upstream + pytest >= 3.5, !=3.7.3 + pytest-checkdocs >= 2.4 + pytest-flake8 + # workaround for tholo/pytest-flake8#87 + flake8 < 5 + # disabled for easier merging + # pytest-black >= 0.3.7; \ + # workaround for jaraco/skeleton#22 + # python_implementation != "PyPy" + pytest-cov + # disabled because there are 88 failures + # pytest-mypy >= 0.9.1; \ + # workaround for jaraco/skeleton#22 + # python_implementation != "PyPy" + pytest-enabler >= 1.3 + + # local + pygments + pytest-cov + pytest-pep8 + pandas + jaraco.test >= 3.2.0 + freezegun + +docs = + # upstream + sphinx + jaraco.packaging >= 9 + rst.linker >= 1.9 + + # local + astronomy = skyfield skyfield-data + tqdm + +[options.entry_points] diff --git a/setup.py b/setup.py deleted file mode 100644 index 7fccbf3b..00000000 --- a/setup.py +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env python -from setuptools import setup - - -if __name__ == '__main__': - setup() diff --git a/tox.ini b/tox.ini index 23e4a057..db0b6c3c 100644 --- a/tox.ini +++ b/tox.ini @@ -1,31 +1,41 @@ [tox] -envlist = pyupgrade,flake8,py36,py37,py38,py39 +envlist = python +minversion = 3.6 +# https://github.com/jaraco/skeleton/issues/6 +tox_pip_extensions_ext_venv_update = true +toxworkdir={env:TOX_WORK_DIR:.tox} + [testenv] deps = - pytest - pandas - pytest-cov - freezegun - -rrequirements.astronomy.txt - -commands_pre = - python setup.py develop - python --version commands = - py.test --cov=workalendar --cov-report term-missing:skip-covered {posargs: workalendar} + pytest {posargs} +usedevelop = True +extras = + testing + astronomy -[testenv:flake8] -deps = - flake8 -commands_pre = -skip_install = true -commands = flake8 workalendar +[testenv:docs] +extras = + docs + testing +changedir = docs +commands = + python -m sphinx -W --keep-going . {toxinidir}/build/html -[testenv:pyupgrade] +[testenv:release] +skip_install = True deps = - pyupgrade - pyupgrade-directories -commands_pre = -skip_install = true -commands = pyup_dirs --py36-plus --recursive . + build + twine>=3 + jaraco.develop>=7.1 +passenv = + TWINE_PASSWORD + GITHUB_TOKEN +setenv = + TWINE_USERNAME = {env:TWINE_USERNAME:__token__} +commands = + python -c "import shutil; shutil.rmtree('dist', ignore_errors=True)" + python -m build + python -m twine upload dist/* + python -m jaraco.develop.create-github-release diff --git a/workalendar/__init__.py b/workalendar/__init__.py deleted file mode 100644 index a5c781a2..00000000 --- a/workalendar/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -import sys - -if sys.version_info[:2] < (3, 8): # coverage: exclude - import importlib_metadata -else: # coverage: exclude - import importlib.metadata as importlib_metadata - -try: - #: Module version, as defined in PEP-0396. - __version__ = importlib_metadata.version(__name__) -except importlib_metadata.PackageNotFoundError: # pragma: no cover - __version__ = "unknown" diff --git a/workalendar/oceania/new_zealand.py b/workalendar/oceania/new_zealand.py deleted file mode 100644 index a3bc6bda..00000000 --- a/workalendar/oceania/new_zealand.py +++ /dev/null @@ -1,85 +0,0 @@ -from datetime import date, timedelta - -from ..core import WesternCalendar, MON, SAT, SUN -from ..registry_tools import iso_register - - -@iso_register("NZ") -class NewZealand(WesternCalendar): - "New Zealand" - include_good_friday = True - include_easter_monday = True - include_boxing_day = True - - FIXED_HOLIDAYS = WesternCalendar.FIXED_HOLIDAYS + ( - (1, 2, "Day after New Year's Day"), - (2, 6, "Waitangi Day"), - (4, 25, "ANZAC Day") - ) - - def get_queens_birthday(self, year): - return ( - NewZealand.get_nth_weekday_in_month(year, 6, MON, 1), - "Queen's Birthday" - ) - - def get_labour_day(self, year): - return ( - NewZealand.get_nth_weekday_in_month(year, 10, MON, 4), - "Labour Day" - ) - - def get_variable_days(self, year): - # usual variable days - days = super().get_variable_days(year) - days.append(self.get_queens_birthday(year)) - days.append(self.get_labour_day(year)) - - waitangi_day = date(year, 2, 6) - if waitangi_day.weekday() in self.get_weekend_days(): - days.append(( - self.find_following_working_day(waitangi_day), - "Waitangi Day Shift") - ) - - anzac_day = date(year, 4, 25) - if anzac_day.weekday() in self.get_weekend_days(): - days.append(( - self.find_following_working_day(anzac_day), - "ANZAC Day Shift") - ) - - christmas = date(year, 12, 25) - boxing_day = date(year, 12, 26) - if christmas.weekday() == SAT: - shift = self.find_following_working_day(christmas) - days.append((shift, "Christmas Shift")) - elif christmas.weekday() == SUN: - shift = self.find_following_working_day(christmas) - days.append((shift + timedelta(days=1), "Christmas Shift")) - - if boxing_day.weekday() == SAT: - shift = self.find_following_working_day(boxing_day) - days.append((shift, "Boxing Day Shift")) - elif boxing_day.weekday() == SUN: - shift = self.find_following_working_day(boxing_day) - days.append((shift + timedelta(days=1), "Boxing Day Shift")) - - new_year = date(year, 1, 1) - day_after_new_year = date(year, 1, 2) - if new_year.weekday() == SAT: - shift = self.find_following_working_day(new_year) - days.append((shift, "New Year Shift")) - elif new_year.weekday() == SUN: - shift = self.find_following_working_day(new_year) - days.append((shift + timedelta(days=1), "New Year Shift")) - - if day_after_new_year.weekday() == SAT: - shift = self.find_following_working_day(day_after_new_year) - days.append((shift, "Day after New Year's Day Shift")) - elif day_after_new_year.weekday() == SUN: - shift = self.find_following_working_day(day_after_new_year) - days.append((shift + timedelta(days=1), - "Day after New Year's Day Shift")) - - return days