├── tests ├── __init__.py ├── test_gematria.py ├── test_parshios.py ├── test_dates.py └── test_hebrewcal.py ├── docs ├── changelog.rst ├── requirements.txt ├── dates.rst ├── hebrewcal.rst ├── parshios.rst ├── index.rst ├── Makefile ├── make.bat └── conf.py ├── setup.cfg ├── .gitignore ├── src └── pyluach │ ├── __init__.py │ ├── gematria.py │ ├── utils.py │ ├── parshios.py │ ├── dates.py │ └── hebrewcal.py ├── .readthedocs.yaml ├── requirements.txt ├── .github └── workflows │ └── testing-and-coverage.yml ├── license.txt ├── pyproject.toml ├── README.rst └── CHANGELOG.rst /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CHANGELOG.rst -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | Sphinx~=6.1.3 2 | sphinx_rtd_theme -------------------------------------------------------------------------------- /docs/dates.rst: -------------------------------------------------------------------------------- 1 | dates module 2 | ============ 3 | 4 | .. automodule:: pyluach.dates 5 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = ENV,dist,docs,tests,build 3 | extend-ignore = E228 4 | -------------------------------------------------------------------------------- /docs/hebrewcal.rst: -------------------------------------------------------------------------------- 1 | hebrewcal module 2 | ================ 3 | 4 | .. automodule:: pyluach.hebrewcal 5 | -------------------------------------------------------------------------------- /docs/parshios.rst: -------------------------------------------------------------------------------- 1 | parshios module 2 | =============== 3 | 4 | .. automodule:: pyluach.parshios 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .* 2 | !.gitignore 3 | !.github/ 4 | *.pyc 5 | *~ 6 | ENV* 7 | *egg* 8 | *build* 9 | *.whl 10 | dist/* 11 | *htmlcov* 12 | -------------------------------------------------------------------------------- /src/pyluach/__init__.py: -------------------------------------------------------------------------------- 1 | """A Python package for dealing with Hebrew (Jewish) calendar dates. 2 | """ 3 | 4 | 5 | __version__ = '2.3.0' 6 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to pyluach's documentation! 2 | =================================== 3 | 4 | .. include:: ../README.rst 5 | 6 | .. toctree:: 7 | :hidden: 8 | 9 | self 10 | 11 | .. toctree:: 12 | :maxdepth: 1 13 | :caption: Contents: 14 | 15 | dates 16 | hebrewcal 17 | parshios 18 | changelog 19 | 20 | Indices and tables 21 | ================== 22 | 23 | * :ref:`genindex` 24 | * :ref:`modindex` 25 | * :ref:`search` 26 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-22.04 11 | tools: 12 | python: "3.11" 13 | 14 | # Build documentation in the docs/ directory with Sphinx 15 | sphinx: 16 | configuration: docs/conf.py 17 | 18 | # If using Sphinx, optionally build your docs in additional formats such as PDF 19 | formats: 20 | - htmlzip 21 | - pdf 22 | - epub 23 | 24 | # Optionally declare the Python requirements required to build your docs 25 | python: 26 | install: 27 | - method: pip 28 | path: . 29 | extra_requirements: 30 | - doc 31 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /tests/test_gematria.py: -------------------------------------------------------------------------------- 1 | from pyluach.gematria import _num_to_str 2 | 3 | 4 | def test_one_letter(): 5 | assert _num_to_str(5) == 'ה׳' 6 | assert _num_to_str(10) == 'י׳' 7 | assert _num_to_str(200) == 'ר׳' 8 | 9 | 10 | def test_two_letters(): 11 | assert _num_to_str(18) == 'י״ח' 12 | assert _num_to_str(15) == 'ט״ו' 13 | assert _num_to_str(16) == 'ט״ז' 14 | assert _num_to_str(101) == 'ק״א' 15 | 16 | 17 | def test_three_letters(): 18 | assert _num_to_str(127) == 'קכ״ז' 19 | assert _num_to_str(489) == 'תפ״ט' 20 | assert _num_to_str(890) == 'תת״צ' 21 | 22 | 23 | def test_four_letters(): 24 | assert _num_to_str(532) == 'תקל״ב' 25 | 26 | 27 | def test_five_letters(): 28 | assert _num_to_str(916) == 'תתקט״ז' 29 | 30 | 31 | def test_thousands(): 32 | assert _num_to_str(5781, True) == 'ה׳תשפ״א' 33 | assert _num_to_str(10000, True) == 'י׳' 34 | assert _num_to_str(12045, True) == 'יב׳מ״ה' 35 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | alabaster==0.7.13 2 | attrs==22.2.0 3 | Babel==2.11.0 4 | beautifulsoup4==4.11.2 5 | certifi==2022.12.7 6 | charset-normalizer==3.0.1 7 | coverage==7.1.0 8 | docutils==0.19 9 | exceptiongroup==1.1.0 10 | flake8==6.0.0 11 | flit==3.8.0 12 | flit-core==3.8.0 13 | idna==3.4 14 | imagesize==1.4.1 15 | importlib-metadata==6.0.0 16 | iniconfig==2.0.0 17 | Jinja2==3.1.2 18 | MarkupSafe==2.1.2 19 | mccabe==0.7.0 20 | packaging==23.0 21 | pluggy==1.0.0 22 | pycodestyle==2.10.0 23 | pyflakes==3.0.1 24 | Pygments==2.14.0 25 | pyluach==2.1.0 26 | pytest==7.2.1 27 | pytest-cov==4.0.0 28 | pytz==2022.7.1 29 | requests==2.28.2 30 | snowballstemmer==2.2.0 31 | soupsieve==2.3.2.post1 32 | sphinx==6.1.3 33 | sphinx-rtd-theme==1.2.0 34 | sphinxcontrib-applehelp==1.0.4 35 | sphinxcontrib-devhelp==1.0.2 36 | sphinxcontrib-htmlhelp==2.0.1 37 | sphinxcontrib-jquery==2.0.0 38 | sphinxcontrib-jsmath==1.0.1 39 | sphinxcontrib-qthelp==1.0.3 40 | sphinxcontrib-serializinghtml==1.1.5 41 | tomli==2.0.1 42 | tomli-w==1.0.0 43 | urllib3==1.26.14 44 | zipp==3.13.0 45 | -------------------------------------------------------------------------------- /.github/workflows/testing-and-coverage.yml: -------------------------------------------------------------------------------- 1 | name: pytest 2 | on: 3 | push: 4 | branches: 5 | - master 6 | - dev 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: setup python 16 | uses: actions/setup-python@v5 17 | with: 18 | python-version: ${{matrix.python-version}} 19 | - name: install 20 | run: | 21 | python -m pip install --upgrade pip 22 | pip install pytest pytest-cov beautifulsoup4 23 | pip install -e ./ 24 | - name: run-tests 25 | run: | 26 | pytest --cov=src/pyluach tests/ 27 | - name: Coveralls Parallel 28 | uses: coverallsapp/github-action@v2 29 | with: 30 | flag-name: ${{ matrix.python-version }} 31 | parallel: true 32 | finish: 33 | needs: test 34 | if: ${{ always() }} 35 | runs-on: ubuntu-latest 36 | steps: 37 | - name: Coveralls Finished 38 | uses: coverallsapp/github-action@v2 39 | with: 40 | parallel-finished: true 41 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | [OSI Approved License] 3 | The MIT License (MIT) 4 | 5 | Copyright (c) 2014 Meir S. List 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["flit_core >=3.2,<4"] 3 | build-backend = "flit_core.buildapi" 4 | 5 | [project] 6 | name = "pyluach" 7 | authors = [ 8 | {name = "MS List", email = "simlist@gmail.com"} 9 | ] 10 | license = {file = "license.txt"} 11 | dynamic = ["description", "version"] 12 | readme = "README.rst" 13 | requires-python = ">=3.8" 14 | classifiers=[ 15 | "Development Status :: 5 - Production/Stable", 16 | "Intended Audience :: Developers", 17 | "Topic :: Software Development :: Libraries :: Python Modules", 18 | "License :: OSI Approved :: MIT License", 19 | "Programming Language :: Python :: 3", 20 | "Programming Language :: Python :: 3 :: Only", 21 | "Programming Language :: Python :: 3.8", 22 | "Programming Language :: Python :: 3.9", 23 | "Programming Language :: Python :: 3.10", 24 | "Programming Language :: Python :: 3.11", 25 | "Programming Language :: Python :: 3.12", 26 | "Programming Language :: Python :: 3.13", 27 | ] 28 | keywords = [ 29 | 'hebrew', 'calendar', 'jewish', 'luach', 'gregorian', 'julian', 30 | 'days', 'dates', 'date', 'conversion', 'parsha', 'holiday' 31 | ] 32 | 33 | [project.optional-dependencies] 34 | test = [ 35 | "pytest", 36 | "pytest-cov", 37 | "flake8", 38 | "beautifulsoup4" 39 | ] 40 | doc = ["sphinx ~= 6.1.3", "sphinx_rtd_theme ~= 1.2.0"] 41 | 42 | [project.urls] 43 | Documentation = "https://readthedocs.org/projects/pyluach/" 44 | Source = "https://github.com/simlist/pyluach" 45 | 46 | [tool.flit.sdist] 47 | exclude = ["tests", "docs", ".github", "requirements.txt", ".gitignore"] 48 | 49 | [tool.pytest.ini_options] 50 | minversion = "6.0" 51 | addopts = "--cov=src/pyluach" 52 | testpaths = ["tests",] 53 | -------------------------------------------------------------------------------- /src/pyluach/gematria.py: -------------------------------------------------------------------------------- 1 | _GEMATRIOS = { 2 | 1: 'א', 3 | 2: 'ב', 4 | 3: 'ג', 5 | 4: 'ד', 6 | 5: 'ה', 7 | 6: 'ו', 8 | 7: 'ז', 9 | 8: 'ח', 10 | 9: 'ט', 11 | 10: 'י', 12 | 20: 'כ', 13 | 30: 'ל', 14 | 40: 'מ', 15 | 50: 'נ', 16 | 60: 'ס', 17 | 70: 'ע', 18 | 80: 'פ', 19 | 90: 'צ', 20 | 100: 'ק', 21 | 200: 'ר', 22 | 300: 'ש', 23 | 400: 'ת' 24 | } 25 | 26 | 27 | def _stringify_gematria(letters): 28 | """Insert geresh or gershayim symbols into gematria.""" 29 | length = len(letters) 30 | if length > 1: 31 | return f'{letters[:-1]}״{letters[-1]}' 32 | if length == 1: 33 | return f'{letters}׳' 34 | return '' 35 | 36 | 37 | def _get_letters(num): 38 | """Convert numbers under 1,000 into raw letters.""" 39 | ones = num % 10 40 | tens = num % 100 - ones 41 | hundreds = num % 1000 - tens - ones 42 | four_hundreds = ''.join(['ת' for i in range(hundreds // 400)]) 43 | ones = _GEMATRIOS.get(ones, '') 44 | tens = _GEMATRIOS.get(tens, '') 45 | hundreds = _GEMATRIOS.get(hundreds % 400, '') 46 | letters = f'{four_hundreds}{hundreds}{tens}{ones}' 47 | return letters.replace('יה', 'טו').replace('יו', 'טז') 48 | 49 | 50 | def _num_to_str(num, thousands=False, withgershayim=True): 51 | """Return gematria string for number. 52 | 53 | Parameters 54 | ---------- 55 | num : int 56 | The number to get the Hebrew letter representation 57 | thousands : bool, optional 58 | True if the hebrew returned should include a letter for the 59 | thousands place ie. 'ה׳' for five thousand. 60 | 61 | Returns 62 | ------- 63 | str 64 | The Hebrew representation of the number. 65 | """ 66 | letters = _get_letters(num) 67 | if withgershayim: 68 | letters = _stringify_gematria(letters) 69 | if thousands: 70 | thousand = _get_letters(num // 1000) 71 | if withgershayim: 72 | thousand = ''.join([thousand, '׳']) 73 | letters = ''.join([thousand, letters]) 74 | return letters 75 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | 13 | import sphinx_rtd_theme 14 | 15 | 16 | # -- Project information ----------------------------------------------------- 17 | 18 | project = 'pyluach' 19 | copyright = '2016, MS List' 20 | author = 'MS List' 21 | 22 | # The full version, including alpha/beta/rc tags 23 | release = '2.3.0' 24 | 25 | 26 | # -- General configuration --------------------------------------------------- 27 | 28 | # Add any Sphinx extension module names here, as strings. They can be 29 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 30 | # ones. 31 | extensions = [ 32 | 'sphinx.ext.autodoc', 33 | 'sphinx.ext.viewcode', 34 | 'sphinx.ext.autosummary', 35 | 'sphinx.ext.intersphinx', 36 | 'sphinx.ext.napoleon', 37 | 'sphinx_rtd_theme', 38 | ] 39 | 40 | # Napolean settings 41 | napoleon_include_special_with_doc = False 42 | 43 | # Autodoc settings 44 | autodoc_default_options = { 45 | 'members': True, 46 | 'inherited-members': True, 47 | 'show-inheritance': True, 48 | 'member-order': 'bysource' 49 | } 50 | autodoc_member_order = 'bysource' 51 | 52 | # Autosummary settings 53 | autosummary_generate = True 54 | 55 | # Intersphinx settings 56 | intersphinx_mapping = {'python': ('https://docs.python.org/3', None)} 57 | 58 | # Add any paths that contain templates here, relative to this directory. 59 | templates_path = ['_templates'] 60 | 61 | # List of patterns, relative to source directory, that match files and 62 | # directories to ignore when looking for source files. 63 | # This pattern also affects html_static_path and html_extra_path. 64 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 65 | 66 | 67 | # -- Options for HTML output ------------------------------------------------- 68 | 69 | # The theme to use for HTML and HTML Help pages. See the documentation for 70 | # a list of builtin themes. 71 | # 72 | html_theme = 'sphinx_rtd_theme' 73 | 74 | # Add any paths that contain custom static files (such as style sheets) here, 75 | # relative to this directory. They are copied after the builtin static files, 76 | # so a file named "default.css" will overwrite the builtin "default.css". 77 | html_static_path = ['_static'] 78 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | pyluach 2 | ======= 3 | .. image:: https://readthedocs.org/projects/pyluach/badge/?version=stable 4 | :target: http://pyluach.readthedocs.io/en/latest/?badge=stable 5 | :alt: Documentation Status 6 | .. image:: https://github.com/simlist/pyluach/actions/workflows/testing-and-coverage.yml/badge.svg?branch=master 7 | :target: https://github.com/simlist/pyluach/actions/workflows/testing-and-coverage.yml 8 | .. image:: https://coveralls.io/repos/github/simlist/pyluach/badge.svg?branch=master 9 | :target: https://coveralls.io/github/simlist/pyluach?branch=dev 10 | 11 | Pyluach is a Python package for dealing with Hebrew (Jewish) calendar dates. 12 | 13 | Features 14 | --------- 15 | * Conversion between Hebrew and Gregorian dates 16 | * Finding the difference between two dates 17 | * Finding a date at a given duration from the given date 18 | * Rich comparisons between dates 19 | * Finding the weekday of a given date 20 | * Finding the weekly Parsha reading of a given date 21 | * Getting the holiday occuring on a given date 22 | * Generating html and text Hebrew calendars 23 | 24 | Installation 25 | ------------- 26 | Use ``pip install pyluach``. 27 | 28 | Documentation 29 | ------------- 30 | Documentation for pyluach can be found at https://readthedocs.org/projects/pyluach/. 31 | 32 | Examples 33 | ------------ 34 | :: 35 | 36 | >>> from pyluach import dates, hebrewcal, parshios 37 | 38 | >>> today = dates.HebrewDate.today() 39 | >>> lastweek_gregorian = (today - 7).to_greg() 40 | >>> lastweek_gregorian < today 41 | True 42 | >>> today - lastweek_gregorian 43 | 7 44 | >>> greg = dates.GregorianDate(1986, 3, 21) 45 | >>> heb = dates.HebrewDate(5746, 13, 10) 46 | >>> greg == heb 47 | True 48 | 49 | >>> purim = dates.HebrewDate(5781, 12, 14) 50 | >>> purim.hebrew_day() 51 | 'י״ד' 52 | >>> purim.hebrew_date_string() 53 | 'י״ד אדר תשפ״א' 54 | >>> purim.hebrew_date_string(True) 55 | 'י״ד אדר ה׳תשפ״א' 56 | 57 | >>> rosh_hashana = dates.HebrewDate(5782, 7, 1) 58 | >>> rosh_hashana.holiday() 59 | 'Rosh Hashana' 60 | >>> rosh_hashana.holiday(hebrew=True) 61 | 'ראש השנה' 62 | >>> (rosh_hashana + 3).holiday() 63 | None 64 | 65 | >>> month = hebrewcal.Month(5781, 10) 66 | >>> month.month_name() 67 | 'Teves' 68 | >>> month.month_name(True) 69 | 'טבת' 70 | >>> month + 3 71 | Month(5781, 1) 72 | >>> for month in hebrewcal.Year(5774).itermonths(): 73 | ... print(month.month_name()) 74 | Tishrei Cheshvan ... 75 | 76 | >>> date = dates.GregorianDate(2010, 10, 6) 77 | >>> parshios.getparsha(date) 78 | [0] 79 | >>> parshios.getparsha_string(date, israel=True) 80 | 'Beraishis' 81 | >>> parshios.getparsha_string(date, hebrew=True) 82 | 'בראשית' 83 | >>> new_date = dates.GregorianDate(2021, 3, 10) 84 | >>> parshios.getparsha_string(new_date) 85 | 'Vayakhel, Pekudei' 86 | >>> parshios.getparsha_string(new_date, hebrew=True) 87 | 'ויקהל, פקודי' 88 | 89 | Contact 90 | -------- 91 | For questions and comments please `raise an issue in github 92 | `_ or contact me at 93 | simlist@gmail.com. 94 | 95 | License 96 | -------- 97 | Pyluach is licensed under the MIT license. -------------------------------------------------------------------------------- /tests/test_parshios.py: -------------------------------------------------------------------------------- 1 | from pyluach import parshios, dates 2 | from pyluach.parshios import _FourParshiosEnum 3 | 4 | 5 | KNOWN_VALUES = { 6 | (2016, 1, 7): [13], 7 | (2017, 3, 21): [21, 22], 8 | (2017, 9, 26): None, 9 | (2020, 9, 19): None, 10 | } 11 | 12 | KNOWN_VALUES_STRINGS = { 13 | (2016, 1, 7): "Va'eira", 14 | (2017, 3, 21): "Vayakhel, Pekudei", 15 | (2017, 9, 26): None 16 | } 17 | 18 | 19 | class TestGetParsha: 20 | 21 | def test_getparsha(self): 22 | for key, value in KNOWN_VALUES.items(): 23 | assert parshios.getparsha(dates.GregorianDate(*key)) == value 24 | 25 | def test_getparsha_string(self): 26 | for key, value in KNOWN_VALUES_STRINGS.items(): 27 | assert ( 28 | parshios.getparsha_string(dates.GregorianDate(*key)) == value 29 | ) 30 | 31 | def test_chukas_balak(self): 32 | chukas_balak = dates.HebrewDate(5780, 4, 12) 33 | assert parshios.getparsha(chukas_balak) == [38, 39] 34 | assert parshios.getparsha(chukas_balak, True) == [39, ] 35 | assert parshios.getparsha(chukas_balak - 8) == [37, ] 36 | assert parshios.getparsha(chukas_balak - 13, True) == [38, ] 37 | shavuos = dates.HebrewDate(5780, 3, 6) 38 | assert parshios.getparsha_string(shavuos, True) == 'Nasso' 39 | assert parshios.getparsha_string(shavuos) is None 40 | assert parshios. getparsha_string(shavuos + 7, True) == "Beha'aloscha" 41 | assert parshios.getparsha_string(shavuos + 7) == 'Nasso' 42 | 43 | def test_eighth_day_pesach(self): 44 | eighth_day_pesach = dates.HebrewDate(5779, 1, 22) 45 | reunion_shabbos = dates.HebrewDate(5779, 5, 2) 46 | assert parshios.getparsha_string(eighth_day_pesach) is None 47 | assert ( 48 | parshios.getparsha_string(eighth_day_pesach, True) == 'Acharei Mos' 49 | ) 50 | assert parshios.getparsha(eighth_day_pesach + 7) == [28] 51 | assert parshios.getparsha(eighth_day_pesach + 7, True) == [29] 52 | assert parshios.getparsha_string(reunion_shabbos) == "Mattos, Masei" 53 | assert parshios.getparsha_string(reunion_shabbos, True) == 'Masei' 54 | 55 | 56 | def test_parshatable(): 57 | assert parshios.parshatable(5777) == parshios._gentable(5777) 58 | assert parshios.parshatable(5778, True) == parshios._gentable(5778, True) 59 | 60 | 61 | def test_iterparshios(): 62 | year = 5776 63 | parshalist = list(parshios.parshatable(year).values()) 64 | index = 0 65 | for p in parshios.iterparshios(year): 66 | assert p == parshalist[index] 67 | index += 1 68 | 69 | 70 | def test_get_parshastring_hebrew(): 71 | date = dates.HebrewDate(5781, 3, 28) 72 | assert parshios.getparsha_string(date, hebrew=True) == 'קרח' 73 | date2 = dates.GregorianDate(2021, 7, 10) 74 | assert parshios.getparsha_string(date2, hebrew=True) == 'מטות, מסעי' 75 | 76 | 77 | def test_shekalim(): 78 | date = dates.HebrewDate(5785, 11, 25) 79 | assert ( 80 | parshios._get_four_parshios(date) == _FourParshiosEnum.SHEKALIM 81 | ) 82 | assert parshios._get_four_parshios(date - 1) is None 83 | assert parshios._get_four_parshios(date + 7) != _FourParshiosEnum.SHEKALIM 84 | 85 | 86 | def test_zachor(): 87 | date = dates.HebrewDate(5785, 12, 2) 88 | assert ( 89 | parshios._get_four_parshios(date) == _FourParshiosEnum.ZACHOR 90 | ) 91 | 92 | 93 | def test_parah(): 94 | date = dates.HebrewDate(5785, 12, 21) 95 | assert parshios.four_parshios(date) == 'Parah' 96 | date = dates.HebrewDate(5784, 13, 14) 97 | assert parshios.four_parshios(date, hebrew=True) == 'פרה' 98 | assert parshios.four_parshios(date - 1) != 'Parah' 99 | date = dates.GregorianDate(2025, 3, 9) 100 | assert parshios.four_parshios(date) == '' 101 | 102 | 103 | def test_hachodesh(): 104 | date = dates.HebrewDate(5785, 12, 29) 105 | assert parshios._get_four_parshios(date) == _FourParshiosEnum.HACHODESH 106 | date = dates.HebrewDate(5782, 1, 1) 107 | assert parshios._get_four_parshios(date) == _FourParshiosEnum.HACHODESH 108 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | Changelog 3 | ========== 4 | 5 | This document records all notable changes to `pyluach `_. 6 | This project adheres to `Semantic Versioning `_. 7 | 8 | `2.3.0`_ (2025-09-08) 9 | ===================== 10 | * Created ``get_four_parshios`` function in parshios module. 11 | * Added support for python 3.12, and 3.13. 12 | * Removed support for python v3.7. 13 | * Changed to use IntEnum for parshios to make code more readable. 14 | 15 | `2.2.0`_ (2023-02-28) 16 | ===================== 17 | * Added `prefix_day` param to ``festival`` and ``holiday`` methods and 18 | functions. 19 | 20 | `2.1.0`_ (2023-02-12) 21 | ===================== 22 | * Added ``add`` and ``subtract`` methods to ``dates.HebrewDate``. 23 | * Added ``replace`` method to ``CalendarDateMixin``. 24 | * Added missing documentation for `%y` and `%Y` in formatting 25 | ``HebrewDate``. 26 | 27 | `2.0.2`_ (2022-10-24) 28 | ===================== 29 | * Fix subtracting one date from another returning ``float`` instead of ``int``. 30 | 31 | `2.0.1`_ (2022-08-24) 32 | ===================== 33 | * Fix issue (`#24`_) where Shavuos is returned in most months on day 7. 34 | 35 | `2.0.0`_ (2022-05-29) 36 | ===================== 37 | * Changed equality comparers to compare object identity on unmatched types. 38 | * Equal dates of different types will no longer be considered identical 39 | keys for dicts. 40 | * Added ``strftime`` and ``__format__`` methods to 41 | ``dates.GregorianDate``. 42 | * Added ``__format__`` method to ``dates.HebrewDate``. 43 | * Added `withgershayim` parameter to ``dates.HebrewDate.hebrew_day`` and 44 | ``dates.HebrewDate.hebrew_year`` methods 45 | * Added ``monthcount`` method to ``hebrewcal.Year``. 46 | * Removed deprecated ``hebrewcal.Month.name`` attribute. 47 | * Implemented HebrewCalendar classes for generating calendars similar to 48 | Calendar classes in the standard library calendar module. 49 | 50 | `1.4.2`_ (2022-05-20) 51 | ===================== 52 | * Fixed bug in ``hebrewcal.Month`` comparisons when one month is before 53 | Nissan and one is not. 54 | 55 | `1.4.1`_ (2022-03-25) 56 | ===================== 57 | * Fixed mistakes in docstring and error message. 58 | 59 | `1.4.0`_ (2022-02-21) 60 | ===================== 61 | * Added parameter `include_working_days` to ``festival`` method and function. 62 | * Removed support for python < 3.6 63 | 64 | `1.3.0`_ (2021-06-09) 65 | ===================== 66 | * Added option to get parsha in Hebrew. 67 | * Added ``dates.HebrewDate`` methods to get hebrew day, month, year, and 68 | date string in Hebrew. 69 | * Added method to get ``hebrewcal.Month`` names in Hebrew. 70 | * Added methods to get year and month strings in Hebrew. 71 | * Added classmethods to ``hebrewcal.Year`` and ``hebrewcal.Month`` to get 72 | objects from dates and pydates. 73 | * Added methods to dates classes to get holidays, fast days and festivals. 74 | * Implemented more consistent Hebrew to English transliterations for parshios. 75 | 76 | `1.2.1`_ (2020-11-08) 77 | ===================== 78 | * Fixed molad having weekday of ``0`` when it should be ``7``. 79 | 80 | `1.2.0`_ (2020-10-28) 81 | ===================== 82 | * Created ``isoweekday`` method for all date types in the ``dates`` module. 83 | * Created fast_day and festival functions (`#11`_) 84 | * Added Pesach Sheni to festival. 85 | 86 | `1.1.1`_ (2020-08-14) 87 | ===================== 88 | * Fixed error getting parsha of Shabbos on Rosh Hashana. 89 | 90 | 91 | `1.1.0`_ (2020-06-03) 92 | ===================== 93 | * Redesigned documentation. 94 | * Added ``molad`` and ``molad_announcement`` methods to ``hebrewcal.Month``. 95 | * Stopped supporting python < 3.4 and modernized code. 96 | 97 | 98 | `1.0.1`_ (2019-03-02) 99 | ===================== 100 | * Initial public release 101 | 102 | 103 | .. _`2.3.0`: https://github.com/simlist/pyluach/compare/v2.2.0...v2.3.0 104 | .. _`2.2.0`: https://github.com/simlist/pyluach/compare/v2.1.0...v2.2.0 105 | .. _`2.1.0`: https://github.com/simlist/pyluach/compare/v2.0.2...v2.1.0 106 | .. _`2.0.2`: https://github.com/simlist/pyluach/compare/v2.0.1...v2.0.2 107 | .. _`2.0.1`: https://github.com/simlist/pyluach/compare/v2.0.0...v2.0.1 108 | .. _`2.0.0`: https://github.com/simlist/pyluach/compare/v1.4.2...v2.0.0 109 | .. _`1.4.2`: https://github.com/simlist/pyluach/compare/v1.4.1...v1.4.2 110 | .. _`1.4.1`: https://github.com/simlist/pyluach/compare/v1.4.0...v1.4.1 111 | .. _`1.4.0`: https://github.com/simlist/pyluach/compare/v1.3.0...v1.4.0 112 | .. _`1.3.0`: https://github.com/simlist/pyluach/compare/v1.2.1...v1.3.0 113 | .. _`1.2.1`: https://github.com/simlist/pyluach/compare/v1.2.0...v1.2.1 114 | .. _`1.2.0`: https://github.com/simlist/pyluach/compare/v1.1.1...v1.2.0 115 | .. _`1.1.1`: https://github.com/simlist/pyluach/compare/v1.1.0...v1.1.1 116 | .. _`1.1.0`: https://github.com/simlist/pyluach/compare/v1.0.1...v1.1.0 117 | .. _`1.0.1`: https://github.com/simlist/pyluach/releases/tag/v1.0.1 118 | 119 | .. _`#11`: https://github.com/simlist/pyluach/issues/11 120 | .. _`#24`: https://github.com/simlist/pyluach/issues/24 121 | -------------------------------------------------------------------------------- /src/pyluach/utils.py: -------------------------------------------------------------------------------- 1 | """The utils module contains shared functions and constants. 2 | 3 | They are to be used internally. 4 | """ 5 | from functools import lru_cache 6 | from enum import Enum 7 | 8 | 9 | class _Days(Enum): 10 | ROSH_HASHANA = 'Rosh Hashana' 11 | YOM_KIPPUR = 'Yom Kippur' 12 | SUCCOS = 'Succos' 13 | SHMINI_ATZERES = 'Shmini Atzeres' 14 | SIMCHAS_TORAH = 'Simchas Torah' 15 | CHANUKA = 'Chanuka' 16 | TU_BSHVAT = "Tu B'shvat" 17 | PURIM_KATAN = 'Purim Katan' 18 | PURIM = 'Purim' 19 | SHUSHAN_PURIM = 'Shushan Purim' 20 | PESACH = 'Pesach' 21 | PESACH_SHENI = 'Pesach Sheni' 22 | LAG_BAOMER = "Lag Ba'omer" 23 | SHAVUOS = 'Shavuos' 24 | TU_BAV = "Tu B'av" 25 | TZOM_GEDALIA = 'Tzom Gedalia' 26 | TENTH_OF_TEVES = '10 of Teves' 27 | TAANIS_ESTHER = 'Taanis Esther' 28 | SEVENTEENTH_OF_TAMUZ = '17 of Tamuz' 29 | NINTH_OF_AV = '9 of Av' 30 | 31 | 32 | _days_hebrew = { 33 | _Days.ROSH_HASHANA: 'ראש השנה', 34 | _Days.YOM_KIPPUR: 'יום כיפור', 35 | _Days.SUCCOS: 'סוכות', 36 | _Days.SHMINI_ATZERES: 'שמיני עצרת', 37 | _Days.SIMCHAS_TORAH: 'שמחת תורה', 38 | _Days.CHANUKA: 'חנוכה', 39 | _Days.TU_BSHVAT: 'ט״ו בשבט', 40 | _Days.PURIM_KATAN: 'פורים קטן', 41 | _Days.PURIM: 'פורים', 42 | _Days.SHUSHAN_PURIM: 'שושן פורים', 43 | _Days.PESACH: 'פסח', 44 | _Days.PESACH_SHENI: 'פסח שני', 45 | _Days.LAG_BAOMER: 'ל״ג בעומר', 46 | _Days.SHAVUOS: 'שבועות', 47 | _Days.TU_BAV: 'ט״ו באב', 48 | _Days.TZOM_GEDALIA: 'צום גדליה', 49 | _Days.TENTH_OF_TEVES: 'י׳ בטבת', 50 | _Days.TAANIS_ESTHER: 'תענית אסתר', 51 | _Days.SEVENTEENTH_OF_TAMUZ: 'י״ז בתמוז', 52 | _Days.NINTH_OF_AV: 'ט׳ באב' 53 | } 54 | 55 | 56 | MONTH_NAMES = [ 57 | 'Nissan', 'Iyar', 'Sivan', 'Tammuz', 'Av', 'Elul', 'Tishrei', 'Cheshvan', 58 | 'Kislev', 'Teves', 'Shevat', 'Adar', 'Adar 1', 'Adar 2'] 59 | 60 | MONTH_NAMES_HEBREW = [ 61 | 'ניסן', 'אייר', 'סיון', 'תמוז', 'אב', 'אלול', 'תשרי', 'חשון', 'כסלו', 62 | 'טבת', 'שבט', 'אדר', 'אדר א׳', 'אדר ב׳'] 63 | 64 | FAST_DAYS = [ 65 | 'Tzom Gedalia', '10 of Teves', 'Taanis Esther', '17 of Tamuz', '9 of Av'] 66 | 67 | FAST_DAYS_HEBREW = [ 68 | 'צום גדליה', 'י׳ בטבת', 'תענית אסתר', 'י״ז בתמוז', 'ט׳ באב'] 69 | 70 | FESTIVALS = [ 71 | 'Rosh Hashana', 'Yom Kippur', 'Succos', 'Shmini Atzeres', 'Simchas Torah', 72 | 'Chanuka', "Tu B'shvat", 'Purim Katan', 'Purim', 'Shushan Purim', 73 | 'Pesach', 'Pesach Sheni', "Lag Ba'omer", 'Shavuos', "Tu B'av"] 74 | 75 | 76 | FESTIVALS_HEBREW = [ 77 | 'ראש השנה', 'יום כיפור', 'סוכות', 'שמיני עצרת', 'שמחת תורה', 'חנוכה', 78 | 'ט״ו בשבט', 'פורים קטן', 'פורים', 'שושן פורים', 'פסח', 'פסח שני', 79 | 'ל״ג בעומר', 'שבועות', 'ט״ו באב' 80 | ] 81 | 82 | 83 | WEEKDAYS = { 84 | 1: 'ראשון', 85 | 2: 'שני', 86 | 3: 'שלישי', 87 | 4: 'רביעי', 88 | 5: 'חמישי', 89 | 6: 'שישי', 90 | 7: 'שבת' 91 | } 92 | 93 | 94 | def _is_leap(year): 95 | if (((7*year) + 1) % 19) < 7: 96 | return True 97 | return False 98 | 99 | 100 | def _elapsed_months(year): 101 | return (235 * year - 234) // 19 102 | 103 | 104 | @lru_cache(maxsize=10) 105 | def _elapsed_days(year): 106 | months_elapsed = _elapsed_months(year) 107 | parts_elapsed = 204 + 793*(months_elapsed%1080) 108 | hours_elapsed = ( 109 | 5 + 12*months_elapsed + 793*(months_elapsed//1080) 110 | + parts_elapsed//1080) 111 | conjunction_day = 1 + 29*months_elapsed + hours_elapsed//24 112 | conjunction_parts = 1080 * (hours_elapsed%24) + parts_elapsed%1080 113 | 114 | if ( 115 | (conjunction_parts >= 19440) 116 | or ( 117 | (conjunction_day % 7 == 2) and (conjunction_parts >= 9924) 118 | and not _is_leap(year) 119 | ) 120 | or ( 121 | (conjunction_day % 7 == 1) and conjunction_parts >= 16789 122 | and _is_leap(year - 1) 123 | ) 124 | ): 125 | alt_day = conjunction_day + 1 126 | else: 127 | alt_day = conjunction_day 128 | if alt_day % 7 in [0, 3, 5]: 129 | alt_day += 1 130 | 131 | return alt_day 132 | 133 | 134 | def _days_in_year(year): 135 | return _elapsed_days(year + 1) - _elapsed_days(year) 136 | 137 | 138 | def _long_cheshvan(year): 139 | """Returns True if Cheshvan has 30 days""" 140 | return _days_in_year(year) % 10 == 5 141 | 142 | 143 | def _short_kislev(year): 144 | """Returns True if Kislev has 29 days""" 145 | return _days_in_year(year) % 10 == 3 146 | 147 | 148 | def _month_length(year, month): 149 | """Months start with Nissan (Nissan is 1 and Tishrei is 7)""" 150 | if month in [1, 3, 5, 7, 11]: 151 | return 30 152 | if month in [2, 4, 6, 10, 13]: 153 | return 29 154 | if month == 12: 155 | if _is_leap(year): 156 | return 30 157 | return 29 158 | if month == 8: # if long Cheshvan return 30, else return 29 159 | if _long_cheshvan(year): 160 | return 30 161 | return 29 162 | if month == 9: # if short Kislev return 29, else return 30 163 | if _short_kislev(year): 164 | return 29 165 | return 30 166 | raise ValueError('Invalid month') 167 | 168 | 169 | def _month_name(year, month, hebrew): 170 | index = month 171 | if month < 12 or not _is_leap(year): 172 | index -= 1 173 | if hebrew: 174 | return MONTH_NAMES_HEBREW[index] 175 | return MONTH_NAMES[index] 176 | 177 | 178 | def _monthslist(year): 179 | months = [7, 8, 9, 10, 11, 12, 13, 1, 2, 3, 4, 5, 6] 180 | if not _is_leap(year): 181 | months.remove(13) 182 | return months 183 | 184 | 185 | def _add_months(year, month, num): 186 | monthslist = _monthslist(year) 187 | index = monthslist.index(month) 188 | months_remaining = len(monthslist[index+1:]) 189 | if num <= months_remaining: 190 | return (year, monthslist[index + num]) 191 | return _add_months(year + 1, 7, num - months_remaining - 1) 192 | 193 | 194 | def _subtract_months(year, month, num): 195 | monthslist = _monthslist(year) 196 | index = monthslist.index(month) 197 | if num <= index: 198 | return (year, monthslist[index - num]) 199 | return _subtract_months(year - 1, 6, num - (index+1)) 200 | 201 | 202 | def _fast_day(date): 203 | """Return name of fast day or None. 204 | 205 | Parameters 206 | ---------- 207 | date : ``HebrewDate``, ``GregorianDate``, or ``JulianDay`` 208 | Any date that implements a ``to_heb()`` method which returns a 209 | ``HebrewDate`` can be used. 210 | 211 | Returns 212 | ------- 213 | str or ``None`` 214 | The name of the fast day or ``None`` if the given date is not 215 | a fast day. 216 | """ 217 | date = date.to_heb() 218 | year = date.year 219 | month = date.month 220 | day = date.day 221 | weekday = date.weekday() 222 | adar = 13 if _is_leap(year) else 12 223 | 224 | if month == 7: 225 | if (weekday == 1 and day == 4) or (weekday != 7 and day == 3): 226 | return _Days.TZOM_GEDALIA 227 | elif month == 10 and day == 10: 228 | return _Days.TENTH_OF_TEVES 229 | elif month == adar: 230 | if (weekday == 5 and day == 11) or weekday != 7 and day == 13: 231 | return _Days.TAANIS_ESTHER 232 | elif month == 4: 233 | if (weekday == 1 and day == 18) or (weekday != 7 and day == 17): 234 | return _Days.SEVENTEENTH_OF_TAMUZ 235 | elif month == 5: 236 | if (weekday == 1 and day == 10) or (weekday != 7 and day == 9): 237 | return _Days.NINTH_OF_AV 238 | return None 239 | 240 | 241 | def _fast_day_string(date, hebrew=False): 242 | fast = _fast_day(date) 243 | if fast is None: 244 | return None 245 | if hebrew: 246 | return _days_hebrew[fast] 247 | return fast.value 248 | 249 | 250 | def _first_day_of_holiday(holiday): 251 | if holiday is _Days.ROSH_HASHANA: 252 | return (7, 1) 253 | if holiday is _Days.SUCCOS: 254 | return (7, 15) 255 | if holiday is _Days.CHANUKA: 256 | return (9, 25) 257 | if holiday is _Days.PESACH: 258 | return (1, 15) 259 | if holiday is _Days.SHAVUOS: 260 | return (3, 6) 261 | return None 262 | 263 | 264 | def _festival(date, israel=False, include_working_days=True): 265 | """Return Jewish festival of given day. 266 | 267 | This method will return all major and minor religous 268 | Jewish holidays not including fast days. 269 | 270 | Parameters 271 | ---------- 272 | date : ``HebrewDate``, ``GregorianDate``, or ``JulianDay`` 273 | Any date that implements a ``to_heb()`` method which returns a 274 | ``HebrewDate`` can be used. 275 | 276 | israel : bool, optional 277 | ``True`` if you want the holidays according to the Israel 278 | schedule. Defaults to ``False``. 279 | 280 | include_working_days : bool, optional 281 | ``True`` to include festival days in which melacha (work) is 282 | allowed; ie. Pesach Sheni, Chol Hamoed, etc. 283 | Default is ``True``. 284 | 285 | Returns 286 | ------- 287 | str or ``None`` 288 | The festival or ``None`` if the given date is not a Jewish 289 | festival. 290 | """ 291 | date = date.to_heb() 292 | year = date.year 293 | month = date.month 294 | day = date.day 295 | if month == 7: 296 | if day in [1, 2]: 297 | return _Days.ROSH_HASHANA 298 | if day == 10: 299 | return _Days.YOM_KIPPUR 300 | if ( 301 | not include_working_days 302 | and (day in range(17, 22) or (israel and day == 16)) 303 | ): 304 | return None 305 | if day in range(15, 22): 306 | return _Days.SUCCOS 307 | if day == 22: 308 | return _Days.SHMINI_ATZERES 309 | if day == 23 and not israel: 310 | return _Days.SIMCHAS_TORAH 311 | elif month in [9, 10] and include_working_days: 312 | kislev_length = _month_length(year, 9) 313 | if ( 314 | month == 9 and day in range(25, kislev_length + 1) 315 | or month == 10 and day in range(1, 8 - (kislev_length - 25)) 316 | ): 317 | return _Days.CHANUKA 318 | elif month == 11 and day == 15 and include_working_days: 319 | return _Days.TU_BSHVAT 320 | elif month == 12 and include_working_days: 321 | leap = _is_leap(year) 322 | if day == 14: 323 | return _Days.PURIM_KATAN if leap else _Days.PURIM 324 | if day == 15 and not leap: 325 | return _Days.SHUSHAN_PURIM 326 | elif month == 13 and include_working_days: 327 | if day == 14: 328 | return _Days.PURIM 329 | if day == 15: 330 | return _Days.SHUSHAN_PURIM 331 | elif month == 1: 332 | if ( 333 | not include_working_days 334 | and (day in range(17, 21) or (israel and day == 16)) 335 | ): 336 | return None 337 | if day in range(15, 22 if israel else 23): 338 | return _Days.PESACH 339 | elif month == 2 and day == 14 and include_working_days: 340 | return _Days.PESACH_SHENI 341 | elif month == 2 and day == 18 and include_working_days: 342 | return _Days.LAG_BAOMER 343 | elif month == 3 and (day == 6 or (not israel and day == 7)): 344 | return _Days.SHAVUOS 345 | elif month == 5 and day == 15 and include_working_days: 346 | return _Days.TU_BAV 347 | return None 348 | 349 | 350 | def _festival_string( 351 | date, 352 | israel=False, 353 | hebrew=False, 354 | include_working_days=True, 355 | ): 356 | festival = _festival(date, israel, include_working_days) 357 | if festival is None: 358 | return None 359 | if hebrew: 360 | return _days_hebrew[festival] 361 | return festival.value 362 | -------------------------------------------------------------------------------- /src/pyluach/parshios.py: -------------------------------------------------------------------------------- 1 | """The parshios module has functions to find the weekly parasha. 2 | 3 | Examples 4 | -------- 5 | >>> from pyluach import dates, parshios 6 | >>> date = dates.HebrewDate(5781, 10, 5) 7 | >>> parshios.getparsha(date) 8 | 'Vayigash' 9 | >>> parshios.getparsha_string(date, hebrew=True) 10 | 'ויגש' 11 | >>> parshios.getparsha_string(dates.GregorianDate(2021, 3, 7), hebrew=True) 12 | 'ויקהל, פקודי' 13 | 14 | Note 15 | ---- 16 | The algorithm is based on Dr. Irv Bromberg's, University of Toronto at 17 | http://individual.utoronto.ca/kalendis/hebrew/parshah.htm 18 | 19 | All English parsha names are transliterated into the American Ashkenazik 20 | pronunciation. 21 | 22 | 23 | Attributes 24 | ---------- 25 | PARSHIOS : list of str 26 | A list of the parshios transliterated into English. 27 | PARSHIOS_HEBREW : list of str 28 | A list of the parshios in Hebrew. 29 | """ 30 | 31 | from collections import deque, OrderedDict 32 | from functools import lru_cache 33 | from enum import Enum, IntEnum, auto 34 | 35 | from pyluach.dates import HebrewDate 36 | from pyluach.utils import _is_leap 37 | 38 | 39 | PARSHIOS = [ 40 | 'Bereishis', 'Noach', 'Lech Lecha', 'Vayeira', 'Chayei Sarah', 'Toldos', 41 | 'Vayeitzei', 'Vayishlach', 'Vayeishev', 'Mikeitz', 'Vayigash', 'Vayechi', 42 | 'Shemos', "Va'eira", 'Bo', 'Beshalach', 'Yisro', 'Mishpatim', 'Terumah', 43 | 'Tetzaveh', 'Ki Sisa', 'Vayakhel', 'Pekudei', 'Vayikra', 'Tzav', 'Shemini', 44 | 'Tazria', 'Metzora', 'Acharei Mos', 'Kedoshim', 'Emor', 'Behar', 45 | 'Bechukosai', 'Bamidbar', 'Nasso', "Beha'aloscha", 'Shelach', 'Korach', 46 | 'Chukas', 'Balak', 'Pinchas', 'Mattos', 'Masei', 'Devarim', "Va'eschanan", 47 | 'Eikev', "Re'eh", 'Shoftim', 'Ki Seitzei', 'Ki Savo', 'Nitzavim', 48 | 'Vayeilech', 'Haazinu', 'Vezos Haberachah' 49 | ] 50 | 51 | 52 | PARSHIOS_HEBREW = [ 53 | 'בראשית', 'נח', 'לך לך', 'וירא', 'חיי שרה', 'תולדות', 'ויצא', 'וישלח', 54 | 'וישב', 'מקץ', 'ויגש', 'ויחי', 'שמות', 'וארא', 'בא', 'בשלח', 'יתרו', 55 | 'משפטים', 'תרומה', 'תצוה', 'כי תשא', 'ויקהל', 'פקודי', 'ויקרא', 'צו', 56 | 'שמיני', 'תזריע', 'מצורע', 'אחרי מות', 'קדושים', 'אמור', 'בהר', 'בחוקותי', 57 | 'במדבר', 'נשא', 'בהעלותך', 'שלח', 'קרח', 'חקת', 'בלק', 'פינחס', 'מטות', 58 | 'מסעי', 'דברים', 'ואתחנן', 'עקב', 'ראה', 'שופטים', 'כי תצא', 'כי תבא', 59 | 'נצבים', 'וילך', 'האזינו', 'וזאת הברכה' 60 | ] 61 | 62 | 63 | class _Parshios_Enum(IntEnum): 64 | BEREISHIS = 0 65 | NOACH = 1 66 | LECH_LECHA = auto() 67 | VAYEIRA = auto() 68 | CHAYEI_SARAH = auto() 69 | TOLDOS = auto() 70 | VAYEITZEI = auto() 71 | VAYISHLACH = auto() 72 | VAYEISHEV = auto() 73 | MIKEITZ = auto() 74 | VAYIGASH = auto() 75 | VAYECHI = auto() 76 | SHEMOS = auto() 77 | VAEIRA = auto() 78 | BO = auto() 79 | BESHALACH = auto() 80 | YISRO = auto() 81 | MISHPATIM = auto() 82 | TERUMAH = auto() 83 | TETZAVEH = auto() 84 | KI_SISA = auto() 85 | VAYAKHEL = auto() 86 | PEKUDEI = auto() 87 | VAYIKRA = auto() 88 | TZAV = auto() 89 | SHEMINI = auto() 90 | TAZRIA = auto() 91 | METZORA = auto() 92 | ACHAREI_MOS = auto() 93 | KEDOSHIM = auto() 94 | EMOR = auto() 95 | BEHAR = auto() 96 | BECHUKOSAI = auto() 97 | BAMIDBAR = auto() 98 | NASSO = auto() 99 | BEHAALOSCHA = auto() 100 | SHELACH = auto() 101 | KORACH = auto() 102 | CHUKAS = auto() 103 | BALAK = auto() 104 | PINCHAS = auto() 105 | MATTOS = auto() 106 | MASEI = auto() 107 | DEVARIM = auto() 108 | VAESCHANAN = auto() 109 | EIKEV = auto() 110 | REEH = auto() 111 | SHOFTIM = auto() 112 | KI_SEITZEI = auto() 113 | KI_SAVO = auto() 114 | NITZAVIM = auto() 115 | VAYEILECH = auto() 116 | HAAZINU = auto() 117 | VEZOS_HABERACHAH = auto() 118 | 119 | 120 | class _FourParshiosEnum(Enum): 121 | SHEKALIM = auto() 122 | ZACHOR = auto() 123 | PARAH = auto() 124 | HACHODESH = auto() 125 | 126 | 127 | _FOUR_PARSHIOS = { 128 | _FourParshiosEnum.ZACHOR: 'Zachor', 129 | _FourParshiosEnum.SHEKALIM: 'Shekalim', 130 | _FourParshiosEnum.HACHODESH: 'Hachodesh', 131 | _FourParshiosEnum.PARAH: 'Parah', 132 | } 133 | 134 | 135 | _FOUR_PARSHIOS_HEBREW = { 136 | _FourParshiosEnum.ZACHOR: 'זכור', 137 | _FourParshiosEnum.SHEKALIM: 'שקלים', 138 | _FourParshiosEnum.PARAH: 'פרה', 139 | _FourParshiosEnum.HACHODESH: 'החודש' 140 | } 141 | 142 | 143 | def _parshaless(date, israel=False): 144 | if israel and date.tuple()[1:] in [(7, 23), (1, 22), (3, 7)]: 145 | return False 146 | if date.month == 7 and date.day in ([1, 2, 10] + list(range(15, 24))): 147 | return True 148 | if date.month == 1 and date.day in range(15, 23): 149 | return True 150 | if date.month == 3 and date.day in [6, 7]: 151 | return True 152 | return False 153 | 154 | 155 | @lru_cache(maxsize=10) 156 | def _gentable(year, israel=False): 157 | """Return OrderedDict mapping date of Shabbos to list of parsha numbers. 158 | 159 | The numbers start with Beraishis as 0. Double parshios are represented 160 | as a list of the two numbers. If there is no Parsha the value is None. 161 | """ 162 | parshalist = deque([51, 52] + list(range(52))) 163 | table = OrderedDict() 164 | leap = _is_leap(year) 165 | pesachday = HebrewDate(year, 1, 15).weekday() 166 | rosh_hashana = HebrewDate(year, 7, 1) 167 | shabbos = rosh_hashana.shabbos() 168 | if rosh_hashana.weekday() > 4: 169 | parshalist.popleft() 170 | 171 | while shabbos.year == year: 172 | if _parshaless(shabbos, israel): 173 | table[shabbos] = None 174 | else: 175 | parsha = parshalist.popleft() 176 | table[shabbos] = [parsha] 177 | if ( 178 | ( 179 | parsha == _Parshios_Enum.VAYAKHEL 180 | and (HebrewDate(year, 1, 14) - shabbos) // 7 < 3 181 | ) 182 | or ( 183 | parsha in [ 184 | _Parshios_Enum.TAZRIA, _Parshios_Enum.ACHAREI_MOS 185 | ] and not leap 186 | ) 187 | or ( 188 | parsha == _Parshios_Enum.BEHAR and not leap 189 | and (not israel or pesachday != 7) 190 | ) 191 | or ( 192 | parsha == _Parshios_Enum.CHUKAS 193 | and not israel and pesachday == 5 194 | ) 195 | or ( 196 | parsha == _Parshios_Enum.MATTOS 197 | and (HebrewDate(year, 5, 9)-shabbos) // 7 < 2 198 | ) 199 | or ( 200 | parsha == _Parshios_Enum.NITZAVIM 201 | and HebrewDate(year+1, 7, 1).weekday() > 4 202 | ) 203 | ): 204 | # If any of that then it's a double parsha. 205 | table[shabbos].append(parshalist.popleft()) 206 | shabbos += 7 207 | return table 208 | 209 | 210 | def getparsha(date, israel=False): 211 | """Return the parsha for a given date. 212 | 213 | Returns the parsha for the Shabbos on or following the given 214 | date. 215 | 216 | Parameters 217 | ---------- 218 | date : ~pyluach.dates.BaseDate 219 | Any subclass of ``BaseDate``. This date does not have to be a Shabbos. 220 | 221 | israel : bool, optional 222 | ``True`` if you want the parsha according to the Israel schedule 223 | (with only one day of Yom Tov). Defaults to ``False``. 224 | 225 | Returns 226 | ------- 227 | list of int or None 228 | A list of the numbers of the parshios for the Shabbos of the given date, 229 | beginning with 0 for Beraishis, or ``None`` if the Shabbos doesn't 230 | have a parsha (i.e. it's on Yom Tov). 231 | """ 232 | shabbos = date.to_heb().shabbos() 233 | table = _gentable(shabbos.year, israel) 234 | return table[shabbos] 235 | 236 | 237 | def getparsha_string(date, israel=False, hebrew=False): 238 | """Return the parsha as a string for the given date. 239 | 240 | This function wraps ``getparsha`` returning the parsha name. 241 | 242 | Parameters 243 | ---------- 244 | date : ~pyluach.dates.BaseDate 245 | Any subclass of ``BaseDate``. The date does not have to be a Shabbos. 246 | 247 | israel : bool, optional 248 | ``True`` if you want the parsha according to the Israel schedule 249 | (with only one day of Yom Tov). Default is ``False``. 250 | 251 | hebrew : bool, optional 252 | ``True`` if you want the name of the parsha in Hebrew. 253 | Default is ``False``. 254 | 255 | Returns 256 | ------- 257 | str or None 258 | The name of the parsha separated by a comma and space if it is a 259 | double parsha or ``None`` if there is no parsha that Shabbos 260 | (ie. it's yom tov). 261 | """ 262 | parsha = getparsha(date, israel) 263 | if parsha is None: 264 | return None 265 | if not hebrew: 266 | name = [PARSHIOS[n] for n in parsha] 267 | else: 268 | name = [PARSHIOS_HEBREW[n] for n in parsha] 269 | return ', '.join(name) 270 | 271 | 272 | def iterparshios(year, israel=False): 273 | """Generate all the parshios in the year. 274 | 275 | Parameters 276 | ---------- 277 | year : int 278 | The Hebrew year to get the parshios for. 279 | 280 | israel : bool, optional 281 | ``True`` if you want the parsha according to the Israel schedule 282 | (with only one day of Yom Tov). Defaults to ``False`` 283 | 284 | Yields 285 | ------ 286 | :obj:`list` of :obj:`int` or :obj:`None` 287 | A list of the numbers of the parshios for the next Shabbos in the 288 | given year. Yields ``None`` for a Shabbos that doesn't have its 289 | own parsha (i.e. it occurs on a yom tov). 290 | """ 291 | table = _gentable(year, israel) 292 | for shabbos in table.values(): 293 | yield shabbos 294 | 295 | 296 | def parshatable(year, israel=False): 297 | """Return a table of all the Shabbosos in the year 298 | 299 | Parameters 300 | ---------- 301 | year : int 302 | The Hebrew year to get the parshios for. 303 | 304 | israel : bool, optional 305 | ``True`` if you want the parshios according to the Israel 306 | schedule (with only one day of Yom Tov). Defaults to ``False``. 307 | 308 | Returns 309 | ------- 310 | ~collections.OrderedDict 311 | An ordered dictionary with the ``HebrewDate`` of each Shabbos 312 | as the key mapped to the parsha as a list of ints, or ``None`` for a 313 | Shabbos with no parsha. 314 | """ 315 | return _gentable(year, israel) 316 | 317 | 318 | def _get_hachodesh(date): 319 | """Return Hachodesh given Hebrew date.""" 320 | year = date.year 321 | shabbos = date.shabbos() 322 | rc_nissan = HebrewDate(year, 1, 1) 323 | if shabbos <= rc_nissan and shabbos - rc_nissan < 7: 324 | return _FourParshiosEnum.HACHODESH 325 | return None 326 | 327 | 328 | def _get_four_parshios(date): 329 | """Return the special parsha given Hebrew date.""" 330 | year = date.year 331 | adar = 12 332 | if _is_leap(year): 333 | adar = 13 334 | shabbos = date.shabbos() 335 | rc_adar = HebrewDate(year, adar, 1) 336 | if shabbos <= rc_adar and rc_adar - shabbos < 7: 337 | return _FourParshiosEnum.SHEKALIM 338 | if shabbos.month == adar: 339 | purim = HebrewDate(year, adar, 14) 340 | if shabbos < purim and (purim - shabbos) < 7: 341 | return _FourParshiosEnum.ZACHOR 342 | if _get_hachodesh(date + 7): 343 | return _FourParshiosEnum.PARAH 344 | if _get_hachodesh(date): 345 | return _FourParshiosEnum.HACHODESH 346 | return None 347 | 348 | 349 | def four_parshios(date, hebrew=False): 350 | """Return which of the four parshios is given date's Shabbos. 351 | 352 | Parameters 353 | ---------- 354 | date : ~pyluach.dates.BaseDate 355 | Any subclass of ``BaseDate``. This date does not have to be a Shabbos. 356 | 357 | hebrew : bool 358 | ``True`` if you want the name of the parsha in Hebrew. 359 | Default is ``False``. 360 | 361 | Returns 362 | ------- 363 | str 364 | The name of the one of the four parshios or an empty string 365 | if that shabbos is not one of them. 366 | """ 367 | date = date.to_heb() 368 | special_parsha = _get_four_parshios(date) 369 | if special_parsha is None: 370 | return '' 371 | if hebrew: 372 | return _FOUR_PARSHIOS_HEBREW[special_parsha] 373 | return _FOUR_PARSHIOS[special_parsha] 374 | -------------------------------------------------------------------------------- /tests/test_dates.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from operator import gt, lt, eq, ne, ge, le, add, sub 3 | 4 | import pytest 5 | 6 | from pyluach import dates, hebrewcal, utils 7 | from pyluach.dates import HebrewDate, GregorianDate, JulianDay, Rounding 8 | 9 | 10 | KNOWN_VALUES = {(2009, 8, 21): (5769, 6, 1), 11 | (2009, 9, 30): (5770, 7, 12), 12 | (2009, 11, 13): (5770, 8, 26), 13 | (2010, 1, 21): (5770, 11, 6), 14 | (2010, 5, 26): (5770, 3, 13), 15 | (2013, 11, 17): (5774, 9, 14), 16 | (2014, 3, 12): (5774, 13, 10), 17 | (2014, 6, 10): (5774, 3, 12), 18 | (2016, 2, 10): (5776, 12, 1) 19 | } 20 | 21 | 22 | @pytest.fixture(scope='module') 23 | def datetypeslist(): 24 | datetypes = [dates.HebrewDate, dates.GregorianDate] 25 | return datetypes 26 | 27 | 28 | class TestClassesSanity: 29 | def test_greg_sanity(self): 30 | for i in range(347998, 2460000, 117): 31 | jd = dates.JulianDay(i) 32 | conf = jd.to_greg().to_jd() 33 | if jd >= dates.GregorianDate(1, 1, 1): 34 | assert jd.day == conf.day 35 | else: 36 | assert abs(jd.day - conf.day) <= 1 37 | bce = GregorianDate(-100, 1, 1) 38 | assert abs(bce.to_heb().to_greg() - bce) <= 1 39 | 40 | def test_heb_sanity(self): 41 | for i in range(347998, 2460000, 117): 42 | jd = dates.JulianDay(i) 43 | conf = jd.to_heb().to_jd() 44 | assert jd.day == conf.day 45 | 46 | 47 | class TestClassesConversion: 48 | def test_from_greg(self): 49 | for date in KNOWN_VALUES: 50 | heb = dates.GregorianDate(*date).to_heb().tuple() 51 | assert KNOWN_VALUES[date] == heb 52 | 53 | def test_from_heb(self): 54 | for date in KNOWN_VALUES: 55 | greg = dates.HebrewDate(*KNOWN_VALUES[date]).to_greg().tuple() 56 | assert date == greg 57 | 58 | 59 | @pytest.fixture 60 | def setup(scope='module'): 61 | caltypes = [GregorianDate, HebrewDate, JulianDay] 62 | deltas = [0, 1, 29, 73, 1004] 63 | return {'caltypes': caltypes, 'deltas': deltas} 64 | 65 | 66 | class TestOperators: 67 | 68 | def test_add(self, setup): 69 | for cal in setup['caltypes']: 70 | for delta in setup['deltas']: 71 | date = cal.today() 72 | date2 = date + delta 73 | assert date.jd + delta == date2.jd 74 | 75 | def test_min_int(self, setup): 76 | """Test subtracting a number from a date""" 77 | for cal in setup['caltypes']: 78 | for delta in setup['deltas']: 79 | date = cal.today() 80 | date2 = date - delta 81 | assert date.jd - delta == date2.jd 82 | 83 | def test_min_date(self, setup): 84 | """Test subtracting one date from another 85 | 86 | This test loops through subtracting the current date of each 87 | calendar from a date of each calendar at intervals from the 88 | current date. 89 | """ 90 | for cal in setup['caltypes']: 91 | for cal2 in setup['caltypes']: 92 | for delta in setup['deltas']: 93 | today = cal.today() 94 | difference = (cal2.today() - delta) - today 95 | assert delta == difference 96 | assert isinstance(difference, int) 97 | 98 | 99 | class TestComparisons: 100 | """In ComparisonTests, comparisons are tested. 101 | 102 | Every function tests one test case comparing a date from each 103 | calendar type to another date from each calendar type. 104 | """ 105 | 106 | def test_gt(self, setup): 107 | """Test all comparers when one date is greater.""" 108 | for cal in setup['caltypes']: 109 | today = cal.today() 110 | for cal2 in setup['caltypes']: 111 | yesterday = cal2.today() - 1 112 | for comp in [gt, ge, ne]: 113 | assert comp(today, yesterday) 114 | for comp in [eq, lt, le]: 115 | assert comp(today, yesterday) is False 116 | 117 | def test_lt(self, setup): 118 | """Test all comparers when one date is less than another.""" 119 | for cal in setup['caltypes']: 120 | today = cal.today() 121 | for cal2 in setup['caltypes']: 122 | tomorrow = cal2.today() + 1 123 | for comp in [lt, le, ne]: 124 | assert comp(today, tomorrow) 125 | for comp in [gt, ge, eq]: 126 | assert comp(today, tomorrow) is False 127 | 128 | def test_eq(self, setup): 129 | """Test all comparers when the dates are equal.""" 130 | for cal in setup['caltypes']: 131 | today = cal.today() 132 | for cal2 in setup['caltypes']: 133 | today2 = cal2.today() 134 | for comp in [eq, ge, le]: 135 | assert comp(today, today2) 136 | for comp in [gt, lt, ne]: 137 | assert comp(today, today2) is False 138 | 139 | 140 | class TestErrors: 141 | 142 | def test_too_low_heb(self): 143 | with pytest.raises(ValueError): 144 | dates.HebrewDate(0, 7, 1) 145 | with pytest.raises(ValueError): 146 | dates.HebrewDate(-1, 7, 1) 147 | 148 | def test_comparer_errors(self): 149 | day1 = dates.HebrewDate(5777, 12, 10) 150 | for date in [day1, day1.to_greg(), day1.to_jd()]: 151 | for comparer in [gt, lt, ge, le]: 152 | for value in [1, 0, 'hello', None, '']: 153 | with pytest.raises(TypeError): 154 | comparer(date, value) 155 | assert (day1 == 5) is False 156 | assert (day1 != 'h') is True 157 | 158 | def test_operator_errors(self): 159 | day = dates.GregorianDate(2016, 11, 20) 160 | for operator in [add, sub]: 161 | for value in ['Hello', '', None]: 162 | with pytest.raises(TypeError): 163 | operator(day, value) 164 | with pytest.raises(TypeError): 165 | day + (day+1) 166 | 167 | def test_HebrewDate_errors(self): 168 | with pytest.raises(ValueError): 169 | HebrewDate(0, 6, 29) 170 | for datetuple in [ 171 | (5778, 0, 5), (5779, -1, 7), 172 | (5759, 14, 8), (5778, 13, 20), 173 | (5782, 12, 31) 174 | ]: 175 | with pytest.raises(ValueError): 176 | HebrewDate(*datetuple) 177 | for datetuple in [(5778, 6, 0), (5779, 8, 31), (5779, 10, 30)]: 178 | with pytest.raises(ValueError): 179 | HebrewDate(*datetuple) 180 | 181 | def test_GregorianDate_errors(self): 182 | for datetuple in [ 183 | (2018, 0, 3), (2018, -2, 8), (2018, 13, 9), 184 | (2018, 2, 0), (2018, 2, 29), (2012, 2, 30) 185 | ]: 186 | with pytest.raises(ValueError): 187 | GregorianDate(*datetuple) 188 | 189 | def test_JD_errors(self): 190 | with pytest.raises(ValueError): 191 | JulianDay(-1).to_heb() 192 | with pytest.raises(TypeError): 193 | JulianDay(689)._to_x(datetime.date) 194 | 195 | 196 | class TestReprandStr: 197 | 198 | def test_repr(self, datetypeslist): 199 | for datetype in datetypeslist: 200 | assert eval(repr(datetype.today())) == datetype.today() 201 | jd = JulianDay.today() 202 | assert eval(repr(jd)) == jd 203 | 204 | def test_jd_str(self): 205 | assert str(JulianDay(550.5)) == '550.5' 206 | assert str(JulianDay(1008)) == '1007.5' 207 | 208 | def test_greg_str(self): 209 | date = GregorianDate(2018, 8, 22) 210 | assert str(date) == '2018-08-22' 211 | assert str(GregorianDate(2008, 12, 2)) == '2008-12-02' 212 | assert str(GregorianDate(1, 1, 1)) == '0001-01-01' 213 | 214 | 215 | def test_weekday(): 216 | assert GregorianDate(2017, 8, 7).weekday() == 2 217 | assert HebrewDate(5777, 6, 1).weekday() == 4 218 | assert JulianDay(2458342.5).weekday() == 1 219 | 220 | 221 | def test_isoweekday(): 222 | assert GregorianDate(2020, 9, 20).isoweekday() == 7 223 | assert GregorianDate(2020, 10, 3).isoweekday() == 6 224 | assert GregorianDate(2020, 10, 5).isoweekday() == 1 225 | assert JulianDay(2458342.5).isoweekday() == 7 226 | 227 | 228 | class TestMixinMethods: 229 | 230 | @pytest.fixture 231 | def date(self): 232 | return dates.GregorianDate(2017, 10, 31) 233 | 234 | def test_str(self, date): 235 | assert str(date) == '2017-10-31' 236 | 237 | def test_dict(self, date): 238 | assert date.dict() == {'year': 2017, 'month': 10, 'day': 31} 239 | 240 | def test_iter(self, date): 241 | assert list(date) == [date.year, date.month, date.day] 242 | 243 | 244 | class TestHolidayMethods: 245 | def test_fast_day(self): 246 | date = dates.HebrewDate(5781, 7, 3) 247 | assert date.holiday() == 'Tzom Gedalia' 248 | assert date.holiday(False, True) == 'צום גדליה' 249 | assert date.fast_day() == 'Tzom Gedalia' 250 | assert date.fast_day(True) == 'צום גדליה' 251 | 252 | def test_festival(self): 253 | date = dates.GregorianDate(2020, 12, 11) 254 | assert date.holiday() == 'Chanuka' 255 | assert date.holiday(hebrew=True) == 'חנוכה' 256 | assert date.festival() == 'Chanuka' 257 | assert date.festival(hebrew=True) == 'חנוכה' 258 | assert date.festival(include_working_days=False) is None 259 | 260 | def test_day_of_holiday(self): 261 | assert HebrewDate(5783, 12, 14)._day_of_holiday(israel=False) == '' 262 | shavuos = HebrewDate(5783, 3, 6) 263 | assert shavuos.holiday(prefix_day=True) == '1 Shavuos' 264 | assert shavuos.holiday(israel=True, prefix_day=True) == 'Shavuos' 265 | shavuos_greg = shavuos.to_greg() 266 | assert shavuos_greg.holiday(prefix_day=True) == '1 Shavuos' 267 | assert shavuos.to_jd().holiday(prefix_day=True) == '1 Shavuos' 268 | 269 | 270 | def test_to_pydate(): 271 | day = HebrewDate(5778, 6, 1) 272 | jd = day.to_jd() 273 | for day_type in [day, jd]: 274 | assert day_type.to_pydate() == datetime.date(2018, 8, 12) 275 | 276 | 277 | def test_from_pydate(): 278 | date = datetime.date(2018, 8, 27) 279 | assert date == GregorianDate.from_pydate(date).to_jd().to_pydate() 280 | assert date == HebrewDate.from_pydate(date).to_pydate() 281 | assert date == JulianDay.from_pydate(date).to_pydate() 282 | 283 | 284 | def test_is_leap(): 285 | assert GregorianDate(2020, 10, 26).is_leap() is True 286 | assert GregorianDate(2021, 10, 26).is_leap() is False 287 | 288 | 289 | def test_hebrew_year(): 290 | date = HebrewDate(5783, 12, 14) 291 | assert date.hebrew_year(True, False) == 'התשפג' 292 | 293 | 294 | def test_hebrew_date_string(): 295 | date = HebrewDate(5782, 7, 1) 296 | assert date.hebrew_date_string() == 'א׳ תשרי תשפ״ב' 297 | assert date.hebrew_date_string(True) == 'א׳ תשרי ה׳תשפ״ב' 298 | 299 | 300 | def test_month_name(): 301 | date = HebrewDate(5781, 12, 14) 302 | assert date.month_name() == 'Adar' 303 | assert date.month_name(True) == 'אדר' 304 | date2 = HebrewDate(5782, 12, 14) 305 | assert date2.month_name() == 'Adar 1' 306 | assert date2.month_name(True) == 'אדר א׳' 307 | 308 | 309 | def test_month_length(): 310 | with pytest.raises(ValueError): 311 | utils._month_length(5782, 14) 312 | 313 | 314 | @pytest.fixture 315 | def date(): 316 | return HebrewDate(5782, 2, 18) 317 | 318 | 319 | class TestFormat: 320 | 321 | def test_errors(self, date): 322 | with pytest.raises(ValueError): 323 | format(date, ' %') 324 | with pytest.raises(ValueError): 325 | format(date, '%*') 326 | with pytest.raises(ValueError): 327 | format(date, '%*-') 328 | with pytest.raises(ValueError): 329 | format(date, '%*-h') 330 | with pytest.raises(ValueError): 331 | format(date, '%z') 332 | with pytest.raises(ValueError): 333 | format(date, '%*z') 334 | with pytest.raises(ValueError): 335 | format(date, '%-') 336 | with pytest.raises(ValueError): 337 | format(date, '%-z') 338 | 339 | def test_format_weekday(self, date): 340 | pydate = date.to_pydate() 341 | A = pydate.strftime('%A') 342 | a = pydate.strftime('%a') 343 | ha = 'ה׳' 344 | hA = 'חמישי' 345 | assert format(date, 'w: %w %a %A %*a %*A') == f'w: 5 {a} {A} {ha} {hA}' 346 | 347 | def test_format_month(self, date): 348 | month = hebrewcal.Month(5782, 2) 349 | B = month.month_name(False) 350 | hB = month.month_name(True) 351 | assert format(date, 'm: %m %-m %B %*B') == f'm: 02 2 {B} {hB}' 352 | 353 | def test_format_day(self, date): 354 | assert format(date, 'd: %d %-d %%') == 'd: 18 18 %' 355 | assert format(date - 9, '%d %-d') == '09 9' 356 | assert format(date, '%*d %*-d') == 'י״ח יח' 357 | assert format(date + 2, '%*d - %*-d') == 'כ׳ - כ' 358 | 359 | def test_format_year(self, date): 360 | hy = 'תשפ״ב' 361 | hY = 'ה׳תשפ״ב' 362 | assert format(date, '%Y %y %*y %*Y') == f'5782 82 {hy} {hY}' 363 | 364 | def test_format_greg(self): 365 | date = GregorianDate(2022, 5, 8) 366 | assert format(date, '%y') == '22' 367 | assert date.strftime('%Y') == '2022' 368 | 369 | 370 | def test_add_years(): 371 | date = HebrewDate(5782, 12, 30) 372 | assert ( 373 | date.add(years=1, rounding=Rounding.PREVIOUS_DAY) 374 | == HebrewDate(5783, 12, 29) 375 | ) 376 | assert date.add(years=1) == HebrewDate(5783, 1, 1) 377 | date = HebrewDate(5782, 13, 1) 378 | assert date.add(years=1) == HebrewDate(5783, 12, 1) 379 | date = HebrewDate(5783, 12, 29) 380 | assert date.add(years=-1) == HebrewDate(5782, 13, 29) 381 | 382 | 383 | def test_add(): 384 | date = HebrewDate(5782, 12, 30) 385 | assert ( 386 | date.add(months=1, rounding=Rounding.PREVIOUS_DAY) 387 | == HebrewDate(5782, 13, 29) 388 | ) 389 | assert date.add(months=1) == HebrewDate(5782, 1, 1) 390 | assert date.add(months=27) == HebrewDate(5784, 1, 30) 391 | assert ( 392 | date.add(months=27, rounding=Rounding.PREVIOUS_DAY) 393 | == HebrewDate(5784, 1, 30) 394 | ) 395 | with pytest.raises(ValueError): 396 | date.add(months=1, rounding=Rounding.EXCEPTION) 397 | date = HebrewDate(5781, 7, 28) 398 | assert date.add(years=2, months=1, days=2) == HebrewDate(5783, 8, 30) 399 | assert date.add(years=3, months=2, days=2) == HebrewDate(5784, 10, 1) 400 | 401 | 402 | def test_add_error(): 403 | date = HebrewDate(5783, 11, 30) 404 | with pytest.raises(TypeError): 405 | date.add(months=1, rounding='Rounding.PREVIOUS_DAY') 406 | 407 | 408 | def test_subtract(): 409 | date = HebrewDate(5782, 12, 30) 410 | assert ( 411 | date.subtract(months=6, rounding=Rounding.PREVIOUS_DAY) 412 | == HebrewDate(5781, 6, 29) 413 | ) 414 | assert date.subtract(months=6) == HebrewDate(5782, 7, 1) 415 | 416 | 417 | def test_replace(): 418 | date = HebrewDate(5782, 4, 20) 419 | assert date.replace(year=5783) == HebrewDate(5783, 4, 20) 420 | assert date.replace(month=12) == HebrewDate(5782, 12, 20) 421 | assert date.replace(day=1) == HebrewDate(5782, 4, 1) 422 | with pytest.raises(ValueError): 423 | HebrewDate(5783, 12, 20).replace(month=13) 424 | -------------------------------------------------------------------------------- /tests/test_hebrewcal.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from copy import copy 3 | import calendar 4 | 5 | from pytest import fixture, raises 6 | from bs4 import BeautifulSoup 7 | 8 | from pyluach import dates, hebrewcal 9 | from pyluach.hebrewcal import Year, Month, holiday, festival, fast_day 10 | from pyluach.hebrewcal import HebrewTextCalendar, HebrewHTMLCalendar 11 | 12 | 13 | class TestYear: 14 | 15 | def test_repryear(self): 16 | year = Year(5777) 17 | assert eval(repr(year)) == year 18 | 19 | def test_iteryear(self): 20 | assert list(Year(5777)) == [7, 8, 9, 10, 11, 12, 1, 2, 3, 4, 5, 6] 21 | assert list(Year(5776)) == [7, 8, 9, 10, 11, 12, 13, 1, 2, 3, 4, 5, 6] 22 | 23 | def test_equalyear(self): 24 | year1 = hebrewcal.Year(5777) 25 | year2 = hebrewcal.Year(5777) 26 | assert year1 == year2 27 | 28 | def test_addtoyear(self): 29 | year = Year(5777) 30 | assert year + 2 == Year(5779) 31 | assert year + 0 == year 32 | with raises(TypeError): 33 | year + year 34 | with raises(TypeError): 35 | year + 'str' 36 | 37 | def test_subtractintfromyear(self): 38 | year = Year(5777) 39 | assert year - 0 == year 40 | assert year - 3 == Year(5774) 41 | with raises(TypeError): 42 | year - 'str' 43 | 44 | def test_subtractyearfromyear(self): 45 | year = Year(5777) 46 | assert year - year == 0 47 | assert year - (year - 1) == 1 48 | assert year - (year + 2) == 2 49 | 50 | def test_monthscount(self): 51 | year = Year(5782) 52 | assert year.monthscount() == 13 53 | assert (year + 1).monthscount() == 12 54 | 55 | def test_iterdays(self): 56 | year = Year(5778) 57 | yearlist = list(year.iterdays()) 58 | assert len(yearlist) == len(year) 59 | assert yearlist[0] == 1 60 | assert yearlist[-1] == len(year) 61 | 62 | def test_iterdates(self): 63 | year = 5778 64 | workingdate = dates.HebrewDate(year, 7, 1) 65 | for date in Year(year).iterdates(): 66 | assert workingdate == date 67 | workingdate += 1 68 | 69 | def test_errors(self): 70 | with raises(ValueError): 71 | Year(0) 72 | 73 | def test_year_string(self): 74 | year = Year(5781) 75 | assert year.year_string() == 'תשפ״א' 76 | assert year.year_string(True) == 'ה׳תשפ״א' 77 | 78 | def test_from_date(self): 79 | date = dates.GregorianDate(2021, 6, 7) 80 | year = Year.from_date(date) 81 | assert year == Year(date.to_heb().year) 82 | 83 | def test_from_pydate(self): 84 | pydate = datetime.date(2021, 6, 7) 85 | date = dates.HebrewDate.from_pydate(pydate) 86 | assert Year.from_pydate(pydate) == Year(date.year) 87 | 88 | 89 | @fixture 90 | def years(): 91 | year1 = Year(5778) 92 | year2 = Year(5780) 93 | return {1: year1, 2: year2} 94 | 95 | 96 | class TestYearComparisons: 97 | 98 | def test_year_equals(self, years): 99 | assert years[1] == copy(years[1]) 100 | assert (years[1] == years[2]) is False 101 | assert years[2] != years[1] 102 | assert (copy(years[2]) != years[2]) is False 103 | assert years[1] != 5778 104 | 105 | def test_year_gt(self, years): 106 | assert years[2] > years[1] 107 | assert (years[1] > years[1]) is False 108 | 109 | def test_years_ge(self, years): 110 | assert copy(years[1]) >= years[1] 111 | assert years[2] >= years[1] 112 | assert (years[1] >= years[2]) is False 113 | 114 | def test_years_lt(self, years): 115 | assert years[1] < years[2] 116 | assert (copy(years[2]) < years[2]) is False 117 | assert (years[2] < years[1]) is False 118 | 119 | def test_years_le(self, years): 120 | assert copy(years[1]) <= years[1] 121 | assert years[1] <= years[2] 122 | assert (years[2] <= years[1]) is False 123 | 124 | def test_errors(self, years): 125 | with raises(TypeError): 126 | years[1] > 5778 127 | with raises(TypeError): 128 | years[1] >= 0 129 | with raises(TypeError): 130 | years[1] < '5778' 131 | with raises(TypeError): 132 | years[1] <= 10000 133 | 134 | 135 | class TestMonth: 136 | 137 | def test_reprmonth(self): 138 | month = Month(5777, 10) 139 | assert eval(repr(month)) == month 140 | 141 | def test_equalmonth(self): 142 | month1 = hebrewcal.Month(5777, 12) 143 | month2 = hebrewcal.Month(5777, 12) 144 | assert month1 == month2 145 | assert not month1 == (month2 + 1) 146 | 147 | def test_addinttomonth(self): 148 | month = hebrewcal.Month(5777, 12) 149 | assert month + 0 == month 150 | assert month + 1 == hebrewcal.Month(5777, 1) 151 | assert month + 6 == hebrewcal.Month(5777, 6) 152 | assert month + 7 == hebrewcal.Month(5778, 7) 153 | assert month + 35 == hebrewcal.Month(5780, 10) 154 | with raises(TypeError): 155 | month + month 156 | with raises(TypeError): 157 | month + 'str' 158 | 159 | def test_subtract_month(self): 160 | month1 = hebrewcal.Month(5775, 10) 161 | month2 = hebrewcal.Month(5776, 10) 162 | month3 = hebrewcal.Month(5777, 10) 163 | assert month1 - month2 == 12 164 | assert month3 - month1 == 25 165 | 166 | def test_subtractintfrommonth(self): 167 | month = hebrewcal.Month(5778, 9) 168 | assert month - 2 == hebrewcal.Month(5778, 7) 169 | assert month - 3 == hebrewcal.Month(5777, 6) 170 | assert month - 30 == hebrewcal.Month(5775, 4) 171 | with raises(TypeError): 172 | month - 'str' 173 | 174 | def test_startingweekday(self): 175 | assert Month(5778, 8).starting_weekday() == 7 176 | assert Month(5778, 9).starting_weekday() == 1 177 | 178 | def test_iterdate(self): 179 | year = 5770 180 | workingdate = dates.HebrewDate(year, 7, 1) 181 | for month in (list(range(7, 13)) + list(range(1, 7))): 182 | for date in Month(year, month).iterdates(): 183 | assert date == workingdate 184 | workingdate += 1 185 | 186 | def test_molad(self): 187 | month = Month(5779, 7) 188 | assert month.molad() == {'weekday': 2, 'hours': 14, 'parts': 316} 189 | month = Month(5779, 5) 190 | assert month.molad() == {'weekday': 5, 'hours': 10, 'parts': 399} 191 | 192 | def test_molad_announcement(self): 193 | month = Month(5780, 3) 194 | assert month.molad_announcement() == { 195 | 'weekday': 6, 'hour': 11, 'minutes': 42, 'parts': 13 196 | } 197 | month = Month(5780, 2) 198 | assert month.molad_announcement() == { 199 | 'weekday': 4, 'hour': 22, 'minutes': 58, 'parts': 12 200 | } 201 | month = Month(5780, 8) 202 | assert month.molad_announcement() == { 203 | 'weekday': 2, 'hour': 18, 'minutes': 34, 'parts': 6 204 | } 205 | month = Month(5780, 12) 206 | assert month.molad_announcement() == { 207 | 'weekday': 1, 'hour': 21, 'minutes': 30, 'parts': 10 208 | } 209 | month = Month(5781, 1) 210 | assert month.molad_announcement() == { 211 | 'weekday': 7, 'hour': 19, 'minutes': 3, 'parts': 5 212 | } 213 | month = Month(5781, 8) 214 | assert month.molad_announcement() == { 215 | 'weekday': 7, 'hour': 3, 'minutes': 23, 'parts': 0 216 | } 217 | 218 | def test_month_name(self): 219 | month = Month(5781, 9) 220 | assert month.month_name() == 'Kislev' 221 | assert month.month_name(hebrew=True) == 'כסלו' 222 | adar = Month(5781, 12) 223 | assert adar.month_name() == 'Adar' 224 | adar_bais = Month(5782, 13) 225 | assert adar_bais.month_name() == 'Adar 2' 226 | 227 | def test_month_string(self): 228 | month = Month(5781, 3) 229 | assert month.month_string() == 'סיון תשפ״א' 230 | assert month.month_string(True) == 'סיון ה׳תשפ״א' 231 | 232 | def test_errors(self): 233 | with raises(ValueError): 234 | Month(-1, 1) 235 | with raises(ValueError): 236 | Month(5781, 13) 237 | with raises(ValueError): 238 | Month(5781, 14) 239 | 240 | def test_from_date(self): 241 | date = dates.HebrewDate(5781, 7, 10) 242 | assert Month.from_date(date) == Month(date.year, date.month) 243 | 244 | def test_from_pydate(self): 245 | pydate = datetime.date(2021, 6, 7) 246 | date = dates.HebrewDate.from_pydate(pydate) 247 | assert Month.from_pydate(pydate) == Month(date.year, date.month) 248 | 249 | 250 | @fixture 251 | def months(): 252 | month1 = Month(5780, 3) 253 | month2 = Month(5780, 4) 254 | month3 = Month(5781, 3) 255 | month12 = Month(5781, 12) 256 | return {1: month1, 2: month2, 3: month3, 12: month12} 257 | 258 | 259 | class TestCompareMonth: 260 | 261 | def test_month_gt(self, months): 262 | assert months[2] > months[1] 263 | assert not (months[1] > months[2]) 264 | assert months[3] > months[1] 265 | assert not (months[2] > months[3]) 266 | assert months[3] > months[12] 267 | assert not (months[12] > months[3]) 268 | 269 | def test_month_ge(self, months): 270 | assert copy(months[1]) >= months[1] 271 | assert months[2] >= months[1] 272 | assert (months[2] >= months[3]) is False 273 | assert months[3] >= months[12] 274 | 275 | def test_month_lt(self, months): 276 | assert (copy(months[2]) < months[2]) is False 277 | assert months[1] < months[2] 278 | assert months[2] < months[3] 279 | assert (months[3] < months[1]) is False 280 | assert Month(5780, 12) < months[1] 281 | 282 | def test_month_le(self, months): 283 | assert copy(months[2]) <= months[2] 284 | assert months[1] <= months[2] 285 | assert (months[3] <= months[2]) is False 286 | assert months[12] <= months[3] 287 | 288 | def test_month_ne(self, months): 289 | assert months[2] != months[1] 290 | assert months[3] != months[1] 291 | assert (copy(months[1]) != months[1]) is False 292 | assert months[3] != 3 293 | 294 | def test_month_errors(self, months): 295 | with raises(TypeError): 296 | months[1] > 5 297 | with raises(TypeError): 298 | assert months[2] <= '5' 299 | with raises(TypeError): 300 | assert months[1] >= 0 301 | with raises(TypeError): 302 | assert months[3] < 100 303 | 304 | 305 | class TestHoliday: 306 | 307 | def test_roshhashana(self): 308 | roshhashana = dates.HebrewDate(5779, 7, 1) 309 | assert all([ 310 | holiday(day, location) == 'Rosh Hashana' 311 | for day in [roshhashana, roshhashana + 1] 312 | for location in [True, False] 313 | ]) 314 | assert all(( 315 | festival( 316 | day, location, 317 | include_working_days=included_days 318 | ) == 'Rosh Hashana' 319 | for day in [roshhashana, roshhashana + 1] 320 | for location in [True, False] 321 | for included_days in [True, False] 322 | )) 323 | assert ( 324 | holiday(roshhashana, hebrew=True, prefix_day=True) 325 | == 'א׳ ראש השנה' 326 | ) 327 | assert festival(roshhashana + 1, prefix_day=True) == '2 Rosh Hashana' 328 | 329 | def test_yomkippur(self): 330 | yom_kippur = dates.HebrewDate(5775, 7, 10) 331 | assert holiday(yom_kippur) == 'Yom Kippur' 332 | assert holiday(yom_kippur, hebrew=True) == 'יום כיפור' 333 | assert festival(yom_kippur, include_working_days=False) == 'Yom Kippur' 334 | assert holiday(yom_kippur, prefix_day=True) == 'Yom Kippur' 335 | 336 | def test_succos(self): 337 | second_day = dates.HebrewDate(5782, 7, 16) 338 | day = dates.HebrewDate(5778, 7, 18) 339 | assert festival(day) == 'Succos' 340 | assert holiday(day, hebrew=True, prefix_day=True) == 'ד׳ סוכות' 341 | day2 = dates.HebrewDate(5778, 7, 23) 342 | assert festival(day2, israel=True, hebrew=True) is None 343 | assert festival(day, include_working_days=False) is None 344 | assert ( 345 | festival(second_day + 1, israel=True, include_working_days=False) 346 | is None 347 | ) 348 | assert ( 349 | festival(second_day, israel=False, include_working_days=False) 350 | == 'Succos' 351 | ) 352 | 353 | def test_shmini(self): 354 | shmini = dates.HebrewDate(5780, 7, 22) 355 | assert holiday(shmini, True) == 'Shmini Atzeres' 356 | assert holiday(shmini) == 'Shmini Atzeres' 357 | assert holiday(shmini + 1, prefix_day=True) == 'Simchas Torah' 358 | assert holiday(shmini + 1, israel=True, prefix_day=True) is None 359 | 360 | def test_chanuka(self): 361 | for year in [5778, 5787]: 362 | chanuka = dates.HebrewDate(year, 9, 25) 363 | assert ( 364 | festival(chanuka + 7, hebrew=True, prefix_day=True) 365 | == 'ח׳ חנוכה' 366 | ) 367 | for i in range(8): 368 | assert holiday(chanuka + i) == 'Chanuka' 369 | assert holiday(chanuka + 8) is None 370 | chanuka = dates.HebrewDate(5782, 9, 25) 371 | assert festival(chanuka, include_working_days=False) is None 372 | 373 | def test_tubshvat(self): 374 | tubeshvat = dates.HebrewDate(5779, 11, 15) 375 | assert holiday(tubeshvat) == "Tu B'shvat" 376 | assert festival(tubeshvat, include_working_days=False) is None 377 | 378 | def test_purim(self): 379 | purims = [dates.HebrewDate(5778, 12, 14), 380 | dates.HebrewDate(5779, 13, 14)] 381 | for purim in purims: 382 | assert holiday(purim, hebrew=True) == 'פורים' 383 | assert holiday(purim + 1) == 'Shushan Purim' 384 | assert holiday(dates.HebrewDate(5779, 12, 14)) == 'Purim Katan' 385 | for purim in purims: 386 | assert ( 387 | festival(purim, israel=True, include_working_days=False) 388 | is None 389 | ) 390 | 391 | def test_pesach(self): 392 | pesach = dates.HebrewDate(5778, 1, 15) 393 | for i in range(6): 394 | assert ( 395 | holiday(pesach + i, True) == 'Pesach' 396 | and holiday(pesach + i) == 'Pesach' 397 | ) 398 | eighth = pesach + 7 399 | assert holiday(eighth) == 'Pesach' and holiday(eighth, True) is None 400 | assert holiday(eighth + 1) is None 401 | chol_hamoed = [pesach + i for i in range(2, 6)] 402 | for day in chol_hamoed: 403 | assert festival(day, include_working_days=False) is None 404 | assert ( 405 | festival(pesach + 1, israel=True, include_working_days=False) 406 | is None 407 | ) 408 | assert holiday(pesach, prefix_day=True) == '1 Pesach' 409 | assert ( 410 | festival(pesach + 2, include_working_days=False, prefix_day=True) 411 | is None 412 | ) 413 | 414 | def test_pesach_sheni(self): 415 | ps = dates.HebrewDate(5781, 2, 14) 416 | assert holiday(ps) == 'Pesach Sheni' 417 | assert holiday(ps + 1) is None 418 | assert festival(ps, include_working_days=False) is None 419 | 420 | def test_lagbaomer(self): 421 | lag_baomer = dates.GregorianDate(2018, 5, 3) 422 | assert festival(lag_baomer) == "Lag Ba'omer" 423 | assert festival(lag_baomer, hebrew=True) == 'ל״ג בעומר' 424 | assert ( 425 | festival(lag_baomer, hebrew=True, include_working_days=False) 426 | is None 427 | ) 428 | 429 | def test_shavuos(self): 430 | shavuos = dates.HebrewDate(5778, 3, 6) 431 | assert all( 432 | (holiday(day) == 'Shavuos' for day in [shavuos, shavuos + 1])) 433 | assert holiday(shavuos, True) == 'Shavuos' 434 | assert holiday(shavuos + 1, True) is None 435 | assert festival(shavuos + 1, include_working_days=False) == 'Shavuos' 436 | not_shavuos = dates.HebrewDate(5782, 4, 7) 437 | assert festival(not_shavuos) is None 438 | assert ( 439 | festival(shavuos + 1, hebrew=True, prefix_day=True) == 'ב׳ שבועות' 440 | ) 441 | 442 | def test_tubeav(self): 443 | tubeav = dates.HebrewDate(5779, 5, 15) 444 | assert holiday(tubeav) == "Tu B'av" 445 | assert festival(tubeav, include_working_days=False) is None 446 | 447 | 448 | class TestFasts: 449 | 450 | def test_gedalia(self): 451 | assert fast_day(dates.HebrewDate(5779, 7, 3)) == 'Tzom Gedalia' 452 | assert holiday(dates.HebrewDate(5778, 7, 3)) is None 453 | assert ( 454 | holiday(dates.HebrewDate(5778, 7, 4), hebrew=True) == 'צום גדליה' 455 | ) 456 | 457 | def test_asara(self): 458 | ten_of_teves = dates.GregorianDate(2018, 12, 18) 459 | assert holiday(ten_of_teves) == '10 of Teves' 460 | assert fast_day(ten_of_teves, hebrew=True) == 'י׳ בטבת' 461 | 462 | def test_esther(self): 463 | fasts = [ 464 | dates.HebrewDate(5778, 12, 13), 465 | dates.HebrewDate(5776, 13, 13), 466 | dates.HebrewDate(5777, 12, 11), # nidche 467 | dates.HebrewDate(5784, 13, 11) # ibbur and nidche 468 | ] 469 | for fast in fasts: 470 | assert holiday(fast) == 'Taanis Esther' 471 | non_fasts = [ 472 | dates.HebrewDate(5776, 12, 13), 473 | dates.HebrewDate(5777, 12, 13), 474 | dates.HebrewDate(5784, 12, 11), 475 | dates.HebrewDate(5784, 13, 13) 476 | ] 477 | for non in non_fasts: 478 | assert holiday(non) is None 479 | 480 | def test_tamuz(self): 481 | fasts = [dates.HebrewDate(5777, 4, 17), dates.HebrewDate(5778, 4, 18)] 482 | for fast in fasts: 483 | assert holiday(fast) == '17 of Tamuz' 484 | assert holiday(dates.HebrewDate(5778, 4, 17)) is None 485 | 486 | def test_av(self): 487 | fasts = [dates.HebrewDate(5777, 5, 9), dates.HebrewDate(5778, 5, 10)] 488 | for fast in fasts: 489 | assert holiday(fast) == '9 of Av' 490 | assert holiday(dates.HebrewDate(5778, 5, 9)) is None 491 | 492 | 493 | def test_to_hebrew_numeral(): 494 | assert hebrewcal.to_hebrew_numeral(5782) == 'תשפ״ב' 495 | 496 | 497 | @fixture 498 | def cal(): 499 | return hebrewcal.HebrewCalendar() 500 | 501 | 502 | class TestCalendar: 503 | 504 | def test_setfirstweekday(self, cal): 505 | cal.firstweekday = 2 506 | assert cal.firstweekday == 2 507 | assert cal._firstpyweekday == 0 508 | cal.firstweekday = 1 509 | 510 | def test_iterweekdays(self): 511 | for startingweekday in range(1, 8): 512 | cal = hebrewcal.HebrewCalendar(startingweekday) 513 | weekdays = list(cal.iterweekdays()) 514 | assert len(weekdays) == 7 515 | assert weekdays[0] == startingweekday 516 | last = startingweekday - 1 or 7 517 | assert weekdays[-1] == last 518 | 519 | def test_first_month(self, cal): 520 | list(cal.itermonthdates(1, 7)) 521 | 522 | def test_itermonthdates(self, cal): 523 | adar2 = list(cal.itermonthdates(5782, 13)) 524 | assert adar2[0] == dates.HebrewDate(5782, 12, 26) 525 | assert adar2[-1] == dates.HebrewDate(5782, 1, 1) 526 | 527 | def test_minyear(self, cal): 528 | list(cal.itermonthdates(1, 1)) 529 | 530 | def test_itermonthdays4(self, cal): 531 | nissan = list(cal.itermonthdays4(5782, 1)) 532 | assert nissan[0] == (5782, 13, 24, 1) 533 | assert nissan[-1] == (5782, 2, 6, 7) 534 | 535 | def test_itermonthdays(self): 536 | for firstweekday in range(1, 8): 537 | cal = hebrewcal.HebrewCalendar(firstweekday) 538 | for y, m in [(1, 7), (6000, 12)]: 539 | days = list(cal.itermonthdays(y, m)) 540 | assert len(days) in [35, 42] 541 | 542 | def test_itermonthdays2(self, cal): 543 | tishrei = list(cal.itermonthdays2(5783, 7)) 544 | assert tishrei[0] == (0, 1) 545 | assert tishrei[-1] == (0, 7) 546 | 547 | def test_yeardatescalendar(self, cal): 548 | year = cal.yeardatescalendar(5783, 4) 549 | assert len(year) == 3 550 | assert len(year[1]) == 4 551 | assert year[2][3][4][6] == dates.HebrewDate(5784, 7, 1) 552 | 553 | def test_yeardays2calendar(self, cal): 554 | year = cal.yeardays2calendar(5784) 555 | assert len(year) == 5 556 | assert len(year[4]) == 1 557 | assert year[1][2][5][6] == (0, 7) 558 | assert year[2][0][2][0] == (14, 1) 559 | 560 | def test_yeardayscalendar(self, cal): 561 | year = cal.yeardayscalendar(5784) 562 | assert year[2][0][2][0] == 14 563 | 564 | def test_errors(self): 565 | with raises(hebrewcal.IllegalWeekdayError): 566 | hebrewcal.HebrewCalendar(0) 567 | with raises(hebrewcal.IllegalWeekdayError): 568 | hebrewcal.HebrewCalendar(8) 569 | 570 | def test_error_message(self): 571 | try: 572 | hebrewcal.HebrewCalendar(0) 573 | except hebrewcal.IllegalWeekdayError as e: 574 | assert str(e).startswith('bad weekday number 0;') 575 | try: 576 | Month(5781, 13) 577 | except hebrewcal.IllegalMonthError as e: 578 | assert str(e).startswith('bad month number 13;') 579 | 580 | 581 | PARSER = 'html.parser' 582 | 583 | 584 | def _soupbuilder(input, tag, class_): 585 | return BeautifulSoup(input, PARSER).find_all(tag, class_=class_) 586 | 587 | 588 | @fixture 589 | def htmlcal(): 590 | return HebrewHTMLCalendar() 591 | 592 | 593 | class TestHebrewHTMLCalendar: 594 | 595 | def test_formatday(self, htmlcal): 596 | assert htmlcal.formatday(0, 1) == ' ' 597 | assert htmlcal.formatday(1, 3) == 'א' 598 | assert htmlcal.formatday(15, 7) == 'טו' 599 | htmlcal.hebrewnumerals = False 600 | assert htmlcal.formatday(2, 2) == '2' 601 | assert htmlcal.formatday(21, 5) == '21' 602 | 603 | def test_length_in_months(self, htmlcal): 604 | months = _soupbuilder(htmlcal.formatyear(5784), 'table', 'month') 605 | assert len(months) == 13 606 | months = _soupbuilder(htmlcal.formatyear(5783), 'table', 'month') 607 | assert len(months) == 12 608 | 609 | def test_months(self, htmlcal): 610 | months = _soupbuilder(htmlcal.formatyear(5784), 'th', 'month') 611 | for i, month in enumerate(hebrewcal.Year(5784).itermonths()): 612 | assert months[i].string == month.month_name() 613 | htmlcal.hebrewmonths = True 614 | months = _soupbuilder(htmlcal.formatyear(5784), 'th', 'month') 615 | for i, month in enumerate(hebrewcal.Year(5784).itermonths()): 616 | assert months[i].string == month.month_name(True) 617 | 618 | def test_weekdays(self, htmlcal): 619 | year = htmlcal.formatyear(5783) 620 | sundays = _soupbuilder(year, 'th', 'sun') 621 | for sun in sundays: 622 | assert sun.string == calendar.day_abbr[6] 623 | htmlcal.hebrewweekdays = True 624 | mondays = _soupbuilder(htmlcal.formatyear(5782), 'th', 'mon') 625 | for mon in mondays: 626 | assert mon.string == 'שני' 627 | 628 | def test_days(self, htmlcal): 629 | year = htmlcal.formatyear(5782) 630 | soup = BeautifulSoup(year, PARSER) 631 | assert soup.find('td', class_='tue').string == 'א' 632 | assert soup.find_all('td', class_='tue')[2].string == 'טו' 633 | 634 | def test_year(self, htmlcal): 635 | month = htmlcal.formatmonth(5782, 2) 636 | header = BeautifulSoup(month, PARSER).find('th', class_='month') 637 | assert header.string == 'Iyar 5782' 638 | htmlcal.hebrewyear = True 639 | month = htmlcal.formatmonth(5782, 2) 640 | header = BeautifulSoup(month, PARSER).find('th', class_='month') 641 | assert header.string == 'Iyar תשפ״ב' 642 | 643 | def test_rtl(self, htmlcal): 644 | year = htmlcal.formatyear(5781) 645 | soup = BeautifulSoup(year, PARSER) 646 | assert soup.find('table', class_='year').get('dir', None) is None 647 | for month in soup.find_all('table', class_='month'): 648 | assert month.get('dir', None) is None 649 | htmlcal.rtl = True 650 | year = htmlcal.formatyear(5781) 651 | soup = BeautifulSoup(year, PARSER) 652 | assert soup.find('table', class_='year').get('dir', None) == 'rtl' 653 | for month in soup.find_all('table', class_='month'): 654 | assert month.get('dir', None) == 'rtl' 655 | 656 | 657 | @fixture 658 | def tcal(): 659 | return HebrewTextCalendar() 660 | 661 | 662 | class TestHebrewTextCalendar: 663 | 664 | def test_formatday(self, tcal): 665 | assert tcal.formatday(0, 1, 3) == ' ' 666 | assert tcal.formatday(2, 4, 4) == ' ב ' 667 | assert tcal.formatday(30, 5, 3) == ' ל' 668 | assert tcal.formatday(16, 7, 3) == ' טז' 669 | tcal.hebrewnumerals = False 670 | assert tcal.formatday(3, 2, 3) == ' 3' 671 | assert tcal.formatday(30, 7, 3) == ' 30' 672 | 673 | def test_formatweekday(self, tcal): 674 | py_tcal = calendar.TextCalendar(6) 675 | for w in [3, 10]: 676 | assert tcal.formatweekday(7, w) == py_tcal.formatweekday(5, w) 677 | tcal.hebrewweekdays = True 678 | assert tcal.formatweekday(2, 4) == ' ב׳ ' 679 | assert tcal.formatweekday(3, 5) == 'שלישי' 680 | 681 | def test_formatmonthname(self, tcal): 682 | assert tcal.formatmonthname(5782, 1) == 'Nissan 5782' 683 | assert tcal.formatmonthname(5784, 12) == 'Adar 1 5784' 684 | assert tcal.formatmonthname(5784, 12, withyear=False) == 'Adar 1' 685 | tcal.hebrewmonths = True 686 | tcal.hebrewyear = True 687 | assert tcal.formatmonthname(5784, 12) == 'אדר א׳ תשפ״ד' 688 | 689 | def test_formatyear(self, tcal): 690 | year = tcal.formatyear(5782) 691 | assert 'Adar 1' in year 692 | year = tcal.formatyear(5783) 693 | assert 'Adar 1' not in year 694 | -------------------------------------------------------------------------------- /src/pyluach/dates.py: -------------------------------------------------------------------------------- 1 | """The dates module implements classes for representing and 2 | manipulating several date types. 3 | 4 | Contents 5 | -------- 6 | * :class:`Rounding` 7 | * :class:`BaseDate` 8 | * :class:`CalendarDateMixin` 9 | * :class:`JulianDay` 10 | * :class:`GregorianDate` 11 | * :class:`HebrewDate` 12 | 13 | Note 14 | ---- 15 | All instances of the classes in this module should be treated as read 16 | only. No attributes should be changed once they're created. 17 | """ 18 | 19 | import abc 20 | from datetime import date 21 | from numbers import Number 22 | from enum import Enum, auto 23 | 24 | from pyluach import utils 25 | from pyluach import gematria 26 | 27 | 28 | class Rounding(Enum): 29 | """Enumerator to provide options for rounding Hebrew dates. 30 | 31 | This provides constants to use as arguments for functions. It 32 | should not be instantiated. 33 | 34 | Attributes 35 | ---------- 36 | PREVIOUS_DAY 37 | If the day is the 30th and the month only has 29 days, round to 38 | the 29th of the month. 39 | NEXT_DAY 40 | If the day is the 30th and the month only has 29 days, round to 41 | the 1st of the next month. 42 | EXCEPTION 43 | If the day is the 30th and the month only has 29 days, raise a 44 | ValueError. 45 | """ 46 | PREVIOUS_DAY = auto() 47 | NEXT_DAY = auto() 48 | EXCEPTION = auto() 49 | 50 | 51 | class BaseDate(abc.ABC): 52 | """BaseDate is a base class for all date types. 53 | 54 | It provides the following arithmetic and comparison operators 55 | common to all child date types. 56 | 57 | =================== ================================================= 58 | Operation Result 59 | =================== ================================================= 60 | d2 = date1 + int New date ``int`` days after date1 61 | d2 = date1 - int New date ``int`` days before date1 62 | int = date1 - date2 Positive integer equal to the duration from date1 63 | to date2 64 | date1 > date2 True if date1 occurs later than date2 65 | date1 < date2 True if date1 occurs earlier than date2 66 | date1 == date2 True if date1 occurs on the same day as date2 67 | date1 != date2 True if ``date1 == date2`` is False 68 | =================== ================================================= 69 | 70 | Any subclass of ``BaseDate`` can be compared to and diffed with any other 71 | subclass date. 72 | """ 73 | 74 | @property 75 | @abc.abstractmethod 76 | def jd(self): 77 | """Return julian day number. 78 | 79 | Returns 80 | ------- 81 | float 82 | The Julian day number at midnight (as ``n.5``). 83 | """ 84 | 85 | @abc.abstractmethod 86 | def to_heb(self): 87 | """Return Hebrew Date. 88 | 89 | Returns 90 | ------- 91 | HebrewDate 92 | """ 93 | 94 | def __hash__(self): 95 | return hash(repr(self)) 96 | 97 | def __add__(self, other): 98 | try: 99 | return JulianDay(self.jd + other)._to_x(self) 100 | except TypeError: 101 | return NotImplemented 102 | 103 | def __sub__(self, other): 104 | try: 105 | if isinstance(other, Number): 106 | return JulianDay(self.jd - other)._to_x(self) 107 | return int(abs(self.jd - other.jd)) 108 | except (AttributeError, TypeError): 109 | return NotImplemented 110 | 111 | def __eq__(self, other): 112 | try: 113 | return self.jd == other.jd 114 | except AttributeError: 115 | return NotImplemented 116 | 117 | def __ne__(self, other): 118 | try: 119 | return self.jd != other.jd 120 | except AttributeError: 121 | return NotImplemented 122 | 123 | def __lt__(self, other): 124 | try: 125 | return self.jd < other.jd 126 | except AttributeError: 127 | return NotImplemented 128 | 129 | def __gt__(self, other): 130 | try: 131 | return self.jd > other.jd 132 | except AttributeError: 133 | return NotImplemented 134 | 135 | def __le__(self, other): 136 | try: 137 | return self.jd <= other.jd 138 | except AttributeError: 139 | return NotImplemented 140 | 141 | def __ge__(self, other): 142 | try: 143 | return self.jd >= other.jd 144 | except AttributeError: 145 | return NotImplemented 146 | 147 | def weekday(self): 148 | """Return day of week as an integer. 149 | 150 | Returns 151 | ------- 152 | int 153 | An integer representing the day of the week with Sunday as 1 154 | through Saturday as 7. 155 | """ 156 | return int(self.jd+.5+1) % 7 + 1 157 | 158 | def isoweekday(self): 159 | """Return the day of the week corresponding to the iso standard. 160 | 161 | Returns 162 | ------- 163 | int 164 | An integer representing the day of the week where Monday 165 | is 1 and and Sunday is 7. 166 | """ 167 | weekday = self.weekday() 168 | if weekday == 1: 169 | return 7 170 | return weekday - 1 171 | 172 | def shabbos(self): 173 | """Return the Shabbos on or following the date. 174 | 175 | Returns 176 | ------- 177 | JulianDay, GregorianDate, or HebrewDate 178 | `self` if the date is Shabbos or else the following Shabbos as 179 | the same date type as called from. 180 | 181 | Examples 182 | -------- 183 | >>> heb_date = HebrewDate(5781, 3, 29) 184 | >>> greg_date = heb_date.to_greg() 185 | >>> heb_date.shabbos() 186 | HebrewDate(5781, 4, 2) 187 | >>> greg_date.shabbos() 188 | GregorianDate(2021, 6, 12) 189 | """ 190 | return self + (7 - self.weekday()) 191 | 192 | def _day_of_holiday(self, israel, hebrew=False): 193 | """Return the day of the holiday. 194 | 195 | Parameters 196 | ---------- 197 | israel : bool, optional 198 | hebrew : bool, optional 199 | 200 | Returns 201 | ------- 202 | str 203 | """ 204 | name = utils._festival_string(self, israel) 205 | if name is not None: 206 | holiday = utils._Days(name) 207 | if holiday is utils._Days.SHAVUOS and israel: 208 | return '' 209 | first_day = utils._first_day_of_holiday(holiday) 210 | if first_day: 211 | year = self.to_heb().year 212 | day = HebrewDate(year, *first_day) - self + 1 213 | if hebrew: 214 | day = gematria._num_to_str(day) 215 | return str(day) 216 | return '' 217 | 218 | def fast_day(self, hebrew=False): 219 | """Return name of fast day of date. 220 | 221 | Parameters 222 | ---------- 223 | hebrew : bool, optional 224 | ``True`` if you want the fast day name in Hebrew letters. Default 225 | is ``False``, which returns the name transliterated into English. 226 | 227 | Returns 228 | ------- 229 | str or None 230 | The name of the fast day or ``None`` if the date is not 231 | a fast day. 232 | """ 233 | return utils._fast_day_string(self, hebrew) 234 | 235 | def festival( 236 | self, 237 | israel=False, 238 | hebrew=False, 239 | include_working_days=True, 240 | prefix_day=False 241 | ): 242 | """Return name of Jewish festival of date. 243 | 244 | This method will return all major and minor religous 245 | Jewish holidays not including fast days. 246 | 247 | Parameters 248 | ---------- 249 | israel : bool, optional 250 | ``True`` if you want the holidays according to the Israel 251 | schedule. Defaults to ``False``. 252 | hebrew : bool, optional 253 | ``True`` if you want the festival name in Hebrew letters. Default 254 | is ``False``, which returns the name transliterated into English. 255 | include_working_days : bool, optional 256 | ``True`` to include festival days on which melacha (work) is 257 | allowed; ie. Pesach Sheni, Chol Hamoed, etc. 258 | Default is ``True``. 259 | prefix_day : bool, optional 260 | ``True`` to prefix multi day festivals with the day of the 261 | festival. Default is ``False``. 262 | 263 | Returns 264 | ------- 265 | str or None 266 | The name of the festival or ``None`` if the given date is not 267 | a Jewish festival. 268 | 269 | Examples 270 | -------- 271 | >>> pesach = HebrewDate(2023, 1, 15) 272 | >>> pesach.festival(prefix_day=True) 273 | '1 Pesach' 274 | >>> pesach.festival(hebrew=True, prefix_day=True) 275 | 'א׳ פסח' 276 | >>> shavuos = HebrewDate(5783, 3, 6) 277 | >>> shavuos.festival(israel=True, prefix_day=True) 278 | 'Shavuos' 279 | """ 280 | name = utils._festival_string( 281 | self, israel, hebrew, include_working_days 282 | ) 283 | if prefix_day and name is not None: 284 | day = self._day_of_holiday(israel=israel, hebrew=hebrew) 285 | if day: 286 | return f'{day} {name}' 287 | return name 288 | 289 | def holiday(self, israel=False, hebrew=False, prefix_day=False): 290 | """Return name of Jewish holiday of the date. 291 | 292 | The holidays include the major and minor religious Jewish 293 | holidays including fast days. 294 | 295 | Parameters 296 | ---------- 297 | israel : bool, optional 298 | ``True`` if you want the holidays according to the Israel 299 | schedule. Defaults to ``False``. 300 | hebrew : bool, optional 301 | ``True`` if you want the holiday name in Hebrew letters. Default is 302 | ``False``, which returns the name transliterated into English. 303 | prefix_day : bool, optional 304 | ``True`` to prefix multi day holidays with the day of the 305 | holiday. Default is ``False``. 306 | 307 | Returns 308 | ------- 309 | str or None 310 | The name of the holiday or ``None`` if the given date is not 311 | a Jewish holiday. 312 | 313 | Examples 314 | -------- 315 | >>> pesach = HebrewDate(2023, 1, 15) 316 | >>> pesach.holiday(prefix_day=True) 317 | '1 Pesach' 318 | >>> pesach.holiday(hebrew=True, prefix_day=True) 319 | 'א׳ פסח' 320 | >>> taanis_esther = HebrewDate(5783, 12, 13) 321 | >>> taanis_esther.holiday(prefix_day=True) 322 | 'Taanis Esther' 323 | """ 324 | return ( 325 | self.fast_day(hebrew=hebrew) 326 | or self.festival(israel, hebrew, prefix_day=prefix_day) 327 | ) 328 | 329 | 330 | class CalendarDateMixin: 331 | """CalendarDateMixin is a mixin for Hebrew and Gregorian dates. 332 | 333 | Parameters 334 | ---------- 335 | year : int 336 | month : int 337 | day : int 338 | 339 | Attributes 340 | ---------- 341 | year : int 342 | month : int 343 | day : int 344 | jd : float 345 | The equivalent Julian day at midnight. 346 | """ 347 | 348 | def __init__(self, year, month, day, jd=None): 349 | self.year = year 350 | self.month = month 351 | self.day = day 352 | self._jd = jd 353 | 354 | def __repr__(self): 355 | class_name = self.__class__.__name__ 356 | return f'{class_name}({self.year}, {self.month}, {self.day})' 357 | 358 | def __str__(self): 359 | return f'{self.year:04d}-{self.month:02d}-{self.day:02d}' 360 | 361 | def __iter__(self): 362 | yield self.year 363 | yield self.month 364 | yield self.day 365 | 366 | def tuple(self): 367 | """Return date as tuple. 368 | 369 | Returns 370 | ------- 371 | tuple of ints 372 | A tuple of ints in the form ``(year, month, day)``. 373 | """ 374 | return (self.year, self.month, self.day) 375 | 376 | def dict(self): 377 | """Return the date as a dictionary. 378 | 379 | Returns 380 | ------- 381 | dict 382 | A dictionary in the form 383 | ``{'year': int, 'month': int, 'day': int}``. 384 | """ 385 | return {'year': self.year, 'month': self.month, 'day': self.day} 386 | 387 | def replace(self, year=None, month=None, day=None): 388 | """Return new date with new values for the specified field. 389 | 390 | Parameters 391 | ---------- 392 | year : int, optional 393 | month: int, optional 394 | day : int, optional 395 | 396 | Returns 397 | ------- 398 | CalendarDateMixin 399 | Any date that inherits from CalendarDateMixin 400 | (``GregorianDate``, ````HebrewDate``). 401 | 402 | Raises 403 | ValueError 404 | Raises a ``ValueError`` if the new date does not exist. 405 | """ 406 | if year is None: 407 | year = self.year 408 | if month is None: 409 | month = self.month 410 | if day is None: 411 | day = self.day 412 | return type(self)(year, month, day) 413 | 414 | 415 | class JulianDay(BaseDate): 416 | """A JulianDay object represents a Julian Day at midnight. 417 | 418 | Parameters 419 | ---------- 420 | day : float or int 421 | The julian day. Note that Julian days start at noon so day 422 | number 10 is represented as 9.5 which is day 10 at midnight. 423 | 424 | Attributes 425 | ---------- 426 | day : float 427 | The Julian Day Number at midnight (as *n*.5) 428 | """ 429 | 430 | def __init__(self, day): 431 | if day-int(day) < .5: 432 | self.day = int(day) - .5 433 | else: 434 | self.day = int(day) + .5 435 | 436 | def __repr__(self): 437 | return f'JulianDay({self.day})' 438 | 439 | def __str__(self): 440 | return str(self.day) 441 | 442 | @property 443 | def jd(self): 444 | """Return julian day. 445 | 446 | Returns 447 | ------- 448 | float 449 | """ 450 | return self.day 451 | 452 | @staticmethod 453 | def from_pydate(pydate): 454 | """Return a `JulianDay` from a python date object. 455 | 456 | Parameters 457 | ---------- 458 | pydate : datetime.date 459 | A python standard library ``datetime.date`` instance 460 | 461 | Returns 462 | ------- 463 | JulianDay 464 | """ 465 | return GregorianDate.from_pydate(pydate).to_jd() 466 | 467 | @staticmethod 468 | def today(): 469 | """Return instance of current Julian day from timestamp. 470 | 471 | Extends the built-in ``datetime.date.today()``. 472 | 473 | Returns 474 | ------- 475 | JulianDay 476 | A JulianDay instance representing the current Julian day from 477 | the timestamp. 478 | 479 | Warning 480 | ------- 481 | Julian Days change at noon, but pyluach treats them as if they 482 | change at midnight, so at midnight this method will return 483 | ``JulianDay(n.5)`` until the following midnight when it will 484 | return ``JulianDay(n.5 + 1)``. 485 | """ 486 | return GregorianDate.today().to_jd() 487 | 488 | def to_greg(self): 489 | """Convert JulianDay to a Gregorian Date. 490 | 491 | Returns 492 | ------- 493 | GregorianDate 494 | The equivalent Gregorian date instance. 495 | 496 | Notes 497 | ----- 498 | This method uses the Fliegel-Van Flandern algorithm. 499 | """ 500 | jd = int(self.day + .5) 501 | L = jd + 68569 502 | n = 4*L // 146097 503 | L = L - (146097*n + 3) // 4 504 | i = (4000 * (L+1)) // 1461001 505 | L = L - ((1461*i) // 4) + 31 506 | j = (80*L) // 2447 507 | day = L - 2447*j // 80 508 | L = j // 11 509 | month = j + 2 - 12*L 510 | year = 100 * (n-49) + i + L 511 | if year < 1: 512 | year -= 1 513 | return GregorianDate(year, month, day, self.day) 514 | 515 | def to_heb(self): 516 | """ Convert to a Hebrew date. 517 | 518 | Returns 519 | ------- 520 | HebrewDate 521 | The equivalent Hebrew date instance. 522 | """ 523 | 524 | if self.day <= 347997: 525 | raise ValueError('Date is before creation') 526 | 527 | jd = int(self.day + .5) # Try to account for half day 528 | jd -= 347997 529 | year = int(jd//365) + 2 # try that to debug early years 530 | first_day = utils._elapsed_days(year) 531 | 532 | while first_day > jd: 533 | year -= 1 534 | first_day = utils._elapsed_days(year) 535 | 536 | months = utils._monthslist(year) 537 | days_remaining = jd - first_day 538 | for month in months: 539 | if days_remaining >= utils._month_length(year, month): 540 | days_remaining -= utils._month_length(year, month) 541 | else: 542 | return HebrewDate(year, month, days_remaining + 1, self.day) 543 | 544 | def _to_x(self, type_): 545 | """Return a date object of the given type.""" 546 | 547 | if isinstance(type_, GregorianDate): 548 | return self.to_greg() 549 | if isinstance(type_, HebrewDate): 550 | return self.to_heb() 551 | if isinstance(type_, JulianDay): 552 | return self 553 | raise TypeError( 554 | 'This method has not been implemented with that type.' 555 | ) 556 | 557 | def to_pydate(self): 558 | """Convert to a datetime.date object. 559 | 560 | Returns 561 | ------- 562 | datetime.date 563 | A standard library ``datetime.date`` instance. 564 | """ 565 | return self.to_greg().to_pydate() 566 | 567 | 568 | class GregorianDate(BaseDate, CalendarDateMixin): 569 | """A GregorianDate object represents a Gregorian date (year, month, day). 570 | 571 | This is an idealized date with the current Gregorian calendar 572 | infinitely extended in both directions. 573 | 574 | Parameters 575 | ---------- 576 | year : int 577 | month : int 578 | day : int 579 | jd : float, optional 580 | This parameter should not be assigned manually. 581 | 582 | Attributes 583 | ---------- 584 | year : int 585 | month : int 586 | day : int 587 | 588 | Warnings 589 | -------- 590 | Although B.C.E. dates are allowed, they should be treated as 591 | approximations as they may return inconsistent results when converting 592 | between date types and using arithmetic and comparison operators! 593 | """ 594 | 595 | def __init__(self, year, month, day, jd=None): 596 | """Initialize a GregorianDate. 597 | 598 | This initializer extends the CalendarDateMixin initializer 599 | adding in date validation specific to Gregorian dates. 600 | """ 601 | if month < 1 or month > 12: 602 | raise ValueError(f'{str(month)} is an invalid month.') 603 | monthlength = self._monthlength(year, month) 604 | if day < 1 or day > monthlength: 605 | raise ValueError(f'Given month has {monthlength} days.') 606 | super().__init__(year, month, day, jd) 607 | 608 | def __format__(self, fmt): 609 | return self.strftime(fmt) 610 | 611 | def strftime(self, fmt): 612 | """Return formatted date. 613 | 614 | Wraps :py:meth:`datetime.date.strftime` method and uses the same 615 | format options. 616 | 617 | Parameters 618 | ---------- 619 | fmt : str 620 | The format string. 621 | 622 | Returns 623 | ------- 624 | str 625 | """ 626 | return self.to_pydate().strftime(fmt) 627 | 628 | @property 629 | def jd(self): 630 | """Return the corresponding Julian day number. 631 | 632 | Returns 633 | ------- 634 | float 635 | The Julian day number at midnight. 636 | """ 637 | if self._jd is None: 638 | year = self.year 639 | month = self.month 640 | day = self.day 641 | if year < 0: 642 | year += 1 643 | if month < 3: 644 | year -= 1 645 | month += 12 646 | month += 1 647 | a = year // 100 648 | b = 2 - a + a//4 649 | self._jd = ( 650 | int(365.25*year) + int(30.6001*month) + b + day + 1720994.5 651 | ) 652 | return self._jd 653 | 654 | @classmethod 655 | def from_pydate(cls, pydate): 656 | """Return a `GregorianDate` instance from a python date object. 657 | 658 | Parameters 659 | ---------- 660 | pydate : datetime.date 661 | A python standard library ``datetime.date`` instance. 662 | 663 | Returns 664 | ------- 665 | GregorianDate 666 | """ 667 | return cls(*pydate.timetuple()[:3]) 668 | 669 | @staticmethod 670 | def today(): 671 | """Return a GregorianDate instance for the current day. 672 | 673 | This static method wraps the Python standard library's 674 | date.today() method to get the date from the timestamp. 675 | 676 | Returns 677 | ------- 678 | GregorianDate 679 | The current Gregorian date from the computer's timestamp. 680 | """ 681 | return GregorianDate.from_pydate(date.today()) 682 | 683 | @staticmethod 684 | def _is_leap(year): 685 | """Return True if year of date is a leap year, otherwise False.""" 686 | if year < 0: 687 | year += 1 688 | if (year % 4 == 0) and not (year % 100 == 0 and year % 400 != 0): 689 | return True 690 | return False 691 | 692 | def is_leap(self): 693 | """Return if the date is in a leap year 694 | 695 | Returns 696 | ------- 697 | bool 698 | True if the date is in a leap year, False otherwise. 699 | """ 700 | return self._is_leap(self.year) 701 | 702 | @classmethod 703 | def _monthlength(cls, year, month): 704 | if month in [1, 3, 5, 7, 8, 10, 12]: 705 | return 31 706 | if month == 2: 707 | if cls._is_leap(year): 708 | return 29 709 | return 28 710 | return 30 711 | 712 | def to_jd(self): 713 | """Convert to a Julian day. 714 | 715 | Returns 716 | ------- 717 | JulianDay 718 | The equivalent JulianDay instance. 719 | """ 720 | return JulianDay(self.jd) 721 | 722 | def to_heb(self): 723 | """Convert to Hebrew date. 724 | 725 | Returns 726 | ------- 727 | HebrewDate 728 | The equivalent HebrewDate instance. 729 | """ 730 | return self.to_jd().to_heb() 731 | 732 | def to_pydate(self): 733 | """Convert to a standard library date. 734 | 735 | Returns 736 | ------- 737 | datetime.date 738 | The equivalent datetime.date instance. 739 | """ 740 | return date(*self.tuple()) 741 | 742 | 743 | class HebrewDate(BaseDate, CalendarDateMixin): 744 | """A class for manipulating Hebrew dates. 745 | 746 | The following format options are available similar to strftime: 747 | 748 | ====== ======= =========================================================== 749 | Format Example Meaning 750 | ====== ======= =========================================================== 751 | %a Sun Weekday as locale's abbreviated name 752 | %A Sunday Weekday as locale's full name 753 | %w 1 Weekday as decimal number 1-7 Sunday-Shabbos 754 | %d 07 Day of the month as a 0-padded 2 digit decimal number 755 | %-d 7 Day of the month as a decimal number 756 | %B Iyar Month name transliterated into English 757 | %m 02 Month as a 0-padded 2 digit decimal number 758 | %-m 2 Month as a decimal number 759 | %y 82, 01 Year without century as a zero-padded decimal number 760 | %Y 5782 Year as a decimal number 761 | %*a א׳ Weekday as a Hebrew numeral 762 | %*A ראשון Weekday name in Hebrew 763 | %*d ז׳, ט״ז Day of month as Hebrew numeral 764 | %*-d א, טו Day of month without gershayim 765 | %*B אייר Name of month in Hebrew 766 | %*y תשפ״ב Year in Hebrew numerals without the thousands place 767 | %*Y ה'תשפ״ב Year in Hebrew numerals with the thousands place 768 | %% % A literal '%' character 769 | ====== ======= =========================================================== 770 | 771 | Example 772 | ------- 773 | >>> date = HebrewDate(5783, 1, 15) 774 | >>> f'Today is {date:%a - %*-d %*B, %*y}' 775 | 'Today is Thu - טו אייר, תשפ"ג' 776 | 777 | Parameters 778 | ---------- 779 | year : int 780 | The Hebrew year. 781 | 782 | month : int 783 | The Hebrew month starting with Nissan as 1 (and Tishrei as 7). If 784 | there is a second Adar in the year it is has a value of 13. 785 | 786 | day : int 787 | The Hebrew day of the month. 788 | 789 | jd : float, optional 790 | This parameter should not be assigned manually. 791 | 792 | Attributes 793 | ---------- 794 | year : int 795 | month : int 796 | The Hebrew month starting with Nissan as 1 (and Tishrei as 7). 797 | If there is a second Adar it has a value of 13. 798 | day : int 799 | The day of the month. 800 | 801 | Raises 802 | ------ 803 | ValueError 804 | If the year is less than 1, if the month is less than 1 or greater 805 | than the last month, or if the day does not exist in the month a 806 | ``ValueError`` will be raised. 807 | """ 808 | 809 | def __init__(self, year, month, day, jd=None): 810 | 811 | """Initialize a HebrewDate instance. 812 | 813 | This initializer extends the CalendarDateMixin adding validation 814 | specific to hebrew dates. 815 | """ 816 | if year < 1: 817 | raise ValueError('Year must be >= 1.') 818 | if month < 1 or month > 13: 819 | raise ValueError(f'{month} is an invalid month.') 820 | if (not utils._is_leap(year)) and month == 13: 821 | raise ValueError(f'{year} is not a leap year') 822 | monthlength = utils._month_length(year, month) 823 | if day < 1 or day > monthlength: 824 | raise ValueError(f'Given month has {monthlength} days.') 825 | super().__init__(year, month, day, jd) 826 | 827 | def __format__(self, fmt): 828 | new = [] 829 | i = 0 830 | while i < len(fmt): 831 | if fmt[i] != '%': 832 | new.append(fmt[i]) 833 | else: 834 | i += 1 835 | try: 836 | curr = fmt[i] 837 | except IndexError as e: 838 | raise ValueError( 839 | 'Format string cannot end with single "%".' 840 | ) from e 841 | if curr == '%': 842 | new.append('%') 843 | elif curr == '*': 844 | i += 1 845 | try: 846 | curr = fmt[i] 847 | except IndexError as e: 848 | raise ValueError( 849 | 'Format string cannot end with "%*".' 850 | ) from e 851 | if curr == '-': 852 | i += 1 853 | try: 854 | curr = fmt[i] 855 | except IndexError as e: 856 | raise ValueError( 857 | 'Format string cannot end with "%*-"' 858 | ) from e 859 | if curr == 'd': 860 | new.append(self.hebrew_day(False)) 861 | else: 862 | raise ValueError('Invalid format string.') 863 | elif curr == 'a': 864 | new.append(gematria._num_to_str(self.weekday())) 865 | elif curr == 'A': 866 | new.append(utils.WEEKDAYS[self.weekday()]) 867 | elif curr == 'd': 868 | new.append(self.hebrew_day()) 869 | elif curr == 'B': 870 | new.append(self.month_name(True)) 871 | elif curr.casefold() == 'y': 872 | new.append(self.hebrew_year(curr == 'Y')) 873 | else: 874 | raise ValueError('Invalid format string.') 875 | elif curr == '-': 876 | i += 1 877 | try: 878 | curr = fmt[i] 879 | except IndexError as e: 880 | raise ValueError( 881 | 'Format string cannot end with "%-"' 882 | ) from e 883 | if curr == 'd': 884 | new.append(str(self.day)) 885 | elif curr == 'm': 886 | new.append(str(self.month)) 887 | else: 888 | raise ValueError('Invalid format string.') 889 | else: 890 | if curr.casefold() == 'a': 891 | new.append(self.to_pydate().strftime(f'%{curr}')) 892 | elif curr == 'w': 893 | new.append(str(self.weekday())) 894 | elif curr == 'd': 895 | new.append(format(self.day, '02d')) 896 | elif curr == 'B': 897 | new.append(self.month_name(False)) 898 | elif curr == 'm': 899 | new.append(format(self.month, '02d')) 900 | elif curr.casefold() == 'y': 901 | new.append(date(self.year, 1, 1).strftime(f'%{curr}')) 902 | else: 903 | raise ValueError('Invalid format string.') 904 | i += 1 905 | return ''.join(new) 906 | 907 | @property 908 | def jd(self): 909 | """Return the corresponding Julian day number. 910 | 911 | Returns 912 | ------- 913 | float 914 | The Julian day number at midnight. 915 | 916 | """ 917 | if self._jd is None: 918 | months = utils._monthslist(self.year) 919 | jd = utils._elapsed_days(self.year) 920 | for m in months: 921 | if m != self.month: 922 | jd += utils._month_length(self.year, m) 923 | else: 924 | self._jd = jd + (self.day-1) + 347996.5 925 | 926 | return self._jd 927 | 928 | @staticmethod 929 | def from_pydate(pydate): 930 | """Return a `HebrewDate` from a python date object. 931 | 932 | Parameters 933 | ---------- 934 | pydate : datetime.date 935 | A python standard library ``datetime.date`` instance 936 | 937 | Returns 938 | ------- 939 | HebrewDate 940 | """ 941 | return GregorianDate.from_pydate(pydate).to_heb() 942 | 943 | @staticmethod 944 | def today(): 945 | """Return HebrewDate instance for the current day. 946 | 947 | This static method wraps the Python standard library's 948 | ``date.today()`` method to get the date from the timestamp. 949 | 950 | Returns 951 | ------- 952 | HebrewDate 953 | The current Hebrew date from the computer's timestamp. 954 | 955 | Warning 956 | ------- 957 | Pyluach treats Hebrew dates as if they change at midnight. If it's 958 | after nightfall but before midnight, to get the true Hebrew date do 959 | ``HebrewDate.today() + 1``. 960 | """ 961 | return GregorianDate.today().to_heb() 962 | 963 | def to_jd(self): 964 | """Convert to a Julian day. 965 | 966 | Returns 967 | ------- 968 | JulianDay 969 | The equivalent JulianDay instance. 970 | """ 971 | return JulianDay(self.jd) 972 | 973 | def to_greg(self): 974 | """Convert to a Gregorian date. 975 | 976 | Returns 977 | ------- 978 | GregorianDate 979 | The equivalent GregorianDate instance. 980 | """ 981 | return self.to_jd().to_greg() 982 | 983 | def to_pydate(self): 984 | """Convert to a standard library date. 985 | 986 | Returns 987 | ------- 988 | datetime.date 989 | The equivalent datetime.date instance. 990 | """ 991 | return self.to_greg().to_pydate() 992 | 993 | def to_heb(self): 994 | return self 995 | 996 | def month_name(self, hebrew=False): 997 | """Return the name of the month. 998 | 999 | Parameters 1000 | ---------- 1001 | hebrew : bool, optional 1002 | ``True`` if the month name should be in Hebrew characters. 1003 | Default is ``False`` which returns the month name 1004 | transliterated into English. 1005 | 1006 | Returns 1007 | ------- 1008 | str 1009 | """ 1010 | return utils._month_name(self.year, self.month, hebrew) 1011 | 1012 | def hebrew_day(self, withgershayim=True): 1013 | """Return the day of the month in Hebrew letters. 1014 | 1015 | Parameters 1016 | ---------- 1017 | withgershayim : bool, optional 1018 | Default is ``True`` which includes a geresh with a single 1019 | character and gershayim between two characters. 1020 | 1021 | Returns 1022 | ------- 1023 | str 1024 | The day of the month in Hebrew letters. 1025 | 1026 | Examples 1027 | -------- 1028 | >>> date = HebrewDate(5782, 3, 6) 1029 | >>> date.hebrew_day() 1030 | 'ו׳' 1031 | >>> date.hebrew_day(False) 1032 | 'ו' 1033 | >>> HebrewDate(5783, 12, 14).hebrew_day() 1034 | 'י״ד' 1035 | """ 1036 | return gematria._num_to_str(self.day, withgershayim=withgershayim) 1037 | 1038 | def hebrew_year(self, thousands=False, withgershayim=True): 1039 | """Return the year in Hebrew letters. 1040 | 1041 | Parameters 1042 | ---------- 1043 | thousands : bool 1044 | ``True`` to prefix the year with a letter for the 1045 | thousands place, ie. 'ה׳תשפ״א'. Default is ``False``. 1046 | withgershayim : bool, optional 1047 | Default is ``True`` which includes a geresh after the thousands 1048 | place if applicable and a gershayim before the last character 1049 | of the year. 1050 | 1051 | Returns 1052 | ------- 1053 | str 1054 | """ 1055 | return gematria._num_to_str(self.year, thousands, withgershayim) 1056 | 1057 | def hebrew_date_string(self, thousands=False): 1058 | """Return a Hebrew string representation of the date. 1059 | 1060 | The date is in the form ``f'{day} {month} {year}'``. 1061 | 1062 | Parameters 1063 | ---------- 1064 | thousands : bool 1065 | ``True`` to have the thousands include in the year. 1066 | Default is ``False``. 1067 | 1068 | Returns 1069 | ------- 1070 | str 1071 | 1072 | Examples 1073 | -------- 1074 | >>> date = HebrewDate(5781, 9, 25) 1075 | >>> date.hebrew_date_string() 1076 | 'כ״ה כסלו תשפ״א' 1077 | >>> date.hebrew_date_string(True) 1078 | 'כ״ה כסלו ה׳תשפ״א' 1079 | """ 1080 | day = self.hebrew_day() 1081 | month = self.month_name(True) 1082 | year = self.hebrew_year(thousands) 1083 | return f'{day} {month} {year}' 1084 | 1085 | def add( 1086 | self, 1087 | years=0, 1088 | months=0, 1089 | days=0, 1090 | adar1=False, 1091 | rounding=Rounding.NEXT_DAY 1092 | ): 1093 | """Add years, months, and days to date. 1094 | 1095 | Parameters 1096 | ---------- 1097 | years : int, optional 1098 | The number of years to add. Default is 0. 1099 | months : int, optional 1100 | The number of months to add. Default is 0. 1101 | days : int, optional 1102 | The number of days to add. Default is 0. 1103 | adar1 : bool, optional 1104 | True to return a date in Adar Aleph if `self` is in a regular 1105 | Adar and after adding the years it's leap year. Default is 1106 | ``False`` which will return the date in Adar Beis. 1107 | rounding : Rounding, optional 1108 | Choose what to do if self is the 30th day of the month, and 1109 | there are only 29 days in the destination month. 1110 | :obj:`Rounding.NEXT_DAY` to return the first day of the next 1111 | month. :obj:`Rounding.PREVIOUS_DAY` to return the last day of 1112 | the month. :obj:`Rounding.EXCEPTION` to raise a ValueError. 1113 | Default is :obj:`Rounding.NEXT_DAY`. 1114 | 1115 | Returns 1116 | ------- 1117 | HebrewDate 1118 | 1119 | Note 1120 | ---- 1121 | This method first adds the `years`. If the starting month is 1122 | Adar and the destination year has two Adars, it chooses which 1123 | one based on the `adar1` argument, then it adds the `months`. If 1124 | the starting day doesn't exist in that month it adjusts it based 1125 | on the `rounding` argument, then it adds the `days`. 1126 | 1127 | Examples 1128 | -------- 1129 | >>> date = HebrewDate(5783, 11, 30) 1130 | >>> date.add(months=1) 1131 | HebrewDate(5783, 1, 1) 1132 | >>> date.add(months=1, rounding=Rounding.PREVIOUS_DAY) 1133 | HebrewDate(5783, 12, 29) 1134 | """ 1135 | year = self.year + years 1136 | month = self.month 1137 | if self.month == 13 and not utils._is_leap(year): 1138 | month = 12 1139 | elif ( 1140 | self.month == 12 1141 | and not utils._is_leap(self.year) 1142 | and utils._is_leap(year) 1143 | and not adar1 1144 | ): 1145 | month = 13 1146 | if months > 0: 1147 | year, month = utils._add_months(year, month, months) 1148 | elif months < 0: 1149 | year, month = utils._subtract_months(year, month, -months) 1150 | if utils._month_length(year, month) < self.day: 1151 | date = HebrewDate(year, month, 29) 1152 | if rounding is Rounding.EXCEPTION: 1153 | raise ValueError(f'{date:%B %Y} has only 29 days.') 1154 | if rounding is Rounding.NEXT_DAY: 1155 | date += 1 1156 | elif not isinstance(rounding, Rounding): 1157 | raise TypeError( 1158 | 'The rounding argument can only be a member of the' 1159 | ' dates.Rounding enum.' 1160 | ) 1161 | else: 1162 | date = HebrewDate(year, month, self.day) 1163 | return date + days 1164 | 1165 | def subtract( 1166 | self, 1167 | years=0, 1168 | months=0, 1169 | days=0, 1170 | adar1=False, 1171 | rounding=Rounding.NEXT_DAY 1172 | ): 1173 | """Subtract years, months, and days from date. 1174 | 1175 | Parameters 1176 | ---------- 1177 | years : int, optional 1178 | The number of years to subtract. Default is 0. 1179 | months : int, optional 1180 | The number of months to subtract. Default is 0. 1181 | days : int, optional 1182 | The number of days to subtract. Default is 0. 1183 | adar1 : bool, optional 1184 | True to return a date in Adar Aleph if `self` is in a regular 1185 | Adar and the destination year is leap year. Default is 1186 | ``False`` which will return the date in Adar Beis. 1187 | rounding : Rounding, optional 1188 | Choose what to do if self is the 30th day of the month, and 1189 | there are only 29 days in the destination month. 1190 | :obj:`Rounding.NEXT_DAY` to return the first day of the next 1191 | month. :obj:`Rounding.PREVIOUS_DAY` to return the last day of 1192 | the month. :obj:`Rounding.EXCEPTION` to raise a ValueError. 1193 | Default is :obj:`Rounding.NEXT_DAY`. 1194 | 1195 | Returns 1196 | ------- 1197 | HebrewDate 1198 | 1199 | Note 1200 | ---- 1201 | This method first subtracts the `years`. If the starting month 1202 | is Adar and the destination year has two Adars, it chooses which 1203 | one based on the `adar1` argument, then it subtracts the 1204 | `months`. If the starting day doesn't exist in that month it 1205 | adjusts it based on the `rounding` argument, then it subtracts 1206 | the `days`. 1207 | """ 1208 | return self.add(-years, -months, -days, adar1, rounding) 1209 | -------------------------------------------------------------------------------- /src/pyluach/hebrewcal.py: -------------------------------------------------------------------------------- 1 | """The hebrewcal module contains Hebrew calendar related classes and functions. 2 | 3 | It contains classes for representing a Hebrew year and month, functions 4 | for getting the holiday or fast day for a given date, and classes adapting 5 | :py:mod:`calendar` classes to render Hebrew calendars. 6 | 7 | Contents 8 | -------- 9 | * :class:`Year` 10 | * :class:`Month` 11 | * :func:`to_hebrew_numeral` 12 | * :class:`HebrewCalendar` 13 | * :class:`HebrewHTMLCalendar` 14 | * :class:`HebrewTextCalendar` 15 | * :func:`fast_day` 16 | * :func:`festival` 17 | * :func:`holiday` 18 | """ 19 | from numbers import Number 20 | from itertools import repeat 21 | import calendar 22 | 23 | from pyluach.dates import HebrewDate 24 | from pyluach import utils 25 | from pyluach.gematria import _num_to_str 26 | 27 | 28 | class IllegalMonthError(ValueError): 29 | """An exception for an illegal month. 30 | 31 | Subclasses ``ValueError`` to show a message for an invalid month number 32 | for the Hebrew calendar. Mimics :py:class:`calendar.IllegalMonthError`. 33 | 34 | Parameters 35 | ---------- 36 | month : int 37 | The invalid month number 38 | """ 39 | def __init__(self, month): 40 | self.month = month 41 | 42 | def __str__(self): 43 | return ( 44 | f'bad month number {self.month}; must be 1-12 or 13 in a leap year' 45 | ) 46 | 47 | 48 | class IllegalWeekdayError(ValueError): 49 | """An exception for an illegal weekday. 50 | 51 | Subclasses ``ValueError`` to show a message for an invalid weekday 52 | number. Mimics :py:class:`calendar.IllegalWeekdayError`. 53 | 54 | Parameters 55 | ---------- 56 | month : int 57 | The invalid month number 58 | """ 59 | def __init__(self, weekday): 60 | self.weekday = weekday 61 | 62 | def __str__(self): 63 | return ( 64 | f'bad weekday number {self.weekday}; ' 65 | f'must be 1 (Sunday) to 7 (Saturday)' 66 | ) 67 | 68 | 69 | class Year: 70 | """A Year object represents a Hebrew calendar year. 71 | 72 | It provided the following operators: 73 | 74 | ===================== ================================================ 75 | Operation Result 76 | ===================== ================================================ 77 | year2 = year1 + int New ``Year`` ``int`` days after year1. 78 | year2 = year1 - int New ``Year`` ``int`` days before year1. 79 | int = year1 - year2 ``int`` equal to the absolute value of 80 | the difference between year2 and year1. 81 | bool = year1 == year2 True if year1 represents the same year as year2. 82 | bool = year1 > year2 True if year1 is later than year2. 83 | bool = year1 >= year2 True if year1 is later or equal to year2. 84 | bool = year1 < year2 True if year 1 earlier than year2. 85 | bool = year1 <= year2 True if year 1 earlier or equal to year 2. 86 | ===================== ================================================ 87 | 88 | Parameters 89 | ---------- 90 | year : int 91 | A Hebrew year. 92 | 93 | Attributes 94 | ---------- 95 | year : int 96 | The hebrew year. 97 | leap : bool 98 | True if the year is a leap year else false. 99 | """ 100 | 101 | def __init__(self, year): 102 | if year < 1: 103 | raise ValueError(f'Year {year} is before creation.') 104 | self.year = year 105 | self.leap = utils._is_leap(year) 106 | 107 | def __repr__(self): 108 | return f'Year({self.year})' 109 | 110 | def __len__(self): 111 | return utils._days_in_year(self.year) 112 | 113 | def __eq__(self, other): 114 | if isinstance(other, Year): 115 | return self.year == other.year 116 | return NotImplemented 117 | 118 | def __add__(self, other): 119 | """Add int to year.""" 120 | try: 121 | return Year(self.year + other) 122 | except TypeError: 123 | return NotImplemented 124 | 125 | def __sub__(self, other): 126 | """Subtract int or Year from Year. 127 | 128 | If other is an int return a new Year other before original year. If 129 | other is a Year object, return delta of the two years as an int. 130 | """ 131 | if isinstance(other, Year): 132 | return abs(self.year - other.year) 133 | try: 134 | return Year(self.year - other) 135 | except TypeError: 136 | return NotImplemented 137 | 138 | def __gt__(self, other): 139 | if isinstance(other, Year): 140 | return self.year > other.year 141 | return NotImplemented 142 | 143 | def __ge__(self, other): 144 | if isinstance(other, Year): 145 | return self > other or self == other 146 | return NotImplemented 147 | 148 | def __lt__(self, other): 149 | if isinstance(other, Year): 150 | return self.year < other.year 151 | return NotImplemented 152 | 153 | def __le__(self, other): 154 | if isinstance(other, Year): 155 | return self < other or self == other 156 | return NotImplemented 157 | 158 | def __iter__(self): 159 | """Yield integer for each month in year.""" 160 | yield from utils._monthslist(self.year) 161 | 162 | def monthscount(self): 163 | """Return number of months in the year. 164 | 165 | Returns 166 | ------- 167 | int 168 | """ 169 | if self.leap: 170 | return 13 171 | return 12 172 | 173 | def itermonths(self): 174 | """Yield Month instance for each month of the year. 175 | 176 | Yields 177 | ------ 178 | :obj:`Month` 179 | The next month in the Hebrew calendar year as a 180 | ``Month`` instance beginning with Tishrei through Elul. 181 | """ 182 | for month in self: 183 | yield Month(self.year, month) 184 | 185 | def iterdays(self): 186 | """Yield integer for each day of the year. 187 | 188 | Yields 189 | ------ 190 | :obj:`int` 191 | An integer beginning with 1 for the the next day of 192 | the year. 193 | """ 194 | for day in range(1, len(self) + 1): 195 | yield day 196 | 197 | def iterdates(self): 198 | """Iterate through each Hebrew date of the year. 199 | 200 | Yields 201 | ------ 202 | :obj:`pyluach.dates.HebrewDate` 203 | The next date of the Hebrew calendar year starting with 204 | the first of Tishrei. 205 | """ 206 | for month in self.itermonths(): 207 | for day in month: 208 | yield HebrewDate(self.year, month.month, day) 209 | 210 | @classmethod 211 | def from_date(cls, date): 212 | """Return Year object that given date occurs in. 213 | 214 | Parameters 215 | ---------- 216 | date : ~pyluach.dates.BaseDate 217 | Any subclass of ``BaseDate``. 218 | 219 | Returns 220 | ------- 221 | Year 222 | """ 223 | return cls(date.to_heb().year) 224 | 225 | @classmethod 226 | def from_pydate(cls, pydate): 227 | """Return Year object from python date object. 228 | 229 | Parameters 230 | ---------- 231 | pydate : datetime.date 232 | A python standard library date object 233 | 234 | Returns 235 | ------- 236 | Year 237 | The Hebrew year the given date occurs in 238 | """ 239 | return cls.from_date(HebrewDate.from_pydate(pydate)) 240 | 241 | def year_string(self, thousands=False): 242 | """Return year as a Hebrew string. 243 | 244 | Parameters 245 | ---------- 246 | thousands: bool, optional 247 | ``True`` to prefix the year with the thousands place. 248 | Default is ``False``. 249 | 250 | Examples 251 | -------- 252 | >>> year = Year(5781) 253 | >>> year.year_string() 254 | תשפ״א 255 | >>> year.year_string(True) 256 | ה׳תשפ״א 257 | """ 258 | return _num_to_str(self.year, thousands) 259 | 260 | 261 | class Month: 262 | """A Month object represents a month of the Hebrew calendar. 263 | 264 | It provides the same operators as a `Year` object. 265 | 266 | Parameters 267 | ---------- 268 | year : int 269 | month : int 270 | The month as an integer starting with 7 for Tishrei through 13 271 | if necessary for Adar Sheni and then 1-6 for Nissan - Elul. 272 | 273 | Attributes 274 | ---------- 275 | year : int 276 | The Hebrew year. 277 | month : int 278 | The month as an integer starting with 7 for Tishrei through 13 279 | if necessary for Adar Sheni and then 1-6 for Nissan - Elul. 280 | """ 281 | 282 | def __init__(self, year, month): 283 | if year < 1: 284 | raise ValueError('Year must be >= 1.') 285 | self.year = year 286 | if month < 1 or month > 12 + utils._is_leap(self.year): 287 | raise IllegalMonthError(month) 288 | self.month = month 289 | 290 | def __repr__(self): 291 | return f'Month({self.year}, {self.month})' 292 | 293 | def __len__(self): 294 | return utils._month_length(self.year, self.month) 295 | 296 | def __iter__(self): 297 | for day in range(1, len(self) + 1): 298 | yield day 299 | 300 | def __eq__(self, other): 301 | if isinstance(other, Month): 302 | return (self.year == other.year and self.month == other.month) 303 | return NotImplemented 304 | 305 | def __add__(self, other): 306 | try: 307 | year, month = utils._add_months(self.year, self.month, other) 308 | return Month(year, month) 309 | except (AttributeError, TypeError): 310 | return NotImplemented 311 | 312 | def __sub__(self, other): 313 | if isinstance(other, Number): 314 | year, month = utils._subtract_months(self.year, self.month, other) 315 | return Month(year, month) 316 | try: 317 | return abs(self._elapsed_months() - other._elapsed_months()) 318 | except AttributeError: 319 | return NotImplemented 320 | 321 | def __gt__(self, other): 322 | if isinstance(other, Month): 323 | return ( 324 | self.year > other.year 325 | or ( 326 | self.year == other.year 327 | and self._month_number() > other._month_number() 328 | ) 329 | ) 330 | return NotImplemented 331 | 332 | def __ge__(self, other): 333 | if isinstance(other, Month): 334 | return self > other or self == other 335 | return NotImplemented 336 | 337 | def __lt__(self, other): 338 | if isinstance(other, Month): 339 | return ( 340 | self.year < other.year 341 | or ( 342 | self.year == other.year 343 | and self._month_number() < other._month_number() 344 | ) 345 | ) 346 | return NotImplemented 347 | 348 | def __le__(self, other): 349 | if isinstance(other, Month): 350 | return self < other or self == other 351 | return NotImplemented 352 | 353 | @classmethod 354 | def from_date(cls, date): 355 | """Return Month object that given date occurs in. 356 | 357 | Parameters 358 | ---------- 359 | date : ~pyluach.dates.BaseDate 360 | Any subclass of ``BaseDate``. 361 | Returns 362 | ------- 363 | Month 364 | The Hebrew month the given date occurs in 365 | """ 366 | heb = date.to_heb() 367 | return Month(heb.year, heb.month) 368 | 369 | @classmethod 370 | def from_pydate(cls, pydate): 371 | """Return Month object from python date object. 372 | 373 | Parameters 374 | ---------- 375 | pydate : datetime.date 376 | A python standard library date object 377 | 378 | Returns 379 | ------- 380 | Month 381 | The Hebrew month the given date occurs in 382 | """ 383 | return cls.from_date(HebrewDate.from_pydate(pydate)) 384 | 385 | def _month_number(self): 386 | """Return month number 1-12 or 13, Tishrei - Elul.""" 387 | return list(Year(self.year)).index(self.month) + 1 388 | 389 | def month_name(self, hebrew=False): 390 | """Return the name of the month. 391 | 392 | Replaces `name` attribute. 393 | 394 | Parameters 395 | ---------- 396 | hebrew : bool, optional 397 | `True` if the month name should be written with Hebrew letters 398 | and False to be transliterated into English using the Ashkenazic 399 | pronunciation. Default is `False`. 400 | 401 | Returns 402 | ------- 403 | str 404 | """ 405 | return utils._month_name(self.year, self.month, hebrew) 406 | 407 | def month_string(self, thousands=False): 408 | """Return month and year in Hebrew. 409 | 410 | Parameters 411 | ---------- 412 | thousands : bool, optional 413 | ``True`` to prefix year with thousands place. 414 | Default is ``False``. 415 | 416 | Returns 417 | ------- 418 | str 419 | The month and year in Hebrew in the form ``f'{month} {year}'``. 420 | """ 421 | return f'{self.month_name(True)} {_num_to_str(self.year, thousands)}' 422 | 423 | def starting_weekday(self): 424 | """Return first weekday of the month. 425 | 426 | Returns 427 | ------- 428 | int 429 | The weekday of the first day of the month starting with Sunday as 1 430 | through Saturday as 7. 431 | """ 432 | return HebrewDate(self.year, self.month, 1).weekday() 433 | 434 | def _elapsed_months(self): 435 | """Return number of months elapsed from beginning of calendar""" 436 | yearmonths = tuple(Year(self.year)) 437 | months_elapsed = ( 438 | utils._elapsed_months(self.year) 439 | + yearmonths.index(self.month) 440 | ) 441 | return months_elapsed 442 | 443 | def iterdates(self): 444 | """Iterate through the Hebrew dates of the month. 445 | 446 | Yields 447 | ------ 448 | :obj:`pyluach.dates.HebrewDate` 449 | The next Hebrew date of the month. 450 | """ 451 | for day in self: 452 | yield HebrewDate(self.year, self.month, day) 453 | 454 | def molad(self): 455 | """Return the month's molad. 456 | 457 | Returns 458 | ------- 459 | dict 460 | A dictionary in the form 461 | ``{weekday: int, hours: int, parts: int}`` 462 | 463 | Note 464 | ----- 465 | This method does not return the molad in the form that is 466 | traditionally announced in the shul. This is the molad in the 467 | form used to calculate the length of the year. 468 | 469 | See Also 470 | -------- 471 | molad_announcement: The molad as it is traditionally announced. 472 | """ 473 | months = self._elapsed_months() 474 | parts = 204 + months*793 475 | hours = 5 + months*12 + parts//1080 476 | days = 2 + months*29 + hours//24 477 | weekday = days % 7 or 7 478 | return {'weekday': weekday, 'hours': hours % 24, 'parts': parts % 1080} 479 | 480 | def molad_announcement(self): 481 | """Return the month's molad in the announcement form. 482 | 483 | Returns a dictionary in the form that the molad is traditionally 484 | announced. The weekday is adjusted to change at midnight and 485 | the hour of the day and minutes are given as traditionally announced. 486 | Note that the hour is given as in a twenty four hour clock ie. 0 for 487 | 12:00 AM through 23 for 11:00 PM. 488 | 489 | Returns 490 | ------- 491 | dict 492 | A dictionary in the form:: 493 | 494 | { 495 | weekday: int, 496 | hour: int, 497 | minutes: int, 498 | parts: int 499 | } 500 | """ 501 | molad = self.molad() 502 | weekday = molad['weekday'] 503 | hour = 18 + molad['hours'] 504 | if hour < 24: 505 | if weekday != 1: 506 | weekday -= 1 507 | else: 508 | weekday = 7 509 | else: 510 | hour -= 24 511 | minutes = molad['parts'] // 18 512 | parts = molad['parts'] % 18 513 | return { 514 | 'weekday': weekday, 'hour': hour, 515 | 'minutes': minutes, 'parts': parts 516 | } 517 | 518 | 519 | def _to_pyweekday(weekday): 520 | if weekday == 1: 521 | return 6 522 | return weekday - 2 523 | 524 | 525 | def _year_and_month(month): 526 | return month.year, month.month 527 | 528 | 529 | def to_hebrew_numeral(num, thousands=False, withgershayim=True): 530 | """Convert int to Hebrew numeral. 531 | 532 | Function useful in formatting Hebrew calendars. 533 | 534 | Parameters 535 | ---------- 536 | num : int 537 | The number to convert 538 | thousands : bool, optional 539 | True if the hebrew returned should include a letter for the 540 | thousands place ie. 'ה׳' for five thousand. Default is ``False``. 541 | withgershayim : bool, optional 542 | ``True`` to include a geresh after a single letter and double 543 | geresh before the last letter if there is more than one letter. 544 | Default is ``True``. 545 | 546 | Returns 547 | ------- 548 | str 549 | The Hebrew numeral representation of the number. 550 | """ 551 | return _num_to_str(num, thousands, withgershayim) 552 | 553 | 554 | class HebrewCalendar(calendar.Calendar): 555 | """Calendar base class. 556 | 557 | This class extends the python library 558 | :py:class:`Calendar ` class for the Hebrew calendar. The 559 | weekdays are 1 for Sunday through 7 for Shabbos. 560 | 561 | Parameters 562 | ---------- 563 | firstweekday : int, optional 564 | The weekday to start each week with. Default is ``1`` for Sunday. 565 | hebrewnumerals : bool, optional 566 | Default is ``True``, which shows the days of the month with Hebrew 567 | numerals. ``False`` shows the days of the month as a decimal number. 568 | hebrewweekdays : bool, optional 569 | ``True`` to show the weekday in Hebrew. Default is ``False``, 570 | which shows the weekday in English. 571 | hebrewmonths : bool, optional 572 | ``True`` to show the month name in Hebrew. Default is ``False``, 573 | which shows the month name transliterated into English. 574 | hebrewyear : bool, optional 575 | ``True`` to show the year in Hebrew numerals. Default is ``False``, 576 | which shows the year as a decimal number. 577 | 578 | Attributes 579 | ---------- 580 | hebrewnumerals : bool 581 | hebrewweekdays : bool 582 | hebrewmonths : bool 583 | hebrewyear : bool 584 | 585 | Note 586 | ---- 587 | All of the parameters other than `firstweekday` are not used in the 588 | ``HebrewCalendar`` base class. They're there for use in child 589 | classes. 590 | """ 591 | 592 | def __init__( 593 | self, firstweekday=1, hebrewnumerals=True, hebrewweekdays=False, 594 | hebrewmonths=False, hebrewyear=False 595 | ): 596 | if not 1 <= firstweekday <= 7: 597 | raise IllegalWeekdayError(firstweekday) 598 | self._firstweekday = firstweekday 599 | self._firstpyweekday = _to_pyweekday(firstweekday) 600 | self.hebrewnumerals = hebrewnumerals 601 | self.hebrewweekdays = hebrewweekdays 602 | self.hebrewmonths = hebrewmonths 603 | self.hebrewyear = hebrewyear 604 | 605 | @property 606 | def firstweekday(self): 607 | """Get and set the weekday the weeks should start with. 608 | 609 | Returns 610 | ------- 611 | int 612 | """ 613 | return self._firstweekday 614 | 615 | @firstweekday.setter 616 | def firstweekday(self, thefirstweekday): 617 | self._firstweekday = thefirstweekday 618 | self._firstpyweekday = _to_pyweekday(thefirstweekday) 619 | 620 | def iterweekdays(self): 621 | """Return one week of weekday numbers. 622 | 623 | The numbers start with the configured first one. 624 | 625 | Yields 626 | ------ 627 | :obj:`int` 628 | The next weekday with 1-7 for Sunday - Shabbos. 629 | The iterator starts with the ``HebrewCalendar`` object's 630 | configured first weekday ie. if configured to start with 631 | Monday it will first yield `2` and end with `1`. 632 | """ 633 | for i in range(self.firstweekday, self.firstweekday + 7): 634 | yield i % 7 or 7 635 | 636 | def itermonthdates(self, year, month): 637 | """Yield dates for one month. 638 | 639 | The iterator will always iterate through complete weeks, so it 640 | will yield dates outside the specified month. 641 | 642 | Parameters 643 | ---------- 644 | year : int 645 | month : int 646 | The Hebrew month starting with 1 for Nissan through 13 for 647 | Adar Sheni if necessary. 648 | 649 | Yields 650 | ------ 651 | :obj:`pyluach.dates.HebrewDate` 652 | The next Hebrew Date of the month starting with the first 653 | date of the week the first of the month falls in, and ending 654 | with the last date of the week that the last day of the month 655 | falls in. 656 | """ 657 | for y, m, d in self.itermonthdays3(year, month): 658 | yield HebrewDate(y, m, d) 659 | 660 | def itermonthdays(self, year, month): 661 | """Like ``itermonthdates()`` but will yield day numbers. 662 | For days outside the specified month the day number is 0. 663 | 664 | Parameters 665 | ---------- 666 | year : int 667 | month : int 668 | 669 | Yields 670 | ------ 671 | :obj:`int` 672 | The day of the month or 0 if the date is before or after the 673 | month. 674 | """ 675 | currmonth = Month(year, month) 676 | day1 = _to_pyweekday(currmonth.starting_weekday()) 677 | ndays = len(currmonth) 678 | days_before = (day1 - self._firstpyweekday) % 7 679 | yield from repeat(0, days_before) 680 | yield from range(1, ndays + 1) 681 | days_after = (self._firstpyweekday - day1 - ndays) % 7 682 | yield from repeat(0, days_after) 683 | 684 | def itermonthdays2(self, year, month): 685 | """Return iterator for the days and weekdays of the month. 686 | 687 | Parameters 688 | ---------- 689 | year : int 690 | month : int 691 | 692 | Yields 693 | ------ 694 | :obj:`tuple` of :obj:`int` 695 | A tuple of ints in the form ``(day of month, weekday)``. 696 | """ 697 | for i, d in enumerate( 698 | self.itermonthdays(year, month), self.firstweekday 699 | ): 700 | yield d, i % 7 or 7 701 | 702 | def itermonthdays3(self, year, month): 703 | """Return iterator for the year, month, and day of the month. 704 | 705 | Parameters 706 | ---------- 707 | year : int 708 | month : int 709 | 710 | Yields 711 | ------ 712 | :obj:`tuple` of :obj:`int` 713 | A tuple of ints in the form ``(year, month, day)``. 714 | """ 715 | currmonth = Month(year, month) 716 | day1 = _to_pyweekday(currmonth.starting_weekday()) 717 | ndays = len(currmonth) 718 | days_before = (day1 - self._firstpyweekday) % 7 719 | days_after = (self._firstpyweekday - day1 - ndays) % 7 720 | try: 721 | prevmonth = currmonth - 1 722 | except ValueError: 723 | prevmonth = currmonth 724 | y, m = _year_and_month(prevmonth) 725 | end = len(prevmonth) + 1 726 | for d in range(end - days_before, end): 727 | yield y, m, d 728 | for d in range(1, ndays + 1): 729 | yield year, month, d 730 | y, m = _year_and_month(currmonth + 1) 731 | for d in range(1, days_after + 1): 732 | yield y, m, d 733 | 734 | def itermonthdays4(self, year, month): 735 | """Return iterator for the year, month, day, and weekday 736 | 737 | Parameters 738 | ---------- 739 | year : int 740 | month : int 741 | 742 | Yields 743 | ------ 744 | :obj:`tuple` of :obj:`int` 745 | A tuple of ints in the form ``(year, month, day, weekday)``. 746 | """ 747 | for i, (y, m, d) in enumerate(self.itermonthdays3(year, month)): 748 | yield y, m, d, (self.firstweekday + i) % 7 or 7 749 | 750 | def yeardatescalendar(self, year, width=3): 751 | """Return data of specified year ready for formatting. 752 | 753 | Parameters 754 | ---------- 755 | year : int 756 | width : int, optional 757 | The number of months per row. Default is 3. 758 | 759 | Returns 760 | ------ 761 | :obj:`list` of list of list of list of :obj:`pyluach.dates.HebrewDate` 762 | Returns a list of month rows. Each month row contains a list 763 | of up to `width` months. Each month contains either 5 or 6 764 | weeks, and each week contains 7 days. Days are ``HebrewDate`` 765 | objects. 766 | """ 767 | months = [ 768 | self.monthdatescalendar(year, m) 769 | for m in Year(year) 770 | ] 771 | return [months[i:i+width] for i in range(0, len(months), width)] 772 | 773 | def yeardays2calendar(self, year, width=3): 774 | """Return the data of the specified year ready for formatting. 775 | 776 | This method is similar to the ``yeardatescalendar`` except the 777 | entries in the week lists are ``(day number, weekday number)`` 778 | tuples. 779 | 780 | Parameters 781 | ---------- 782 | year : int 783 | width : int, optional 784 | The number of months per row. Default is 3. 785 | 786 | Returns 787 | ------- 788 | :obj:`list` of list of list of list of :obj:`tuple` 789 | Returns a list of month rows. Each month row contains a list 790 | of up to `width` months. Each month contains between 4 and 6 791 | weeks, and each week contains 1-7 days. Days are tuples with 792 | the form ``(day number, weekday number)``. 793 | """ 794 | months = [ 795 | self.monthdays2calendar(year, m) 796 | for m in Year(year) 797 | ] 798 | return [months[i:i+width] for i in range(0, len(months), width)] 799 | 800 | def yeardayscalendar(self, year, width=3): 801 | """Return the data of the specified year ready for formatting. 802 | 803 | This method is similar to the ``yeardatescalendar`` except the 804 | entries in the week lists are day numbers. 805 | 806 | Parameters 807 | ---------- 808 | year : int 809 | width : int, optional 810 | The number of months per row. Default is 3. 811 | 812 | Returns 813 | ------- 814 | :obj:`list` of list of list of list of :obj:`int` 815 | Returns a list of month rows. Each month row contains a list 816 | of up to `width` months. Each month contains either 5 or 6 817 | weeks, and each week contains 1-7 days. Each day is the day of 818 | the month as an int. 819 | """ 820 | months = [ 821 | self.monthdayscalendar(year, m) 822 | for m in Year(year) 823 | ] 824 | return [months[i:i+width] for i in range(0, len(months), width)] 825 | 826 | def monthdatescalendar(self, year, month): 827 | """Return matrix (list of lists) of dates for month's calendar. 828 | 829 | Each row represents a week; week entries are HebrewDate instances. 830 | 831 | Parameters 832 | ---------- 833 | year : int 834 | month : int 835 | 836 | Returns 837 | ------- 838 | :obj:`list` of list of :obj:`pyluach.dates.HebrewDate` 839 | List of weeks in the month containing 7 ``HebrewDate`` 840 | instances each. 841 | """ 842 | return super().monthdatescalendar(year, month) 843 | 844 | 845 | class HebrewHTMLCalendar(HebrewCalendar, calendar.HTMLCalendar): 846 | """Class to generate html calendars . 847 | 848 | Adapts :py:class:`calendar.HTMLCalendar` for the Hebrew calendar. 849 | 850 | Parameters 851 | ---------- 852 | firstweekday : int, optional 853 | The weekday to start each week with. Default is ``1`` for Sunday. 854 | hebrewnumerals : bool, optional 855 | Default is ``True``, which shows the days of the month with Hebrew 856 | numerals. ``False`` shows the days of the month as a decimal number. 857 | hebrewweekdays : bool, optional 858 | ``True`` to show the weekday in Hebrew. Default is ``False``, 859 | which shows the weekday in English. 860 | hebrewmonths : bool, optional 861 | ``True`` to show the month name in Hebrew. Default is ``False``, 862 | which shows the month name transliterated into English. 863 | hebrewyear : bool, optional 864 | ``True`` to show the year in Hebrew numerals. Default is ``False``, 865 | which shows the year as a decimal number. 866 | rtl : bool, optional 867 | ``True`` to arrange the months and the days of the month from 868 | right to left. Default is ``False``. 869 | 870 | Attributes 871 | ---------- 872 | hebrewnumerals : bool 873 | hebrewweekdays : bool 874 | hebrewmonths : bool 875 | hebrewyear : bool 876 | rtl : bool 877 | """ 878 | 879 | def __init__( 880 | self, firstweekday=1, hebrewnumerals=True, hebrewweekdays=False, 881 | hebrewmonths=False, hebrewyear=False, rtl=False 882 | ): 883 | self.rtl = rtl 884 | super().__init__( 885 | firstweekday, 886 | hebrewnumerals, 887 | hebrewweekdays, 888 | hebrewmonths, 889 | hebrewyear 890 | ) 891 | 892 | def _rtl_str(self): 893 | if self.rtl: 894 | return ' dir="rtl"' 895 | return '' 896 | 897 | def formatday(self, day, weekday): 898 | """Return a day as an html table cell. 899 | 900 | Parameters 901 | ---------- 902 | day : int 903 | The day of the month or zero for a day outside the month. 904 | weekday : int 905 | The weekday with 1 as Sunday through 7 as Shabbos. 906 | 907 | Returns 908 | ------- 909 | str 910 | """ 911 | pyweekday = _to_pyweekday(weekday) 912 | if day == 0: 913 | return f' ' 914 | if self.hebrewnumerals: 915 | day = to_hebrew_numeral(day, withgershayim=False) 916 | return f'{day}' 917 | 918 | def formatweekday(self, day): 919 | """Return a weekday name as an html table header. 920 | 921 | Parameters 922 | ---------- 923 | day : int 924 | The day of the week 1-7 with Sunday as 1 and Shabbos as 7. 925 | 926 | Returns 927 | ------- 928 | str 929 | """ 930 | pyday = _to_pyweekday(day) 931 | if self.hebrewweekdays: 932 | dayname = utils.WEEKDAYS[day][:3] 933 | else: 934 | dayname = calendar.day_abbr[pyday] 935 | return ( 936 | f'{dayname}' 937 | ) 938 | 939 | def formatyearnumber(self, theyear): 940 | """Return a formatted year. 941 | 942 | Parameters 943 | ---------- 944 | theyear : int 945 | 946 | Returns 947 | ------- 948 | int or str 949 | If ``self.hebrewyear`` is ``True`` return the year as a Hebrew 950 | numeral, else return `theyear` as is. 951 | """ 952 | if self.hebrewyear: 953 | return to_hebrew_numeral(theyear) 954 | return theyear 955 | 956 | def formatmonthname(self, theyear, themonth, withyear=True): 957 | """Return month name as an html table row. 958 | 959 | Parameters 960 | ---------- 961 | theyear : int 962 | themonth : int 963 | The month as an int 1-12 Nissan - Adar and 13 if leap year. 964 | withyear : bool, optional 965 | ``True`` to append the year to the month name. Default is 966 | ``True``. 967 | 968 | Return 969 | ------ 970 | str 971 | """ 972 | s = Month(theyear, themonth).month_name(self.hebrewmonths) 973 | if withyear: 974 | s = f'{s} {self.formatyearnumber(theyear)}' 975 | return ( 976 | f'' 977 | f'{s}' 978 | ) 979 | 980 | def formatmonth(self, theyear, themonth, withyear=True): 981 | """Return a formatted month as an html table. 982 | 983 | Parameters 984 | ---------- 985 | theyear : int 986 | themonth : int 987 | withyear : bool, optional 988 | ``True`` to have the year appended to the month name. Default 989 | is ``True``. 990 | 991 | Returns 992 | ------- 993 | str 994 | """ 995 | v = [] 996 | a = v.append 997 | a( 998 | '' 1000 | ) 1001 | a('\n') 1002 | a(self.formatmonthname(theyear, themonth, withyear=withyear)) 1003 | a('\n') 1004 | a(self.formatweekheader()) 1005 | a('\n') 1006 | for week in self.monthdays2calendar(theyear, themonth): 1007 | a(self.formatweek(week)) 1008 | a('\n') 1009 | a('
') 1010 | a('\n') 1011 | return ''.join(v) 1012 | 1013 | def formatyear(self, theyear, width=3): 1014 | """Return a formatted year as an html table. 1015 | 1016 | Parameters 1017 | ---------- 1018 | theyear : int 1019 | width : int, optional 1020 | The number of months to display per row. Default is 3. 1021 | 1022 | Returns 1023 | ------- 1024 | str 1025 | """ 1026 | year = Year(theyear) 1027 | monthscount = year.monthscount() 1028 | yearmonths = list(year) 1029 | v = [] 1030 | a = v.append 1031 | width = max(width, 1) 1032 | a( 1033 | '' 1035 | ) 1036 | a('\n') 1037 | a( 1038 | f'' 1040 | ) 1041 | for i in range(1, monthscount + 1, width): 1042 | # months in this row 1043 | months = range(i, min(i+width, monthscount + 1)) 1044 | a('') 1045 | for m in months: 1046 | a('') 1051 | a('') 1052 | a('
' 1039 | f'{self.formatyearnumber(theyear)}
') 1047 | a(self.formatmonth( 1048 | theyear, yearmonths[m-1], withyear=False 1049 | )) 1050 | a('
') 1053 | return ''.join(v) 1054 | 1055 | 1056 | class HebrewTextCalendar(HebrewCalendar, calendar.TextCalendar): 1057 | """Subclass of HebrewCalendar that outputs a plaintext calendar. 1058 | 1059 | ``HebrewTextCalendar`` adapts :py:class:`calendar.TextCalendar` for the 1060 | Hebrew calendar. 1061 | 1062 | Parameters 1063 | ---------- 1064 | firstweekday : int, optional 1065 | The weekday to start each week with. Default is ``1`` for Sunday. 1066 | hebrewnumerals : bool, optional 1067 | Default is ``True``, which shows the days of the month with Hebrew 1068 | numerals. ``False`` shows the days of the month as a decimal number. 1069 | hebrewweekdays : bool, optional 1070 | ``True`` to show the weekday in Hebrew. Default is ``False``, 1071 | which shows the weekday in English. 1072 | hebrewmonths : bool, optional 1073 | ``True`` to show the month name in Hebrew. Default is ``False``, 1074 | which shows the month name transliterated into English. 1075 | hebrewyear : bool, optional 1076 | ``True`` to show the year in Hebrew numerals. Default is ``False``, 1077 | which shows the year as a decimal number. 1078 | 1079 | Attributes 1080 | ---------- 1081 | hebrewnumerals : bool 1082 | hebrewweekdays : bool 1083 | hebrewmonths : bool 1084 | hebrewyear : bool 1085 | 1086 | Note 1087 | ---- 1088 | This class generates plain text calendars. Any program that adds 1089 | any formatting may misrender the calendars especially when using any 1090 | Hebrew characters. 1091 | """ 1092 | 1093 | def formatday(self, day, weekday, width): 1094 | """Return a formatted day. 1095 | 1096 | Extends calendar.TextCalendar formatday method. 1097 | 1098 | Parameters 1099 | ---------- 1100 | day : int 1101 | The day of the month. 1102 | weekday : int 1103 | The weekday 1-7 Sunday-Shabbos. 1104 | width : int 1105 | The width of the day column. 1106 | 1107 | Returns 1108 | ------- 1109 | str 1110 | """ 1111 | if self.hebrewnumerals: 1112 | if day == 0: 1113 | s = '' 1114 | else: 1115 | s = f'{to_hebrew_numeral(day, withgershayim=False):>2}' 1116 | return s.center(width) 1117 | return super().formatday(day, weekday, width) 1118 | 1119 | def formatweekday(self, day, width): 1120 | """Return formatted weekday. 1121 | 1122 | Extends calendar.TextCalendar formatweekday method. 1123 | 1124 | Parameters 1125 | ---------- 1126 | day : int 1127 | The weekday 1-7 Sunday-Shabbos. 1128 | width : int 1129 | The width of the day column. 1130 | 1131 | Returns 1132 | ------- 1133 | str 1134 | """ 1135 | if self.hebrewweekdays: 1136 | if width < 5: 1137 | name = to_hebrew_numeral(day) 1138 | else: 1139 | name = utils.WEEKDAYS[day] 1140 | return name[:width].center(width) 1141 | return super().formatweekday(_to_pyweekday(day), width) 1142 | 1143 | def formatmonthname( 1144 | self, theyear, themonth, width=0, withyear=True 1145 | ): 1146 | """Return formatted month name. 1147 | 1148 | Parameters 1149 | ---------- 1150 | theyear : int 1151 | themonth : int 1152 | 1-12 or 13 for Nissan-Adar Sheni 1153 | width : int, optional 1154 | The number of columns per day. Default is 0 1155 | withyear : bool, optional 1156 | Default is ``True`` to include the year with the month name. 1157 | 1158 | Returns 1159 | ------- 1160 | str 1161 | """ 1162 | s = Month(theyear, themonth).month_name(self.hebrewmonths) 1163 | if withyear: 1164 | if self.hebrewyear: 1165 | year = to_hebrew_numeral(theyear) 1166 | else: 1167 | year = theyear 1168 | s = f'{s} {year}' 1169 | return s.center(width) 1170 | 1171 | def formatyear(self, theyear, w=2, l=1, c=6, m=3): # noqa: E741 1172 | """Return a year's calendar as a multi-line string. 1173 | 1174 | Parameters 1175 | ---------- 1176 | theyear : int 1177 | w : int, optional 1178 | The date column width. Default is 2 1179 | l : int, optional 1180 | The number of lines per week. Default is 1. 1181 | c : int, optional 1182 | The number of columns in between each month. Default is 6 1183 | m : int, optional 1184 | The number of months per row. Default is 3. 1185 | 1186 | Returns 1187 | ------- 1188 | str 1189 | """ 1190 | w = max(2, w) 1191 | l = max(1, l) # noqa: E741 1192 | c = max(2, c) 1193 | colwidth = (w + 1) * 7 - 1 1194 | v = [] 1195 | a = v.append 1196 | a(repr(theyear).center(colwidth*m+c*(m-1)).rstrip()) 1197 | a('\n'*l) 1198 | header = self.formatweekheader(w) 1199 | yearmonths = list(Year(theyear)) 1200 | for (i, row) in enumerate(self.yeardays2calendar(theyear, m)): 1201 | # months in this row 1202 | months = range(m*i+1, min(m*(i+1)+1, len(yearmonths)+1)) 1203 | a('\n'*l) 1204 | names = ( 1205 | self.formatmonthname(theyear, yearmonths[k-1], colwidth, False) 1206 | for k in months 1207 | ) 1208 | a(calendar.formatstring(names, colwidth, c).rstrip()) 1209 | a('\n'*l) 1210 | headers = (header for k in months) 1211 | a(calendar.formatstring(headers, colwidth, c).rstrip()) 1212 | a('\n'*l) 1213 | # max number of weeks for this row 1214 | height = max(len(cal) for cal in row) 1215 | for j in range(height): 1216 | weeks = [] 1217 | for cal in row: 1218 | if j >= len(cal): 1219 | weeks.append('') 1220 | else: 1221 | weeks.append(self.formatweek(cal[j], w)) 1222 | a(calendar.formatstring(weeks, colwidth, c).rstrip()) 1223 | a('\n' * l) 1224 | return ''.join(v) 1225 | 1226 | 1227 | def fast_day(date, hebrew=False): 1228 | """Return name of fast day or None. 1229 | 1230 | Parameters 1231 | ---------- 1232 | date : ~pyluach.dates.BaseDate 1233 | Any date instance from a subclass of ``BaseDate`` can be used. 1234 | hebrew : bool, optional 1235 | ``True`` if you want the fast_day name in Hebrew letters. Default 1236 | is ``False``, which returns the name transliterated into English. 1237 | 1238 | Returns 1239 | ------- 1240 | str or None 1241 | The name of the fast day or ``None`` if the given date is not 1242 | a fast day. 1243 | """ 1244 | return date.fast_day(hebrew) 1245 | 1246 | 1247 | def festival( 1248 | date, 1249 | israel=False, 1250 | hebrew=False, 1251 | include_working_days=True, 1252 | prefix_day=False 1253 | ): 1254 | """Return Jewish festival of given day. 1255 | 1256 | This method will return all major and minor religous 1257 | Jewish holidays not including fast days. 1258 | 1259 | Parameters 1260 | ---------- 1261 | date : ~pyluach.dates.BaseDate 1262 | Any subclass of ``BaseDate`` can be used. 1263 | 1264 | israel : bool, optional 1265 | ``True`` if you want the festivals according to the Israel 1266 | schedule. Defaults to ``False``. 1267 | hebrew : bool, optional 1268 | ``True`` if you want the festival name in Hebrew letters. Default 1269 | is ``False``, which returns the name transliterated into English. 1270 | include_working_days : bool, optional 1271 | ``True`` to include festival days on which melacha (work) is 1272 | allowed; ie. Pesach Sheni, Chol Hamoed, etc. 1273 | Default is ``True``. 1274 | prefix_day : bool, optional 1275 | ``True`` to prefix multi day festivals with the day of the 1276 | festival. Default is ``False``. 1277 | 1278 | Returns 1279 | ------- 1280 | str or None 1281 | The name of the festival or ``None`` if the given date is not 1282 | a Jewish festival. 1283 | 1284 | Examples 1285 | -------- 1286 | >>> from pyluach.dates import HebrewDate 1287 | pesach = HebrewDate(2023, 1, 15) 1288 | >>> festival(pesach, prefix_day=True) 1289 | '1 Pesach' 1290 | >>> festival(pesach, hebrew=True, prefix_day=True) 1291 | 'א׳ פסח' 1292 | >>> shavuos = HebrewDate(5783, 3, 6) 1293 | >>> festival(shavuos, israel=True, prefix_day=True) 1294 | 'Shavuos' 1295 | """ 1296 | return date.festival(israel, hebrew, include_working_days, prefix_day) 1297 | 1298 | 1299 | def holiday(date, israel=False, hebrew=False, prefix_day=False): 1300 | """Return Jewish holiday of given date. 1301 | 1302 | The holidays include the major and minor religious Jewish 1303 | holidays including fast days. 1304 | 1305 | Parameters 1306 | ---------- 1307 | date : pyluach.dates.BaseDate 1308 | Any subclass of ``BaseDate`` can be used. 1309 | israel : bool, optional 1310 | ``True`` if you want the holidays according to the israel 1311 | schedule. Default is ``False``. 1312 | hebrew : bool, optional 1313 | ``True`` if you want the holiday name in Hebrew letters. Default 1314 | is ``False``, which returns the name transliterated into English. 1315 | prefix_day : bool, optional 1316 | ``True`` to prefix multi day holidays with the day of the 1317 | holiday. Default is ``False``. 1318 | 1319 | Returns 1320 | ------- 1321 | str or None 1322 | The name of the holiday or ``None`` if the given date is not 1323 | a Jewish holiday. 1324 | 1325 | Examples 1326 | -------- 1327 | >>> from pyluach.dates import HebrewDate 1328 | >>> pesach = HebrewDate(2023, 1, 15) 1329 | >>> holiday(pesach, prefix_day=True) 1330 | '1 Pesach' 1331 | >>> holiday(pesach, hebrew=True, prefix_day=True) 1332 | 'א׳ פסח' 1333 | >>> taanis_esther = HebrewDate(5783, 12, 13) 1334 | >>> holiday(taanis_esther, prefix_day=True) 1335 | 'Taanis Esther' 1336 | """ 1337 | return date.holiday(israel, hebrew, prefix_day) 1338 | --------------------------------------------------------------------------------