""" 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)