GaianCalendar 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.
@@ -0,0 +1,136 @@
1
+ Metadata-Version: 2.4
2
+ Name: GaianCalendar
3
+ Version: 0.1.0
4
+ Summary: Python library for the Gaian Calendar — a perpetual 13-month solar calendar based on ISO week-year arithmetic.
5
+ Author: Immanuelle
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Emma-Leonhart/GaianCalendar
8
+ Project-URL: Issues, https://github.com/Emma-Leonhart/GaianCalendar/issues
9
+ Keywords: calendar,gaian,date,alternative calendar,perpetual calendar,ISO week,zodiac
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Software Development :: Libraries
20
+ Classifier: Topic :: Utilities
21
+ Requires-Python: >=3.9
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Dynamic: license-file
25
+
26
+ # GaianCalendar
27
+
28
+ A pure-Python library for the **Gaian Calendar** — a perpetual 13-month solar calendar based on ISO week-year arithmetic.
29
+
30
+ ```python
31
+ from gaian_calendar import GaianDate
32
+
33
+ today = GaianDate.today()
34
+ print(today) # e.g. "Aquarius 22, 12026 GE"
35
+ print(today.day_of_year) # e.g. 78
36
+ print(today.is_leap_year) # True
37
+ print(today.format("WWWW, MMMM d, yyyy GE")) # "Sunday, Aquarius 22, 12026 GE"
38
+ ```
39
+
40
+ ---
41
+
42
+ ## Status
43
+
44
+ **Pre-release — planning phase.** See [`planning/`](planning/) for design documents.
45
+
46
+ ---
47
+
48
+ ## What is the Gaian Calendar?
49
+
50
+ The Gaian Calendar is a perpetual reform calendar with these properties:
51
+
52
+ - **13 regular months** of exactly 28 days (4 weeks × 7 days), named after zodiac constellations
53
+ - **1 intercalary month** (Horus, 7 days) in leap years only — years with ISO week 53
54
+ - **Perpetual**: every calendar date always falls on the same weekday, every year
55
+ - **Year numbering**: Gaian year = ISO week-year + 10,000 (so 2026 CE = 12026 GE)
56
+ - **Zero weekday drift**: recurring events stay on the same day of week indefinitely
57
+
58
+ The 13 months in order: Sagittarius · Capricorn · Aquarius · Pisces · Aries · Taurus · Gemini · Cancer · Leo · Virgo · Libra · Scorpius · Ophiuchus · (Horus in leap years)
59
+
60
+ ---
61
+
62
+ ## Installation
63
+
64
+ ```bash
65
+ pip install gaian-calendar
66
+ ```
67
+
68
+ *(Not yet published — coming soon)*
69
+
70
+ ---
71
+
72
+ ## Usage
73
+
74
+ ```python
75
+ from datetime import date, timedelta
76
+ from gaian_calendar import GaianDate, GaianMonth, is_leap_year
77
+
78
+ # Today
79
+ d = GaianDate.today()
80
+
81
+ # From Gregorian
82
+ d = GaianDate.from_gregorian(date(2026, 2, 22))
83
+ print(d) # "Aquarius 22, 12026 GE"
84
+ print(d.to_gregorian()) # 2026-02-22
85
+
86
+ # Properties
87
+ print(d.year) # 12026
88
+ print(d.month) # 3
89
+ print(d.day) # 22
90
+ print(d.month_name) # "Aquarius"
91
+ print(d.month_symbol) # "♒"
92
+ print(d.weekday_name) # "Sunday"
93
+ print(d.weekday_symbol) # "☉"
94
+ print(d.day_of_year) # 78
95
+ print(d.is_leap_year) # True
96
+
97
+ # Arithmetic
98
+ next_week = d + timedelta(weeks=1)
99
+ yesterday = d - timedelta(days=1)
100
+ delta = d - GaianDate(12026, 1, 1) # timedelta
101
+
102
+ # Parsing
103
+ d = GaianDate.parse("Aquarius 22, 12026")
104
+ d = GaianDate.parse("12026-03-22")
105
+ d = GaianDate.parse("3/22/12026")
106
+
107
+ # Formatting
108
+ d.format("MMMM d, yyyy GE") # "Aquarius 22, 12026 GE"
109
+ d.format("MMM* DDD") # "♒ 078"
110
+ d.format("yyyy-MM-dd") # "12026-03-22"
111
+ d.format("ddd") # "22nd"
112
+
113
+ # Leap year check
114
+ is_leap_year(12026) # True
115
+ is_leap_year(12025) # False
116
+
117
+ # Month info
118
+ from gaian_calendar import GaianMonth
119
+ m = GaianMonth.AQUARIUS
120
+ print(m.symbol) # "♒"
121
+ print(m.element) # "Air"
122
+ ```
123
+
124
+ ---
125
+
126
+ ## Similar Libraries
127
+
128
+ This library is modeled after:
129
+ - [`hijridate`](https://pypi.org/project/hijridate/) — Islamic Hijri calendar
130
+ - [`pyluach`](https://pypi.org/project/pyluach/) — Hebrew calendar
131
+
132
+ ---
133
+
134
+ ## License
135
+
136
+ MIT
@@ -0,0 +1,18 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ GaianCalendar.egg-info/PKG-INFO
5
+ GaianCalendar.egg-info/SOURCES.txt
6
+ GaianCalendar.egg-info/dependency_links.txt
7
+ GaianCalendar.egg-info/top_level.txt
8
+ gaian_calendar/__init__.py
9
+ gaian_calendar/_convert.py
10
+ gaian_calendar/_data.py
11
+ gaian_calendar/_format.py
12
+ gaian_calendar/date.py
13
+ gaian_calendar/month.py
14
+ gaian_calendar/weekday.py
15
+ tests/test_convert.py
16
+ tests/test_date.py
17
+ tests/test_format.py
18
+ tests/test_month_weekday.py
@@ -0,0 +1 @@
1
+ gaian_calendar
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Immanuelle
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,136 @@
1
+ Metadata-Version: 2.4
2
+ Name: GaianCalendar
3
+ Version: 0.1.0
4
+ Summary: Python library for the Gaian Calendar — a perpetual 13-month solar calendar based on ISO week-year arithmetic.
5
+ Author: Immanuelle
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Emma-Leonhart/GaianCalendar
8
+ Project-URL: Issues, https://github.com/Emma-Leonhart/GaianCalendar/issues
9
+ Keywords: calendar,gaian,date,alternative calendar,perpetual calendar,ISO week,zodiac
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Software Development :: Libraries
20
+ Classifier: Topic :: Utilities
21
+ Requires-Python: >=3.9
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Dynamic: license-file
25
+
26
+ # GaianCalendar
27
+
28
+ A pure-Python library for the **Gaian Calendar** — a perpetual 13-month solar calendar based on ISO week-year arithmetic.
29
+
30
+ ```python
31
+ from gaian_calendar import GaianDate
32
+
33
+ today = GaianDate.today()
34
+ print(today) # e.g. "Aquarius 22, 12026 GE"
35
+ print(today.day_of_year) # e.g. 78
36
+ print(today.is_leap_year) # True
37
+ print(today.format("WWWW, MMMM d, yyyy GE")) # "Sunday, Aquarius 22, 12026 GE"
38
+ ```
39
+
40
+ ---
41
+
42
+ ## Status
43
+
44
+ **Pre-release — planning phase.** See [`planning/`](planning/) for design documents.
45
+
46
+ ---
47
+
48
+ ## What is the Gaian Calendar?
49
+
50
+ The Gaian Calendar is a perpetual reform calendar with these properties:
51
+
52
+ - **13 regular months** of exactly 28 days (4 weeks × 7 days), named after zodiac constellations
53
+ - **1 intercalary month** (Horus, 7 days) in leap years only — years with ISO week 53
54
+ - **Perpetual**: every calendar date always falls on the same weekday, every year
55
+ - **Year numbering**: Gaian year = ISO week-year + 10,000 (so 2026 CE = 12026 GE)
56
+ - **Zero weekday drift**: recurring events stay on the same day of week indefinitely
57
+
58
+ The 13 months in order: Sagittarius · Capricorn · Aquarius · Pisces · Aries · Taurus · Gemini · Cancer · Leo · Virgo · Libra · Scorpius · Ophiuchus · (Horus in leap years)
59
+
60
+ ---
61
+
62
+ ## Installation
63
+
64
+ ```bash
65
+ pip install gaian-calendar
66
+ ```
67
+
68
+ *(Not yet published — coming soon)*
69
+
70
+ ---
71
+
72
+ ## Usage
73
+
74
+ ```python
75
+ from datetime import date, timedelta
76
+ from gaian_calendar import GaianDate, GaianMonth, is_leap_year
77
+
78
+ # Today
79
+ d = GaianDate.today()
80
+
81
+ # From Gregorian
82
+ d = GaianDate.from_gregorian(date(2026, 2, 22))
83
+ print(d) # "Aquarius 22, 12026 GE"
84
+ print(d.to_gregorian()) # 2026-02-22
85
+
86
+ # Properties
87
+ print(d.year) # 12026
88
+ print(d.month) # 3
89
+ print(d.day) # 22
90
+ print(d.month_name) # "Aquarius"
91
+ print(d.month_symbol) # "♒"
92
+ print(d.weekday_name) # "Sunday"
93
+ print(d.weekday_symbol) # "☉"
94
+ print(d.day_of_year) # 78
95
+ print(d.is_leap_year) # True
96
+
97
+ # Arithmetic
98
+ next_week = d + timedelta(weeks=1)
99
+ yesterday = d - timedelta(days=1)
100
+ delta = d - GaianDate(12026, 1, 1) # timedelta
101
+
102
+ # Parsing
103
+ d = GaianDate.parse("Aquarius 22, 12026")
104
+ d = GaianDate.parse("12026-03-22")
105
+ d = GaianDate.parse("3/22/12026")
106
+
107
+ # Formatting
108
+ d.format("MMMM d, yyyy GE") # "Aquarius 22, 12026 GE"
109
+ d.format("MMM* DDD") # "♒ 078"
110
+ d.format("yyyy-MM-dd") # "12026-03-22"
111
+ d.format("ddd") # "22nd"
112
+
113
+ # Leap year check
114
+ is_leap_year(12026) # True
115
+ is_leap_year(12025) # False
116
+
117
+ # Month info
118
+ from gaian_calendar import GaianMonth
119
+ m = GaianMonth.AQUARIUS
120
+ print(m.symbol) # "♒"
121
+ print(m.element) # "Air"
122
+ ```
123
+
124
+ ---
125
+
126
+ ## Similar Libraries
127
+
128
+ This library is modeled after:
129
+ - [`hijridate`](https://pypi.org/project/hijridate/) — Islamic Hijri calendar
130
+ - [`pyluach`](https://pypi.org/project/pyluach/) — Hebrew calendar
131
+
132
+ ---
133
+
134
+ ## License
135
+
136
+ MIT
@@ -0,0 +1,111 @@
1
+ # GaianCalendar
2
+
3
+ A pure-Python library for the **Gaian Calendar** — a perpetual 13-month solar calendar based on ISO week-year arithmetic.
4
+
5
+ ```python
6
+ from gaian_calendar import GaianDate
7
+
8
+ today = GaianDate.today()
9
+ print(today) # e.g. "Aquarius 22, 12026 GE"
10
+ print(today.day_of_year) # e.g. 78
11
+ print(today.is_leap_year) # True
12
+ print(today.format("WWWW, MMMM d, yyyy GE")) # "Sunday, Aquarius 22, 12026 GE"
13
+ ```
14
+
15
+ ---
16
+
17
+ ## Status
18
+
19
+ **Pre-release — planning phase.** See [`planning/`](planning/) for design documents.
20
+
21
+ ---
22
+
23
+ ## What is the Gaian Calendar?
24
+
25
+ The Gaian Calendar is a perpetual reform calendar with these properties:
26
+
27
+ - **13 regular months** of exactly 28 days (4 weeks × 7 days), named after zodiac constellations
28
+ - **1 intercalary month** (Horus, 7 days) in leap years only — years with ISO week 53
29
+ - **Perpetual**: every calendar date always falls on the same weekday, every year
30
+ - **Year numbering**: Gaian year = ISO week-year + 10,000 (so 2026 CE = 12026 GE)
31
+ - **Zero weekday drift**: recurring events stay on the same day of week indefinitely
32
+
33
+ The 13 months in order: Sagittarius · Capricorn · Aquarius · Pisces · Aries · Taurus · Gemini · Cancer · Leo · Virgo · Libra · Scorpius · Ophiuchus · (Horus in leap years)
34
+
35
+ ---
36
+
37
+ ## Installation
38
+
39
+ ```bash
40
+ pip install gaian-calendar
41
+ ```
42
+
43
+ *(Not yet published — coming soon)*
44
+
45
+ ---
46
+
47
+ ## Usage
48
+
49
+ ```python
50
+ from datetime import date, timedelta
51
+ from gaian_calendar import GaianDate, GaianMonth, is_leap_year
52
+
53
+ # Today
54
+ d = GaianDate.today()
55
+
56
+ # From Gregorian
57
+ d = GaianDate.from_gregorian(date(2026, 2, 22))
58
+ print(d) # "Aquarius 22, 12026 GE"
59
+ print(d.to_gregorian()) # 2026-02-22
60
+
61
+ # Properties
62
+ print(d.year) # 12026
63
+ print(d.month) # 3
64
+ print(d.day) # 22
65
+ print(d.month_name) # "Aquarius"
66
+ print(d.month_symbol) # "♒"
67
+ print(d.weekday_name) # "Sunday"
68
+ print(d.weekday_symbol) # "☉"
69
+ print(d.day_of_year) # 78
70
+ print(d.is_leap_year) # True
71
+
72
+ # Arithmetic
73
+ next_week = d + timedelta(weeks=1)
74
+ yesterday = d - timedelta(days=1)
75
+ delta = d - GaianDate(12026, 1, 1) # timedelta
76
+
77
+ # Parsing
78
+ d = GaianDate.parse("Aquarius 22, 12026")
79
+ d = GaianDate.parse("12026-03-22")
80
+ d = GaianDate.parse("3/22/12026")
81
+
82
+ # Formatting
83
+ d.format("MMMM d, yyyy GE") # "Aquarius 22, 12026 GE"
84
+ d.format("MMM* DDD") # "♒ 078"
85
+ d.format("yyyy-MM-dd") # "12026-03-22"
86
+ d.format("ddd") # "22nd"
87
+
88
+ # Leap year check
89
+ is_leap_year(12026) # True
90
+ is_leap_year(12025) # False
91
+
92
+ # Month info
93
+ from gaian_calendar import GaianMonth
94
+ m = GaianMonth.AQUARIUS
95
+ print(m.symbol) # "♒"
96
+ print(m.element) # "Air"
97
+ ```
98
+
99
+ ---
100
+
101
+ ## Similar Libraries
102
+
103
+ This library is modeled after:
104
+ - [`hijridate`](https://pypi.org/project/hijridate/) — Islamic Hijri calendar
105
+ - [`pyluach`](https://pypi.org/project/pyluach/) — Hebrew calendar
106
+
107
+ ---
108
+
109
+ ## License
110
+
111
+ MIT
@@ -0,0 +1,21 @@
1
+ """
2
+ gaian_calendar — Python library for the Gaian Calendar.
3
+
4
+ A perpetual 13-month solar calendar based on ISO week-year arithmetic.
5
+ Gaian year = ISO week-year + 10,000.
6
+ """
7
+
8
+ __version__ = "0.1.0"
9
+
10
+ from .date import GaianDate
11
+ from .month import GaianMonth
12
+ from .weekday import GaianWeekday
13
+ from ._convert import is_leap_year
14
+
15
+ __all__ = [
16
+ "GaianDate",
17
+ "GaianMonth",
18
+ "GaianWeekday",
19
+ "is_leap_year",
20
+ "__version__",
21
+ ]
@@ -0,0 +1,83 @@
1
+ """
2
+ Core Gaian Calendar arithmetic: conversions, validation, derived properties.
3
+ All functions work with plain ints and datetime.date — no class dependencies.
4
+ """
5
+ from datetime import date
6
+
7
+
8
+ # ---------------------------------------------------------------------------
9
+ # Leap year
10
+ # ---------------------------------------------------------------------------
11
+
12
+ def _iso_weeks_in_year(iso_year: int) -> int:
13
+ """Return 52 or 53: the number of ISO weeks in the given ISO week-year."""
14
+ # Dec 28 is always in the last real ISO week of the year (never week 1 of next)
15
+ return date(iso_year, 12, 28).isocalendar()[1]
16
+
17
+
18
+ def is_leap_year(gaian_year: int) -> bool:
19
+ """Return True if the Gaian year has a Horus month (53 ISO weeks)."""
20
+ return _iso_weeks_in_year(gaian_year - 10_000) == 53
21
+
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # Validation
25
+ # ---------------------------------------------------------------------------
26
+
27
+ def validate_date(year: int, month: int, day: int) -> None:
28
+ """Raise ValueError if (year, month, day) is not a valid Gaian date."""
29
+ if year < 10_001 or year > 19_999:
30
+ raise ValueError(f"Year {year} out of supported range (10001–19999)")
31
+ leap = is_leap_year(year)
32
+ max_month = 14 if leap else 13
33
+ if month < 1 or month > max_month:
34
+ if month == 14 and not leap:
35
+ raise ValueError(
36
+ f"Month 14 (Horus) only exists in leap years; {year} is not a leap year"
37
+ )
38
+ raise ValueError(f"Month {month} out of range (1–{max_month}) for year {year}")
39
+ max_day = 7 if month == 14 else 28
40
+ if day < 1 or day > max_day:
41
+ raise ValueError(
42
+ f"Day {day} out of range for month {month} "
43
+ f"({'Horus, max 7' if month == 14 else 'max 28'})"
44
+ )
45
+
46
+
47
+ # ---------------------------------------------------------------------------
48
+ # Conversion: Gregorian ↔ Gaian
49
+ # ---------------------------------------------------------------------------
50
+
51
+ def gregorian_to_gaian(d: date) -> tuple[int, int, int]:
52
+ """Convert a Gregorian date to a (gaian_year, month, day) tuple."""
53
+ iso_year, iso_week, iso_weekday = d.isocalendar()
54
+ month = (iso_week - 1) // 4 + 1 # 1–14
55
+ week_in_month = (iso_week - 1) % 4 # 0–3
56
+ day = week_in_month * 7 + iso_weekday # 1–28 (or 1–7 for Horus)
57
+ gaian_year = iso_year + 10_000
58
+ return gaian_year, month, day
59
+
60
+
61
+ def gaian_to_gregorian(year: int, month: int, day: int) -> date:
62
+ """Convert a Gaian date to a Gregorian datetime.date."""
63
+ iso_year = year - 10_000
64
+ iso_week = (month - 1) * 4 + (day - 1) // 7 + 1 # 1–53
65
+ iso_weekday = (day - 1) % 7 + 1 # 1–7
66
+ return date.fromisocalendar(iso_year, iso_week, iso_weekday)
67
+
68
+
69
+ # ---------------------------------------------------------------------------
70
+ # Derived properties
71
+ # ---------------------------------------------------------------------------
72
+
73
+ def day_of_year(month: int, day: int) -> int:
74
+ """Return the day-of-year (1–364, or up to 371 for Horus days)."""
75
+ if month <= 13:
76
+ return (month - 1) * 28 + day
77
+ else: # Horus (month 14)
78
+ return 364 + day
79
+
80
+
81
+ def day_of_week(day: int) -> int:
82
+ """Return ISO weekday (1=Monday … 7=Sunday). Same for all years — perpetual."""
83
+ return (day - 1) % 7 + 1
@@ -0,0 +1,80 @@
1
+ """
2
+ Static data: month and weekday metadata for the Gaian Calendar.
3
+ """
4
+
5
+ MONTHS: list[dict] = [
6
+ {"number": 1, "name": "Sagittarius", "abbrev": "Sag", "symbol": "♐", "element": "Fire", "iso_weeks": (1, 4)},
7
+ {"number": 2, "name": "Capricorn", "abbrev": "Cap", "symbol": "♑", "element": "Earth", "iso_weeks": (5, 8)},
8
+ {"number": 3, "name": "Aquarius", "abbrev": "Aqu", "symbol": "♒", "element": "Air", "iso_weeks": (9, 12)},
9
+ {"number": 4, "name": "Pisces", "abbrev": "Pis", "symbol": "♓", "element": "Water", "iso_weeks": (13, 16)},
10
+ {"number": 5, "name": "Aries", "abbrev": "Ari", "symbol": "♈", "element": "Fire", "iso_weeks": (17, 20)},
11
+ {"number": 6, "name": "Taurus", "abbrev": "Tau", "symbol": "♉", "element": "Earth", "iso_weeks": (21, 24)},
12
+ {"number": 7, "name": "Gemini", "abbrev": "Gem", "symbol": "♊", "element": "Air", "iso_weeks": (25, 28)},
13
+ {"number": 8, "name": "Cancer", "abbrev": "Can", "symbol": "♋", "element": "Water", "iso_weeks": (29, 32)},
14
+ {"number": 9, "name": "Leo", "abbrev": "Leo", "symbol": "♌", "element": "Fire", "iso_weeks": (33, 36)},
15
+ {"number": 10, "name": "Virgo", "abbrev": "Vir", "symbol": "♍", "element": "Earth", "iso_weeks": (37, 40)},
16
+ {"number": 11, "name": "Libra", "abbrev": "Lib", "symbol": "♎", "element": "Air", "iso_weeks": (41, 44)},
17
+ {"number": 12, "name": "Scorpius", "abbrev": "Sco", "symbol": "♏", "element": "Water", "iso_weeks": (45, 48)},
18
+ {"number": 13, "name": "Ophiuchus", "abbrev": "Oph", "symbol": "⛎", "element": "Healing", "iso_weeks": (49, 52)},
19
+ {"number": 14, "name": "Horus", "abbrev": "Hor", "symbol": "𓅃", "element": None, "iso_weeks": (53, 53)},
20
+ ]
21
+
22
+ WEEKDAYS: list[dict] = [
23
+ {"number": 1, "name": "Monday", "abbrev": "Mon", "symbol": "☽", "planet": "Moon"},
24
+ {"number": 2, "name": "Tuesday", "abbrev": "Tue", "symbol": "♂", "planet": "Mars"},
25
+ {"number": 3, "name": "Wednesday", "abbrev": "Wed", "symbol": "☿", "planet": "Mercury"},
26
+ {"number": 4, "name": "Thursday", "abbrev": "Thu", "symbol": "♃", "planet": "Jupiter"},
27
+ {"number": 5, "name": "Friday", "abbrev": "Fri", "symbol": "♀", "planet": "Venus"},
28
+ {"number": 6, "name": "Saturday", "abbrev": "Sat", "symbol": "♄", "planet": "Saturn"},
29
+ {"number": 7, "name": "Sunday", "abbrev": "Sun", "symbol": "☉", "planet": "Sun"},
30
+ ]
31
+
32
+ # Ordinal suffixes for days 1–28
33
+ _ORDINAL_SUFFIXES = {1: "st", 2: "nd", 3: "rd"}
34
+
35
+ def ordinal(n: int) -> str:
36
+ """Return ordinal string: 1 → '1st', 2 → '2nd', 15 → '15th'."""
37
+ suffix = _ORDINAL_SUFFIXES.get(n % 10, "th")
38
+ # Special case: 11th, 12th, 13th (not 11st, 12nd, 13rd)
39
+ if 11 <= n % 100 <= 13:
40
+ suffix = "th"
41
+ return f"{n}{suffix}"
42
+
43
+ _NUMBER_WORDS = [
44
+ "", "First", "Second", "Third", "Fourth", "Fifth", "Sixth", "Seventh",
45
+ "Eighth", "Ninth", "Tenth", "Eleventh", "Twelfth", "Thirteenth",
46
+ "Fourteenth", "Fifteenth", "Sixteenth", "Seventeenth", "Eighteenth",
47
+ "Nineteenth", "Twentieth", "Twenty-first", "Twenty-second", "Twenty-third",
48
+ "Twenty-fourth", "Twenty-fifth", "Twenty-sixth", "Twenty-seventh", "Twenty-eighth",
49
+ ]
50
+
51
+ def number_word(n: int) -> str:
52
+ """Return word form: 1 → 'First', 15 → 'Fifteenth'."""
53
+ if 1 <= n <= 28:
54
+ return _NUMBER_WORDS[n]
55
+ raise ValueError(f"number_word only supports 1–28, got {n}")
56
+
57
+ # Fast lookup maps
58
+ _MONTH_BY_NUMBER: dict[int, dict] = {m["number"]: m for m in MONTHS}
59
+ _MONTH_BY_NAME: dict[str, dict] = {}
60
+ for _m in MONTHS:
61
+ _MONTH_BY_NAME[_m["name"].lower()] = _m
62
+ _MONTH_BY_NAME[_m["abbrev"].lower()] = _m
63
+
64
+ _WEEKDAY_BY_NUMBER: dict[int, dict] = {w["number"]: w for w in WEEKDAYS}
65
+
66
+ def get_month(number: int) -> dict:
67
+ if number not in _MONTH_BY_NUMBER:
68
+ raise ValueError(f"Invalid month number: {number}")
69
+ return _MONTH_BY_NUMBER[number]
70
+
71
+ def get_month_by_name(name: str) -> dict:
72
+ key = name.lower().rstrip(".")
73
+ if key not in _MONTH_BY_NAME:
74
+ raise ValueError(f"Unknown month name: {name!r}")
75
+ return _MONTH_BY_NAME[key]
76
+
77
+ def get_weekday(number: int) -> dict:
78
+ if number not in _WEEKDAY_BY_NUMBER:
79
+ raise ValueError(f"Invalid weekday number: {number}")
80
+ return _WEEKDAY_BY_NUMBER[number]