Files
tarot/thelema_calendar.py
nose 79d4f1a09e k
2025-11-25 22:19:36 -08:00

216 lines
6.6 KiB
Python

"""
Thelema Calendar calculation function.
Converts Gregorian calendar to Thelema Calendar (Year of the Beast).
References:
- Year 1 of Thelema = 1904 CE (Spring Equinox)
- 22-year cycles (The Great Year)
- Weekday planetary rulers
"""
from datetime import datetime, date, timedelta, timezone
from typing import Dict, List, Optional
# Planetary symbols for weekdays (Sun=0, Mon=1, ..., Sat=6)
WEEKDAY_PLANETS = ["☉︎", "☽︎", "♂︎", "☿︎", "♃︎", "♀︎", "♄︎"]
# Luminary symbols
LUMINARY_SYMBOLS = {
"Moon": "☾︎",
"Mercury": "☿︎",
"Mars": "♂︎",
"Venus": "♀︎",
"Sun": "☉︎",
}
# Luminary order
LUMINARIES = ["Moon", "Mercury", "Mars", "Venus", "Sun"]
def to_roman(num: int) -> str:
"""Convert an integer to Roman numerals.
Args:
num: Integer to convert (1-3999)
Returns:
Roman numeral string
"""
val = [
1000, 900, 500, 400,
100, 90, 50, 40,
10, 9, 5, 4,
1
]
syms = [
"M", "CM", "D", "CD",
"C", "XC", "L", "XL",
"X", "IX", "V", "IV",
"I"
]
roman_num = ''
i = 0
while num > 0:
for _ in range(num // val[i]):
roman_num += syms[i]
num -= val[i]
i += 1
return roman_num
def _apply_gmt_offset(moment: datetime, gmt_offset: str) -> datetime:
"""Apply GMT offset to a datetime object.
Args:
moment: datetime object to convert
gmt_offset: Offset as string (e.g., "+3:00", "-8:00", "+5:30")
Returns:
datetime object adjusted to the specified timezone
Example:
>>> utc_time = datetime(2025, 11, 16, 12, 0, 0)
>>> pst_time = _apply_gmt_offset(utc_time, "-8:00")
>>> pst_time.hour
4 # 12:00 UTC - 8 hours = 04:00 PST
"""
# Parse GMT offset (e.g., "+3:00" or "-8:00")
gmt_offset = gmt_offset.strip()
# Handle format like "+3:00", "-8:00", "+5:30"
if ":" in gmt_offset:
sign = 1 if gmt_offset[0] == "+" else -1
parts = gmt_offset[1:].split(":")
hours = int(parts[0])
minutes = int(parts[1]) if len(parts) > 1 else 0
offset_seconds = sign * (hours * 3600 + minutes * 60)
else:
# Handle format like "+3" or "-8"
sign = 1 if gmt_offset[0] == "+" else -1
hours = int(gmt_offset[1:])
offset_seconds = sign * hours * 3600
# Create timezone with the offset
tz = timezone(timedelta(seconds=offset_seconds))
# Convert the naive datetime to aware datetime in UTC, then convert to target timezone
utc_dt = moment.replace(tzinfo=timezone.utc)
local_dt = utc_dt.astimezone(tz)
# Return as naive datetime (without timezone info) for calculation
return local_dt.replace(tzinfo=None)
def get_astro_data() -> Dict[str, str]:
"""Get current astrological data for luminaries.
This is a placeholder that returns mock data.
In a real implementation, you would scrape astro.com or use an astrology library.
Returns:
Dictionary mapping luminary names to their astrological positions
Format: "26°Taurus", "19°Sagittarius", etc.
"""
# Mock data - replace with actual astrological calculations
astro_data = {
"Moon": "24°Sagittarius",
"Mercury": "7°Aquarius",
"Mars": "4°Virgo",
"Venus": "30°Scorpio",
"Sun": "26°Taurus",
}
return astro_data
def thelema_calendar(moment: Optional[datetime] = None, gmt_offset: Optional[str] = None) -> str:
"""Calculate and return the Thelema Calendar date.
The Thelema Calendar counts from the Spring Equinox of 1904 CE (Year 1 of the Beast).
It divides time into 22-year cycles and displays planetary correspondences.
Args:
moment: datetime object (defaults to current date/time)
gmt_offset: GMT offset as string (e.g., "+3:00", "-8:00", "+5:30")
If provided, converts the given moment to this timezone before calculating.
Example: "+3:00" for UTC+3, "-8:00" for PST (UTC-8), "+5:30" for IST
Returns:
Formatted Thelema Calendar string with format:
"[Luminary positions] : [Weekday planet] : [Epoch][Year]"
Example:
>>> # Current time in UTC
>>> Tarot.thelema_calendar()
>>> # Specific time in PST (UTC-8)
>>> from datetime import datetime
>>> moment = datetime(2025, 11, 16, 14, 30, 0)
>>> Tarot.thelema_calendar(moment, "-8:00")
>>> # IST (UTC+5:30)
>>> Tarot.thelema_calendar(gmt_offset="+5:30")
"""
if moment is None:
moment = datetime.now()
# Apply timezone offset if provided
if gmt_offset is not None:
moment = _apply_gmt_offset(moment, gmt_offset)
today = moment.date()
current_year = today.year
# Calculate year difference from Thelema epoch (1904)
year_diff = current_year - 1904
# Check if we've passed the spring equinox (March 22)
# If yes, increment the year count
equinox_passed = (today.month > 3) or (today.month == 3 and today.day >= 22)
if equinox_passed:
year_diff += 1
# Calculate epoch (22-year cycles)
epoch_count = year_diff // 22
epoch_roman = to_roman(epoch_count)
# Calculate year within current 22-year cycle
thelema_year = (current_year - 12) % 22
thelema_year_roman = to_roman(thelema_year).lower()
# Get weekday (0=Monday, 6=Sunday in Python; we want 0=Sunday for alignment)
# Python's weekday(): Monday=0, Sunday=6
# We want: Sunday=0, Monday=1, ..., Saturday=6
python_weekday = today.weekday() # 0=Mon, 6=Sun
weekday_index = (python_weekday + 1) % 7 # Convert to Sun=0, Mon=1, etc.
# Determine weekday based on UTC hour (or provided moment hour)
utc_hour = moment.hour # Note: this is local hour, not UTC in this simplified version
if utc_hour >= 0 and utc_hour < 14:
day_weekday = weekday_index % 7
else:
day_weekday = (weekday_index + 1) % 7
weekday_symbol = WEEKDAY_PLANETS[day_weekday]
# Get astrological data
astro_data = get_astro_data()
# Build luminary string
luminary_strings = []
for luminary in LUMINARIES:
symbol = LUMINARY_SYMBOLS[luminary]
position = astro_data.get(luminary, "unknown")
luminary_strings.append(f"{symbol} {position}")
# Combine all cycles
cycles = [
" : ".join(luminary_strings),
weekday_symbol,
f"{epoch_roman}{thelema_year_roman}",
]
return " : ".join(cycles)