hebrewcal 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. hebrewcal-0.1.0/LICENSE +21 -0
  2. hebrewcal-0.1.0/PKG-INFO +145 -0
  3. hebrewcal-0.1.0/README.md +103 -0
  4. hebrewcal-0.1.0/pyproject.toml +104 -0
  5. hebrewcal-0.1.0/setup.cfg +4 -0
  6. hebrewcal-0.1.0/src/hebrewcal/__init__.py +37 -0
  7. hebrewcal-0.1.0/src/hebrewcal/astro/__init__.py +7 -0
  8. hebrewcal-0.1.0/src/hebrewcal/calendars/__init__.py +5 -0
  9. hebrewcal-0.1.0/src/hebrewcal/calendars/gregorian.py +90 -0
  10. hebrewcal-0.1.0/src/hebrewcal/calendars/hebrew.py +65 -0
  11. hebrewcal-0.1.0/src/hebrewcal/calendars/julian.py +95 -0
  12. hebrewcal-0.1.0/src/hebrewcal/calendars_alt/__init__.py +4 -0
  13. hebrewcal-0.1.0/src/hebrewcal/conversion.py +32 -0
  14. hebrewcal-0.1.0/src/hebrewcal/core/__init__.py +6 -0
  15. hebrewcal-0.1.0/src/hebrewcal/core/calendar.py +55 -0
  16. hebrewcal-0.1.0/src/hebrewcal/core/rata_die.py +26 -0
  17. hebrewcal-0.1.0/src/hebrewcal/eras/__init__.py +6 -0
  18. hebrewcal-0.1.0/src/hebrewcal/eras/anno_mundi.py +34 -0
  19. hebrewcal-0.1.0/src/hebrewcal/formatting/__init__.py +1 -0
  20. hebrewcal-0.1.0/src/hebrewcal/formatting/dates.py +31 -0
  21. hebrewcal-0.1.0/src/hebrewcal/hebrew/__init__.py +7 -0
  22. hebrewcal-0.1.0/src/hebrewcal/hebrew/dechiyot.py +26 -0
  23. hebrewcal-0.1.0/src/hebrewcal/hebrew/keviah.py +48 -0
  24. hebrewcal-0.1.0/src/hebrewcal/hebrew/metonic.py +17 -0
  25. hebrewcal-0.1.0/src/hebrewcal/hebrew/molad.py +44 -0
  26. hebrewcal-0.1.0/src/hebrewcal/hebrew/yeartype.py +52 -0
  27. hebrewcal-0.1.0/src/hebrewcal/names.py +71 -0
  28. hebrewcal-0.1.0/src/hebrewcal/numerals.py +114 -0
  29. hebrewcal-0.1.0/src/hebrewcal/parsing/__init__.py +5 -0
  30. hebrewcal-0.1.0/src/hebrewcal/parsing/dates.py +35 -0
  31. hebrewcal-0.1.0/src/hebrewcal/py.typed +0 -0
  32. hebrewcal-0.1.0/src/hebrewcal/religious/__init__.py +6 -0
  33. hebrewcal-0.1.0/src/hebrewcal.egg-info/PKG-INFO +145 -0
  34. hebrewcal-0.1.0/src/hebrewcal.egg-info/SOURCES.txt +40 -0
  35. hebrewcal-0.1.0/src/hebrewcal.egg-info/dependency_links.txt +1 -0
  36. hebrewcal-0.1.0/src/hebrewcal.egg-info/requires.txt +13 -0
  37. hebrewcal-0.1.0/src/hebrewcal.egg-info/top_level.txt +1 -0
  38. hebrewcal-0.1.0/tests/test_conversion.py +48 -0
  39. hebrewcal-0.1.0/tests/test_metadata.py +28 -0
  40. hebrewcal-0.1.0/tests/test_names.py +38 -0
  41. hebrewcal-0.1.0/tests/test_numerals.py +36 -0
  42. hebrewcal-0.1.0/tests/test_reference_dates.py +60 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Benjamin Schnabel
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,145 @@
1
+ Metadata-Version: 2.4
2
+ Name: hebrewcal
3
+ Version: 0.1.0
4
+ Summary: A pure-Python library for the Hebrew calendar: conversion, astronomy, holidays and religious times, computed entirely locally.
5
+ Author-email: Benjamin Schnabel <Benjamin-777@gmx.de>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/bsesic/hebrewcal
8
+ Project-URL: Repository, https://github.com/bsesic/hebrewcal
9
+ Project-URL: Documentation, https://hebrewcal.readthedocs.io
10
+ Project-URL: Issues, https://github.com/bsesic/hebrewcal/issues
11
+ Project-URL: Changelog, https://github.com/bsesic/hebrewcal/blob/main/CHANGELOG.md
12
+ Keywords: hebrew,calendar,jewish,zmanim,molad,gregorian,julian,calendrical
13
+ Classifier: Development Status :: 2 - Pre-Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: Religion
16
+ Classifier: Intended Audience :: Science/Research
17
+ Classifier: License :: OSI Approved :: MIT License
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Religion
24
+ Classifier: Topic :: Scientific/Engineering :: Astronomy
25
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
26
+ Classifier: Typing :: Typed
27
+ Requires-Python: >=3.11
28
+ Description-Content-Type: text/markdown
29
+ License-File: LICENSE
30
+ Provides-Extra: dev
31
+ Requires-Dist: pytest>=8.0; extra == "dev"
32
+ Requires-Dist: pytest-cov>=5.0; extra == "dev"
33
+ Requires-Dist: ruff>=0.6; extra == "dev"
34
+ Requires-Dist: flake8>=7.0; extra == "dev"
35
+ Requires-Dist: mypy>=1.11; extra == "dev"
36
+ Requires-Dist: pre-commit>=3.8; extra == "dev"
37
+ Provides-Extra: docs
38
+ Requires-Dist: sphinx>=7.0; extra == "docs"
39
+ Requires-Dist: furo>=2024.1; extra == "docs"
40
+ Requires-Dist: myst-parser>=3.0; extra == "docs"
41
+ Dynamic: license-file
42
+
43
+ # hebrewcal
44
+
45
+ [![CI](https://github.com/bsesic/hebrewcal/actions/workflows/ci.yml/badge.svg)](https://github.com/bsesic/hebrewcal/actions/workflows/ci.yml)
46
+ [![Documentation Status](https://readthedocs.org/projects/hebrewcal/badge/?version=latest)](https://hebrewcal.readthedocs.io/en/latest/?badge=latest)
47
+ [![PyPI version](https://img.shields.io/pypi/v/hebrewcal.svg)](https://pypi.org/project/hebrewcal/)
48
+ [![Python versions](https://img.shields.io/pypi/pyversions/hebrewcal.svg)](https://pypi.org/project/hebrewcal/)
49
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
50
+
51
+ A pure-Python library for the Hebrew calendar.
52
+
53
+ `hebrewcal` makes the Hebrew calendar usable programmatically and converts it
54
+ bidirectionally against the Gregorian and Julian calendars. Every computation is
55
+ performed locally — the library never issues network calls to any external service.
56
+
57
+ It is built for two audiences:
58
+
59
+ - **Religious use** — holidays (Israel and the Diaspora, including minority feasts and
60
+ Shushan Purim), Shabbat candle lighting and Havdalah, zmanim, Torah readings, the Omer
61
+ count, yahrzeit, and the sabbatical and jubilee cycle.
62
+ - **Academic use** — historical, medieval and ancient dates, Babylonian and biblical
63
+ month names, proleptic calendars, the Julian/Gregorian reform, and the documented
64
+ "missing years" of the Anno Mundi count.
65
+
66
+ > **Project status.** Early development. The calendar core, conversion and date handling
67
+ > (Phase 1) are implemented. Astronomy, holidays and religious times follow on the
68
+ > [roadmap](docs/specs/2026-06-03-architecture-roadmap.md).
69
+
70
+ ## Installation
71
+
72
+ ```bash
73
+ pip install hebrewcal
74
+ ```
75
+
76
+ `hebrewcal` requires Python 3.11+ and has **no runtime dependencies** (standard library
77
+ only).
78
+
79
+ ## Quick start
80
+
81
+ ```python
82
+ from hebrewcal import GregorianDate, HebrewDate, to_hebrew, to_gregorian, weekday
83
+
84
+ # What Hebrew date and weekday corresponds to 31 October 1867?
85
+ g = GregorianDate(1867, 10, 31)
86
+ h = to_hebrew(g)
87
+ print(h) # HebrewDate(year=5628, month=8, day=2) -> 2 Marheshvan 5628
88
+ print(weekday(g).name) # THURSDAY
89
+
90
+ # Convert a Hebrew date back to Gregorian.
91
+ print(to_gregorian(HebrewDate(5785, 7, 1))) # GregorianDate(year=2024, month=10, day=3)
92
+ ```
93
+
94
+ Every calendar reduces a date to an integer **Rata Die (RD)** day count and rebuilds a
95
+ date from it, so any two calendars are interconvertible through RD:
96
+
97
+ ```python
98
+ from hebrewcal import GregorianDate, to_julian
99
+
100
+ GregorianDate(2026, 6, 26).to_rd() # 739793
101
+ to_julian(GregorianDate(2026, 6, 26)) # JulianDate(year=2026, month=6, day=13)
102
+ ```
103
+
104
+ ## Features
105
+
106
+ - Proleptic **Gregorian** and **Julian** calendars, with an explicit, configurable
107
+ Julian/Gregorian reform helper.
108
+ - A complete **Hebrew** calendar: molad and halakim, the dechiyot ("four gates"), year
109
+ typing (deficient / regular / complete), the keviah signature, and the Metonic cycle.
110
+ - Bidirectional **conversion** between any supported calendars through Rata Die.
111
+ - **Parsing** of Gregorian input (ISO 8601, DIN 5008 and slash form) and **formatting**
112
+ in numeric and named styles.
113
+ - A **gematria** converter between integers and Hebrew numerals.
114
+ - Month and weekday **name tables** (transliteration, Babylonian, biblical).
115
+ - The **Anno Mundi** era with a documented "missing years" notice.
116
+
117
+ ## Documentation
118
+
119
+ Full documentation, including a user guide with many examples, lives at
120
+ **[hebrewcal.readthedocs.io](https://hebrewcal.readthedocs.io)**.
121
+
122
+ ## Development
123
+
124
+ ```bash
125
+ git clone https://github.com/bsesic/hebrewcal.git
126
+ cd hebrewcal
127
+ python -m venv .venv && source .venv/bin/activate
128
+ pip install -e ".[dev]"
129
+ pre-commit install
130
+ ```
131
+
132
+ Run the lint gate and the test suite:
133
+
134
+ ```bash
135
+ flake8
136
+ ruff check .
137
+ mypy
138
+ pytest
139
+ ```
140
+
141
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for the branching strategy and conventions.
142
+
143
+ ## License
144
+
145
+ [MIT](LICENSE) © Benjamin Schnabel
@@ -0,0 +1,103 @@
1
+ # hebrewcal
2
+
3
+ [![CI](https://github.com/bsesic/hebrewcal/actions/workflows/ci.yml/badge.svg)](https://github.com/bsesic/hebrewcal/actions/workflows/ci.yml)
4
+ [![Documentation Status](https://readthedocs.org/projects/hebrewcal/badge/?version=latest)](https://hebrewcal.readthedocs.io/en/latest/?badge=latest)
5
+ [![PyPI version](https://img.shields.io/pypi/v/hebrewcal.svg)](https://pypi.org/project/hebrewcal/)
6
+ [![Python versions](https://img.shields.io/pypi/pyversions/hebrewcal.svg)](https://pypi.org/project/hebrewcal/)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
8
+
9
+ A pure-Python library for the Hebrew calendar.
10
+
11
+ `hebrewcal` makes the Hebrew calendar usable programmatically and converts it
12
+ bidirectionally against the Gregorian and Julian calendars. Every computation is
13
+ performed locally — the library never issues network calls to any external service.
14
+
15
+ It is built for two audiences:
16
+
17
+ - **Religious use** — holidays (Israel and the Diaspora, including minority feasts and
18
+ Shushan Purim), Shabbat candle lighting and Havdalah, zmanim, Torah readings, the Omer
19
+ count, yahrzeit, and the sabbatical and jubilee cycle.
20
+ - **Academic use** — historical, medieval and ancient dates, Babylonian and biblical
21
+ month names, proleptic calendars, the Julian/Gregorian reform, and the documented
22
+ "missing years" of the Anno Mundi count.
23
+
24
+ > **Project status.** Early development. The calendar core, conversion and date handling
25
+ > (Phase 1) are implemented. Astronomy, holidays and religious times follow on the
26
+ > [roadmap](docs/specs/2026-06-03-architecture-roadmap.md).
27
+
28
+ ## Installation
29
+
30
+ ```bash
31
+ pip install hebrewcal
32
+ ```
33
+
34
+ `hebrewcal` requires Python 3.11+ and has **no runtime dependencies** (standard library
35
+ only).
36
+
37
+ ## Quick start
38
+
39
+ ```python
40
+ from hebrewcal import GregorianDate, HebrewDate, to_hebrew, to_gregorian, weekday
41
+
42
+ # What Hebrew date and weekday corresponds to 31 October 1867?
43
+ g = GregorianDate(1867, 10, 31)
44
+ h = to_hebrew(g)
45
+ print(h) # HebrewDate(year=5628, month=8, day=2) -> 2 Marheshvan 5628
46
+ print(weekday(g).name) # THURSDAY
47
+
48
+ # Convert a Hebrew date back to Gregorian.
49
+ print(to_gregorian(HebrewDate(5785, 7, 1))) # GregorianDate(year=2024, month=10, day=3)
50
+ ```
51
+
52
+ Every calendar reduces a date to an integer **Rata Die (RD)** day count and rebuilds a
53
+ date from it, so any two calendars are interconvertible through RD:
54
+
55
+ ```python
56
+ from hebrewcal import GregorianDate, to_julian
57
+
58
+ GregorianDate(2026, 6, 26).to_rd() # 739793
59
+ to_julian(GregorianDate(2026, 6, 26)) # JulianDate(year=2026, month=6, day=13)
60
+ ```
61
+
62
+ ## Features
63
+
64
+ - Proleptic **Gregorian** and **Julian** calendars, with an explicit, configurable
65
+ Julian/Gregorian reform helper.
66
+ - A complete **Hebrew** calendar: molad and halakim, the dechiyot ("four gates"), year
67
+ typing (deficient / regular / complete), the keviah signature, and the Metonic cycle.
68
+ - Bidirectional **conversion** between any supported calendars through Rata Die.
69
+ - **Parsing** of Gregorian input (ISO 8601, DIN 5008 and slash form) and **formatting**
70
+ in numeric and named styles.
71
+ - A **gematria** converter between integers and Hebrew numerals.
72
+ - Month and weekday **name tables** (transliteration, Babylonian, biblical).
73
+ - The **Anno Mundi** era with a documented "missing years" notice.
74
+
75
+ ## Documentation
76
+
77
+ Full documentation, including a user guide with many examples, lives at
78
+ **[hebrewcal.readthedocs.io](https://hebrewcal.readthedocs.io)**.
79
+
80
+ ## Development
81
+
82
+ ```bash
83
+ git clone https://github.com/bsesic/hebrewcal.git
84
+ cd hebrewcal
85
+ python -m venv .venv && source .venv/bin/activate
86
+ pip install -e ".[dev]"
87
+ pre-commit install
88
+ ```
89
+
90
+ Run the lint gate and the test suite:
91
+
92
+ ```bash
93
+ flake8
94
+ ruff check .
95
+ mypy
96
+ pytest
97
+ ```
98
+
99
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for the branching strategy and conventions.
100
+
101
+ ## License
102
+
103
+ [MIT](LICENSE) © Benjamin Schnabel
@@ -0,0 +1,104 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "hebrewcal"
7
+ version = "0.1.0"
8
+ description = "A pure-Python library for the Hebrew calendar: conversion, astronomy, holidays and religious times, computed entirely locally."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Benjamin Schnabel", email = "Benjamin-777@gmx.de" }]
13
+ keywords = [
14
+ "hebrew",
15
+ "calendar",
16
+ "jewish",
17
+ "zmanim",
18
+ "molad",
19
+ "gregorian",
20
+ "julian",
21
+ "calendrical",
22
+ ]
23
+ classifiers = [
24
+ "Development Status :: 2 - Pre-Alpha",
25
+ "Intended Audience :: Developers",
26
+ "Intended Audience :: Religion",
27
+ "Intended Audience :: Science/Research",
28
+ "License :: OSI Approved :: MIT License",
29
+ "Operating System :: OS Independent",
30
+ "Programming Language :: Python :: 3",
31
+ "Programming Language :: Python :: 3.11",
32
+ "Programming Language :: Python :: 3.12",
33
+ "Programming Language :: Python :: 3.13",
34
+ "Topic :: Religion",
35
+ "Topic :: Scientific/Engineering :: Astronomy",
36
+ "Topic :: Software Development :: Libraries :: Python Modules",
37
+ "Typing :: Typed",
38
+ ]
39
+ # No runtime dependencies: everything is computed locally using the standard library only.
40
+ dependencies = []
41
+
42
+ [project.urls]
43
+ Homepage = "https://github.com/bsesic/hebrewcal"
44
+ Repository = "https://github.com/bsesic/hebrewcal"
45
+ Documentation = "https://hebrewcal.readthedocs.io"
46
+ Issues = "https://github.com/bsesic/hebrewcal/issues"
47
+ Changelog = "https://github.com/bsesic/hebrewcal/blob/main/CHANGELOG.md"
48
+
49
+ [project.optional-dependencies]
50
+ dev = [
51
+ "pytest>=8.0",
52
+ "pytest-cov>=5.0",
53
+ "ruff>=0.6",
54
+ "flake8>=7.0",
55
+ "mypy>=1.11",
56
+ "pre-commit>=3.8",
57
+ ]
58
+ docs = [
59
+ "sphinx>=7.0",
60
+ "furo>=2024.1",
61
+ "myst-parser>=3.0",
62
+ ]
63
+
64
+ [tool.setuptools]
65
+ package-dir = { "" = "src" }
66
+
67
+ [tool.setuptools.packages.find]
68
+ where = ["src"]
69
+
70
+ [tool.setuptools.package-data]
71
+ hebrewcal = ["py.typed"]
72
+
73
+ [tool.pytest.ini_options]
74
+ minversion = "8.0"
75
+ testpaths = ["tests"]
76
+ addopts = "-ra --strict-markers"
77
+
78
+ [tool.coverage.run]
79
+ branch = true
80
+ source = ["hebrewcal"]
81
+
82
+ [tool.coverage.report]
83
+ show_missing = true
84
+
85
+ [tool.ruff]
86
+ line-length = 100
87
+ target-version = "py311"
88
+ src = ["src", "tests"]
89
+
90
+ [tool.ruff.lint]
91
+ select = ["E", "F", "W", "I", "N", "UP", "B", "C4", "SIM", "RUF"]
92
+ # This library deliberately embeds Hebrew text in strings, comments and
93
+ # docstrings. The ambiguous-unicode checks (homoglyph detection for Latin code)
94
+ # are false positives in that context, so they are disabled project-wide.
95
+ ignore = ["RUF001", "RUF002", "RUF003"]
96
+
97
+ [tool.ruff.lint.isort]
98
+ known-first-party = ["hebrewcal"]
99
+
100
+ [tool.mypy]
101
+ python_version = "3.11"
102
+ strict = true
103
+ warn_unreachable = true
104
+ files = ["src", "tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,37 @@
1
+ """hebrewcal — a pure-Python library for the Hebrew calendar.
2
+
3
+ The library makes the Hebrew calendar usable programmatically and converts it
4
+ bidirectionally against the Gregorian and Julian calendars. Every computation is
5
+ performed locally; the library never issues network calls to any external service.
6
+
7
+ The whole design pivots on the Rata Die (RD) day count from Dershowitz & Reingold,
8
+ *Calendrical Calculations*: every calendar implements only ``to_rd`` and ``from_rd``,
9
+ and conversion between any two calendars always goes through RD.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from importlib import metadata
15
+
16
+ from hebrewcal.calendars.gregorian import GregorianDate
17
+ from hebrewcal.calendars.hebrew import HebrewDate
18
+ from hebrewcal.calendars.julian import JulianDate
19
+ from hebrewcal.conversion import to_gregorian, to_hebrew, to_julian, weekday
20
+ from hebrewcal.core.calendar import Weekday
21
+
22
+ try:
23
+ __version__ = metadata.version("hebrewcal")
24
+ except metadata.PackageNotFoundError: # pragma: no cover - source checkout without install
25
+ __version__ = "0.0.0.dev0"
26
+
27
+ __all__ = [
28
+ "GregorianDate",
29
+ "HebrewDate",
30
+ "JulianDate",
31
+ "Weekday",
32
+ "__version__",
33
+ "to_gregorian",
34
+ "to_hebrew",
35
+ "to_julian",
36
+ "weekday",
37
+ ]
@@ -0,0 +1,7 @@
1
+ """Astronomy and locations — pure Python, no dependencies.
2
+
3
+ Implements solar position, sunrise/sunset and twilight (Meeus / NOAA algorithms),
4
+ a geographic ``Location`` type, and time-zone handling via the standard-library
5
+ ``zoneinfo`` module. This is the foundation for all time-of-day religious
6
+ calculations.
7
+ """
@@ -0,0 +1,5 @@
1
+ """Concrete calendars: Gregorian, Julian and Hebrew.
2
+
3
+ Each calendar converts to and from the Rata Die day count, which makes every
4
+ calendar interconvertible without any pairwise conversion code.
5
+ """
@@ -0,0 +1,90 @@
1
+ """The proleptic Gregorian calendar.
2
+
3
+ Valid for all years, including years <= 0 (proleptic). The RD value matches the
4
+ Python standard-library proleptic Gregorian ordinal, which makes cross-checking
5
+ straightforward.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+
12
+ from hebrewcal.core.rata_die import RD_EPOCH
13
+
14
+ _MONTH_LENGTHS = (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
15
+
16
+
17
+ def is_leap_year(year: int) -> bool:
18
+ """Return whether ``year`` is a Gregorian leap year."""
19
+ return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
20
+
21
+
22
+ def last_day_of_month(year: int, month: int) -> int:
23
+ """Return the number of days in ``month`` of ``year``."""
24
+ if month == 2 and is_leap_year(year):
25
+ return 29
26
+ return _MONTH_LENGTHS[month - 1]
27
+
28
+
29
+ @dataclass(frozen=True, order=True)
30
+ class GregorianDate:
31
+ """A date in the proleptic Gregorian calendar."""
32
+
33
+ year: int
34
+ month: int
35
+ day: int
36
+
37
+ def __post_init__(self) -> None:
38
+ if not 1 <= self.month <= 12:
39
+ raise ValueError(f"month out of range: {self.month}")
40
+ if not 1 <= self.day <= last_day_of_month(self.year, self.month):
41
+ raise ValueError(f"day out of range: {self.day}")
42
+
43
+ def to_rd(self) -> int:
44
+ """Return the Rata Die day count for this date."""
45
+ y = self.year
46
+ if self.month <= 2:
47
+ correction = 0
48
+ elif is_leap_year(y):
49
+ correction = -1
50
+ else:
51
+ correction = -2
52
+ return (
53
+ RD_EPOCH
54
+ - 1
55
+ + 365 * (y - 1)
56
+ + (y - 1) // 4
57
+ - (y - 1) // 100
58
+ + (y - 1) // 400
59
+ + (367 * self.month - 362) // 12
60
+ + correction
61
+ + self.day
62
+ )
63
+
64
+ @classmethod
65
+ def from_rd(cls, rd: int) -> GregorianDate:
66
+ """Reconstruct a Gregorian date from an RD value."""
67
+ year = _year_from_rd(rd)
68
+ prior_days = rd - cls(year, 1, 1).to_rd()
69
+ if rd < cls(year, 3, 1).to_rd():
70
+ correction = 0
71
+ elif is_leap_year(year):
72
+ correction = 1
73
+ else:
74
+ correction = 2
75
+ month = (12 * (prior_days + correction) + 373) // 367
76
+ day = rd - cls(year, month, 1).to_rd() + 1
77
+ return cls(year, month, day)
78
+
79
+
80
+ def _year_from_rd(rd: int) -> int:
81
+ """Return the Gregorian year containing the given RD value."""
82
+ d0 = rd - RD_EPOCH
83
+ n400, d1 = divmod(d0, 146097)
84
+ n100, d2 = divmod(d1, 36524)
85
+ n4, d3 = divmod(d2, 1461)
86
+ n1 = d3 // 365
87
+ year = 400 * n400 + 100 * n100 + 4 * n4 + n1
88
+ if n100 == 4 or n1 == 4:
89
+ return year
90
+ return year + 1
@@ -0,0 +1,65 @@
1
+ """The Hebrew calendar date type.
2
+
3
+ Month numbering is standard (Nisan = 1 ... Tishri = 7 ... Adar/Adar I = 12,
4
+ Adar II = 13). The civil year begins at Tishri. Conversion to and from RD uses
5
+ the year-typing machinery in :mod:`hebrewcal.hebrew`.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+
12
+ from hebrewcal.hebrew.yeartype import (
13
+ last_day_of_month,
14
+ last_month_of_year,
15
+ new_year_rd,
16
+ )
17
+
18
+ _TISHRI = 7
19
+
20
+
21
+ @dataclass(frozen=True, order=True)
22
+ class HebrewDate:
23
+ """A date in the Hebrew calendar."""
24
+
25
+ year: int
26
+ month: int
27
+ day: int
28
+
29
+ def __post_init__(self) -> None:
30
+ if not 1 <= self.month <= last_month_of_year(self.year):
31
+ raise ValueError(f"month out of range: {self.month}")
32
+ if not 1 <= self.day <= last_day_of_month(self.year, self.month):
33
+ raise ValueError(f"day out of range: {self.day}")
34
+
35
+ def to_rd(self) -> int:
36
+ """Return the Rata Die day count for this date."""
37
+ if self.month < _TISHRI:
38
+ # Months Nisan..Elul fall in the second half of the civil year.
39
+ months_after_tishri = range(_TISHRI, last_month_of_year(self.year) + 1)
40
+ months_before = range(1, self.month)
41
+ else:
42
+ months_after_tishri = range(_TISHRI, self.month)
43
+ months_before = range(0, 0) # empty
44
+ days_before = sum(
45
+ last_day_of_month(self.year, m) for m in months_after_tishri
46
+ ) + sum(last_day_of_month(self.year, m) for m in months_before)
47
+ return new_year_rd(self.year) + days_before + self.day - 1
48
+
49
+ @classmethod
50
+ def from_rd(cls, rd: int) -> HebrewDate:
51
+ """Reconstruct a Hebrew date from an RD value."""
52
+ # Estimate the year, then correct by direct comparison.
53
+ approx = (rd - new_year_rd(1)) // 366 + 1
54
+ year = approx
55
+ while new_year_rd(year + 1) <= rd:
56
+ year += 1
57
+ while new_year_rd(year) > rd:
58
+ year -= 1
59
+ # Determine the starting month: Nisan (1) if on/after 1 Nisan, else Tishri (7).
60
+ start = 1 if rd >= cls(year, 1, 1).to_rd() else _TISHRI
61
+ month = start
62
+ while rd > cls(year, month, last_day_of_month(year, month)).to_rd():
63
+ month += 1
64
+ day = rd - cls(year, month, 1).to_rd() + 1
65
+ return cls(year, month, day)
@@ -0,0 +1,95 @@
1
+ """The proleptic Julian calendar with explicit reform handling.
2
+
3
+ The library never performs a silent Julian/Gregorian switch. Everything is
4
+ computed through RD; the historical 1582 (and later, regional) reform is exposed
5
+ as an explicit, configurable helper so callers decide when the cutover applies.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+
12
+ from hebrewcal.calendars.gregorian import GregorianDate
13
+
14
+ # RD of Julian 1 January 1 == Gregorian 30 December 0 == -1.
15
+ JULIAN_EPOCH: int = -1
16
+
17
+ _MONTH_LENGTHS = (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
18
+
19
+
20
+ def is_leap_year(year: int) -> bool:
21
+ """Return whether ``year`` is a Julian leap year (proleptic, no year 0)."""
22
+ if year > 0:
23
+ return year % 4 == 0
24
+ # There is no year 0; proleptically, leap years satisfy year % 4 == 3.
25
+ return year % 4 == 3
26
+
27
+
28
+ def last_day_of_month(year: int, month: int) -> int:
29
+ """Return the number of days in ``month`` of ``year``."""
30
+ if month == 2 and is_leap_year(year):
31
+ return 29
32
+ return _MONTH_LENGTHS[month - 1]
33
+
34
+
35
+ @dataclass(frozen=True, order=True)
36
+ class JulianDate:
37
+ """A date in the proleptic Julian calendar."""
38
+
39
+ year: int
40
+ month: int
41
+ day: int
42
+
43
+ def __post_init__(self) -> None:
44
+ if self.year == 0:
45
+ raise ValueError("Julian calendar has no year 0")
46
+ if not 1 <= self.month <= 12:
47
+ raise ValueError(f"month out of range: {self.month}")
48
+ if not 1 <= self.day <= last_day_of_month(self.year, self.month):
49
+ raise ValueError(f"day out of range: {self.day}")
50
+
51
+ def to_rd(self) -> int:
52
+ """Return the Rata Die day count for this date."""
53
+ y = self.year + 1 if self.year < 0 else self.year
54
+ if self.month <= 2:
55
+ correction = 0
56
+ elif is_leap_year(self.year):
57
+ correction = -1
58
+ else:
59
+ correction = -2
60
+ return (
61
+ JULIAN_EPOCH
62
+ - 1
63
+ + 365 * (y - 1)
64
+ + (y - 1) // 4
65
+ + (367 * self.month - 362) // 12
66
+ + correction
67
+ + self.day
68
+ )
69
+
70
+ @classmethod
71
+ def from_rd(cls, rd: int) -> JulianDate:
72
+ """Reconstruct a Julian date from an RD value."""
73
+ approx = (4 * (rd - JULIAN_EPOCH) + 1464) // 1461
74
+ year = approx - 1 if approx <= 0 else approx
75
+ prior_days = rd - cls(year, 1, 1).to_rd()
76
+ if rd < cls(year, 3, 1).to_rd():
77
+ correction = 0
78
+ elif is_leap_year(year):
79
+ correction = 1
80
+ else:
81
+ correction = 2
82
+ month = (12 * (prior_days + correction) + 373) // 367
83
+ day = rd - cls(year, month, 1).to_rd() + 1
84
+ return cls(year, month, day)
85
+
86
+
87
+ def last_gregorian_before_reform() -> GregorianDate:
88
+ """Return the last Gregorian-labelled date before the 1582 papal cutover.
89
+
90
+ The papal bull skipped from Julian Thursday 4 October 1582 to Gregorian
91
+ Friday 15 October 1582. Adoption was regional and much later in many places;
92
+ callers that need a different cutover should supply their own date. This
93
+ helper exists so the default reference point is explicit, not implicit.
94
+ """
95
+ return GregorianDate(1582, 10, 4)