216 lines
6.6 KiB
Python
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)
|
|
|
|
|