Last active
May 7, 2026 23:34
-
-
Save adgedenkers/aa28a537b8328298f739a4d62869c1d4 to your computer and use it in GitHub Desktop.
Spiral Date Calculator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| spiral-date — look up the Nine Day Sun position for any date. | |
| Usage: | |
| spiral-date # today | |
| spiral-date 2026-04-15 # specific date (Sun 5) | |
| spiral-date 2020-06-15 # specific date (Sun 4) | |
| spiral-date -3113-08-11 # Sun 4 anchor (astronomical year notation) | |
| spiral-date tomorrow # relative | |
| spiral-date +7 # 7 days from now | |
| Sun 4: August 11, 3114 BCE → March 19, 2026 | |
| Sun 5: March 20, 2026 → onward | |
| Dates before August 11, 3114 BCE (Sun 3 and earlier) are not yet defined. | |
| """ | |
| import sys | |
| from datetime import date, timedelta | |
| # --------------------------------------------------------------------------- | |
| # Framework constants — pure base-9 | |
| # --------------------------------------------------------------------------- | |
| DAYS_PER_CYCLE = 9 | |
| DAYS_PER_SPIRAL = 81 | |
| DAYS_PER_EPOCH = 729 | |
| DAYS_PER_AEON = 6_561 | |
| DAYS_PER_WATCH = 59_049 | |
| DAYS_PER_ERA = 531_441 | |
| META_CYCLE_LENGTH = 63 | |
| # --------------------------------------------------------------------------- | |
| # Sun anchors | |
| # | |
| # Sun 4: Maya Long Count creation date (0.0.0.0.0 = 4 Ajaw 8 Kumk'u) | |
| # August 11, 3114 BCE (proleptic Gregorian) | |
| # GMT correlation JDN = 584283 | |
| # | |
| # Sun 5: First equinox after the Saturn-Neptune conjunction at 0° Aries | |
| # March 20, 2026 | |
| # --------------------------------------------------------------------------- | |
| SUN5_ANCHOR = date(2026, 3, 20) | |
| # We can't represent 3114 BCE with datetime, so we use JDN for Sun 4. | |
| # JDN for the Sun 4 anchor (Maya creation date): | |
| SUN4_ANCHOR_JDN = 584283 | |
| # JDN for the Sun 5 anchor: | |
| SUN5_ANCHOR_JDN = _sun5_jdn = None # computed at module load | |
| WEEKDAY_PLANETS = { | |
| 0: ("Monday", "Moon", "☽"), | |
| 1: ("Tuesday", "Mars", "♂"), | |
| 2: ("Wednesday", "Mercury", "☿"), | |
| 3: ("Thursday", "Jupiter", "♃"), | |
| 4: ("Friday", "Venus", "♀"), | |
| 5: ("Saturday", "Saturn", "♄"), | |
| 6: ("Sunday", "Sun", "☉"), | |
| } | |
| DAYS = { | |
| 1: ("Aris", "Ignition", "The flame is lit"), | |
| 2: ("Selun", "Descent", "The flame descends"), | |
| 3: ("Valen", "Structure", "The flame builds form"), | |
| 4: ("Oran", "Motion", "The flame moves"), | |
| 5: ("Thael", "Vision", "The flame dreams"), | |
| 6: ("Riven", "Break / Severance", "The flame sheds"), | |
| 7: ("Miren", "Return", "The flame integrates"), | |
| 8: ("Kiren", "Service", "The flame offers"), | |
| 9: ("Sayel", "Sealing", "The flame rests"), | |
| } | |
| VERBS = { | |
| 1: "Start", 2: "Listen", 3: "Anchor", 4: "Act", 5: "Dream", | |
| 6: "Release", 7: "Integrate", 8: "Offer", 9: "Complete", | |
| } | |
| SABBATH_LABELS = [ | |
| "", | |
| "Day 9 — rest day", | |
| "Day 9 of Cycle 9 — deep rest", | |
| "Day 9 · Cycle 9 · Spiral 9 — season sabbath", | |
| "Depth 4 — quadruple nine", | |
| "Depth 5 — quintuple nine", | |
| "Depth 6 — sextuple nine", | |
| "Depth 7 — full sabbath (every level at 9)", | |
| ] | |
| # --------------------------------------------------------------------------- | |
| # JDN conversion | |
| # --------------------------------------------------------------------------- | |
| def gregorian_to_jdn(y, m, d): | |
| """ | |
| Convert a proleptic Gregorian date to Julian Day Number. | |
| Handles negative (astronomical) years for BCE dates. | |
| """ | |
| if m <= 2: | |
| y -= 1 | |
| m += 12 | |
| A = y // 100 | |
| B = 2 - A + A // 4 | |
| return int(365.25 * (y + 4716)) + int(30.6001 * (m + 1)) + d + B - 1524 | |
| def jdn_to_gregorian(jdn): | |
| """Convert JDN back to (year, month, day) in proleptic Gregorian.""" | |
| l = jdn + 68569 | |
| n = 4 * l // 146097 | |
| l = l - (146097 * n + 3) // 4 | |
| i = 4000 * (l + 1) // 1461001 | |
| l = l - 1461 * i // 4 + 31 | |
| j = 80 * l // 2447 | |
| d = l - 2447 * j // 80 | |
| l = j // 11 | |
| m = j + 2 - 12 * l | |
| y = 100 * (n - 49) + i + l | |
| return y, m, d | |
| def jdn_to_weekday(jdn): | |
| """Return weekday index (0=Monday, 6=Sunday) from JDN.""" | |
| return (jdn + 0) % 7 # JDN 0 = Monday | |
| # Compute Sun 5 anchor JDN at module load | |
| SUN5_ANCHOR_JDN = gregorian_to_jdn(2026, 3, 20) | |
| # --------------------------------------------------------------------------- | |
| # Position math | |
| # --------------------------------------------------------------------------- | |
| def position_from_delta(delta): | |
| """ | |
| Given a non-negative day offset from a Sun's anchor, | |
| return the base-9 stack coordinates. | |
| """ | |
| remainder = delta | |
| era_offset = remainder // DAYS_PER_ERA; remainder %= DAYS_PER_ERA | |
| watch_offset = remainder // DAYS_PER_WATCH; remainder %= DAYS_PER_WATCH | |
| aeon_offset = remainder // DAYS_PER_AEON; remainder %= DAYS_PER_AEON | |
| epoch_offset = remainder // DAYS_PER_EPOCH; remainder %= DAYS_PER_EPOCH | |
| spiral_offset = remainder // DAYS_PER_SPIRAL; remainder %= DAYS_PER_SPIRAL | |
| cycle_offset = remainder // DAYS_PER_CYCLE; remainder %= DAYS_PER_CYCLE | |
| return { | |
| "era": 1 + era_offset, | |
| "watch": 1 + watch_offset, | |
| "aeon": 1 + aeon_offset, | |
| "epoch": 1 + epoch_offset, | |
| "spiral": 1 + spiral_offset, | |
| "cycle": 1 + cycle_offset, | |
| "day": 1 + remainder, | |
| "days_since_anchor": delta, | |
| } | |
| def resolve_sun(jdn): | |
| """ | |
| Determine which Sun a JDN falls in and return | |
| (sun_number, anchor_jdn, delta_from_anchor). | |
| Returns None if before Sun 4. | |
| """ | |
| if jdn >= SUN5_ANCHOR_JDN: | |
| return 5, SUN5_ANCHOR_JDN, jdn - SUN5_ANCHOR_JDN | |
| elif jdn >= SUN4_ANCHOR_JDN: | |
| return 4, SUN4_ANCHOR_JDN, jdn - SUN4_ANCHOR_JDN | |
| else: | |
| return None, None, None | |
| def sabbath_depth(p): | |
| depth = 0 | |
| for level in [p["day"], p["cycle"], p["spiral"], p["epoch"], | |
| p["aeon"], p["watch"], p["era"]]: | |
| if level == 9: | |
| depth += 1 | |
| else: | |
| break | |
| return depth | |
| def cycle_seed_jdn(jdn, p): | |
| """JDN of Day 1 of the cycle containing this date.""" | |
| return jdn - (p["day"] - 1) | |
| # --------------------------------------------------------------------------- | |
| # Date parsing — supports both CE and BCE dates | |
| # --------------------------------------------------------------------------- | |
| def parse_input(arg): | |
| """ | |
| Parse input and return a JDN. | |
| Accepts: | |
| today, tomorrow, yesterday | |
| +N, -N (relative days) | |
| YYYY-MM-DD (CE dates, positive year) | |
| -YYYY-MM-DD (astronomical year for BCE; -3113 = 3114 BCE) | |
| """ | |
| if arg in ("today", ""): | |
| d = date.today() | |
| return gregorian_to_jdn(d.year, d.month, d.day) | |
| if arg == "tomorrow": | |
| d = date.today() + timedelta(days=1) | |
| return gregorian_to_jdn(d.year, d.month, d.day) | |
| if arg == "yesterday": | |
| d = date.today() - timedelta(days=1) | |
| return gregorian_to_jdn(d.year, d.month, d.day) | |
| # Relative days: +N or -N (but not -YYYY-MM-DD) | |
| if arg.startswith("+"): | |
| d = date.today() + timedelta(days=int(arg)) | |
| return gregorian_to_jdn(d.year, d.month, d.day) | |
| if arg.startswith("-") and arg.count("-") == 1: | |
| # Just a negative number like -7 | |
| d = date.today() + timedelta(days=int(arg)) | |
| return gregorian_to_jdn(d.year, d.month, d.day) | |
| # ISO-ish date: YYYY-MM-DD or -YYYY-MM-DD | |
| parts = arg.split("-") | |
| if arg.startswith("-"): | |
| # Negative/astronomical year: -3113-08-11 | |
| # parts = ['', '3113', '08', '11'] | |
| year = -int(parts[1]) | |
| month = int(parts[2]) | |
| day = int(parts[3]) | |
| else: | |
| # Positive year: 2026-03-20 | |
| year = int(parts[0]) | |
| month = int(parts[1]) | |
| day = int(parts[2]) | |
| return gregorian_to_jdn(year, month, day) | |
| def format_date_str(jdn): | |
| """Format a JDN as a human-readable date string.""" | |
| y, m, d = jdn_to_gregorian(jdn) | |
| months = [ | |
| "", "January", "February", "March", "April", "May", "June", | |
| "July", "August", "September", "October", "November", "December", | |
| ] | |
| weekdays = [ | |
| "Monday", "Tuesday", "Wednesday", "Thursday", | |
| "Friday", "Saturday", "Sunday", | |
| ] | |
| wd = weekdays[jdn_to_weekday(jdn)] | |
| if y <= 0: | |
| # Convert astronomical year to BCE | |
| bce_year = 1 - y | |
| return f"{wd}, {months[m]} {d}, {bce_year} BCE" | |
| else: | |
| return f"{wd}, {months[m]} {d}, {y} CE" | |
| # --------------------------------------------------------------------------- | |
| # Output | |
| # --------------------------------------------------------------------------- | |
| def display(jdn): | |
| sun, anchor_jdn, delta = resolve_sun(jdn) | |
| if sun is None: | |
| date_str = format_date_str(jdn) | |
| print() | |
| print(f" {date_str}") | |
| print(f" This date is before Sun 4 (August 11, 3114 BCE).") | |
| print(f" Sun 3 and earlier are not yet defined.") | |
| print() | |
| sys.exit(1) | |
| p = position_from_delta(delta) | |
| sig_full = ( | |
| f"{sun} : {p['era']}.{p['watch']}.{p['aeon']}." | |
| f"{p['epoch']}.{p['spiral']}.{p['cycle']}.{p['day']}" | |
| ) | |
| sig_short = ( | |
| f"{p['aeon']}.{p['epoch']}.{p['spiral']}.{p['cycle']}.{p['day']}" | |
| ) | |
| day_num = p["day"] | |
| ritual_name, tone, flame = DAYS[day_num] | |
| verb = VERBS[day_num] | |
| wd_idx = jdn_to_weekday(jdn) | |
| wd_name, wd_planet, wd_glyph = WEEKDAY_PLANETS[wd_idx] | |
| seed_jdn = cycle_seed_jdn(jdn, p) | |
| seed_wd_idx = jdn_to_weekday(seed_jdn) | |
| _, seed_planet, seed_glyph = WEEKDAY_PLANETS[seed_wd_idx] | |
| meta_day = (delta % META_CYCLE_LENGTH) + 1 | |
| depth = sabbath_depth(p) | |
| date_str = format_date_str(jdn) | |
| print() | |
| print(f" {date_str}") | |
| print(f" {'─' * 44}") | |
| print(f" {sig_full}") | |
| print(f" {sig_short} (short)") | |
| print() | |
| print(f" Sun {sun} Day {day_num} — {ritual_name}") | |
| print(f" Tone: {tone}") | |
| print(f" Flame: {flame}") | |
| print(f" Verb: {verb}") | |
| print() | |
| print(f" Weekday planet: {wd_planet} {wd_glyph} ({wd_name})") | |
| print(f" Cycle seed planet: {seed_planet} {seed_glyph}") | |
| print(f" Meta-cycle: {meta_day}/63") | |
| print(f" Days since Sun {sun} anchor: {delta:,}") | |
| if depth > 0: | |
| print() | |
| print(f" ✦ {SABBATH_LABELS[depth]}") | |
| print() | |
| # --------------------------------------------------------------------------- | |
| # Self-test | |
| # --------------------------------------------------------------------------- | |
| def verify(): | |
| """Verify against known reference points.""" | |
| tests = [ | |
| # (description, jdn_input, expected_sig) | |
| ( | |
| "Sun 4 anchor (Aug 11, 3114 BCE)", | |
| SUN4_ANCHOR_JDN, | |
| "4 : 1.1.1.1.1.1.1", | |
| ), | |
| ( | |
| "Sun 5 anchor (Mar 20, 2026)", | |
| SUN5_ANCHOR_JDN, | |
| "5 : 1.1.1.1.1.1.1", | |
| ), | |
| ( | |
| "Last day of Sun 4 (Mar 19, 2026)", | |
| SUN5_ANCHOR_JDN - 1, | |
| "4 : 4.5.8.1.5.8.4", | |
| ), | |
| ( | |
| "Sun 4 day 2 (Aug 12, 3114 BCE)", | |
| SUN4_ANCHOR_JDN + 1, | |
| "4 : 1.1.1.1.1.1.2", | |
| ), | |
| ( | |
| "Sun 5 day 2 (Mar 21, 2026)", | |
| SUN5_ANCHOR_JDN + 1, | |
| "5 : 1.1.1.1.1.1.2", | |
| ), | |
| ] | |
| all_pass = True | |
| for desc, jdn, expected_sig in tests: | |
| sun, anchor_jdn, delta = resolve_sun(jdn) | |
| p = position_from_delta(delta) | |
| sig = ( | |
| f"{sun} : {p['era']}.{p['watch']}.{p['aeon']}." | |
| f"{p['epoch']}.{p['spiral']}.{p['cycle']}.{p['day']}" | |
| ) | |
| ok = sig == expected_sig | |
| status = "✓" if ok else "✗" | |
| if not ok: | |
| all_pass = False | |
| print(f" {status} {desc}") | |
| print(f" got: {sig}") | |
| print(f" expected: {expected_sig}") | |
| else: | |
| print(f" {status} {desc}: {sig}") | |
| return all_pass | |
| # --------------------------------------------------------------------------- | |
| # Main | |
| # --------------------------------------------------------------------------- | |
| def main(): | |
| if len(sys.argv) > 1 and sys.argv[1] == "--verify": | |
| print() | |
| print(" Verifying against known reference points...") | |
| print() | |
| ok = verify() | |
| print() | |
| if ok: | |
| print(" All checks passed.") | |
| else: | |
| print(" SOME CHECKS FAILED.") | |
| sys.exit(1) | |
| print() | |
| return | |
| arg = sys.argv[1] if len(sys.argv) > 1 else "today" | |
| try: | |
| jdn = parse_input(arg) | |
| except (ValueError, TypeError, IndexError): | |
| print(f" Could not parse date: {arg}") | |
| print() | |
| print(f" Usage: spiral-date [YYYY-MM-DD | -YYYY-MM-DD | today | tomorrow | +N | -N]") | |
| print(f" spiral-date --verify") | |
| print() | |
| print(f" Examples:") | |
| print(f" spiral-date # today") | |
| print(f" spiral-date 2026-05-07 # Sun 5 date") | |
| print(f" spiral-date 2020-01-01 # Sun 4 date") | |
| print(f" spiral-date -3113-08-11 # Sun 4 anchor (3114 BCE)") | |
| print(f" spiral-date +9 # 9 days from now") | |
| sys.exit(1) | |
| display(jdn) | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment