#!/usr/bin/env python3 """ Concordia ⇄ Gregorian ⇄ Julian Offset Calculator (Continuous Interactive Version) + Hebrew Author: Christopher Welch (Julian support added) Description: - Runs continuously for multiple conversions in one session. - Input either a Gregorian date (YYYY-MM-DD / Month Day Year / MM DD YYYY), a Julian date in the same styles, or a Concordia offset (integer). - Outputs: • Gregorian date in "Month Day, Year" (BCE for y <= 0) • Julian date in "Month Day, Year" (BCE for y <= 0, proleptic Julian) • Hebrew date (if convertdate installed and in range; otherwise helpful note) - Type 'exit' to quit. Anchor date (Concordia Day 0): April 14 2030 (Gregorian) """ from datetime import date, timedelta, datetime # Optional Hebrew conversion support _HAVE_CONVERTDATE = True try: from convertdate import hebrew except Exception: _HAVE_CONVERTDATE = False # if missing, we’ll still run and label accordingly ANCHOR = date(2030, 4, 14) # ----------------------- # Parsing / formatting # ----------------------- def parse_date(datestr: str) -> date: """ Parse a date string using several common formats (AD years only). Newly added: 'Month Day Year' (with or without comma) and 'MM DD YYYY'. Examples accepted: - 2029-07-26 - 07/26/2029 - July 26, 2029 - July 26 2029 - 26 Jul 2029 - 26 July 2029 - 07 26 2029 - Jul 26 2029 - Apr 14 2030 - April 14 2030 """ formats = [ "%Y-%m-%d", # 2029-07-26 "%m/%d/%Y", # 07/26/2029 "%b %d, %Y", # Jul 26, 2029 "%B %d, %Y", # July 26, 2029 "%d %b %Y", # 26 Jul 2029 "%d %B %Y", # 26 July 2029 "%m %d %Y", # 07 26 2029 (added) "%B %d %Y", # July 26 2029 (added, no comma) "%b %d %Y", # Jul 26 2029 (added, no comma) ] s = datestr.strip() for fmt in formats: try: return datetime.strptime(s, fmt).date() except ValueError: continue raise ValueError(f"Could not parse '{datestr}'. Try formats like 'April 14 2030', 'April 14, 2030', '04 14 2030', or 'YYYY-MM-DD'.") MONTH_NAMES = [ None, "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ] def format_greg_ymd(y: int, m: int, d: int) -> str: """Format proleptic Gregorian Y/M/D as 'Month Day, Year' or 'Month Day, Year BCE' if y<=0.""" month = MONTH_NAMES[m] if 1 <= m <= 12 else f"Month {m}" if y <= 0: # Astronomical year y: 0 => 1 BCE, -1 => 2 BCE, ... bce_year = 1 - y return f"{month} {d:02d}, {bce_year} BCE" else: return f"{month} {d:02d}, {y}" def format_jul_ymd(y: int, m: int, d: int) -> str: """Format proleptic Julian Y/M/D as 'Month Day, Year' or 'Month Day, Year BCE' if y<=0.""" month = MONTH_NAMES[m] if 1 <= m <= 12 else f"Month {m}" if y <= 0: bce_year = 1 - y return f"{month} {d:02d}, {bce_year} BCE (Julian)" else: return f"{month} {d:02d}, {y} (Julian)" def format_date(dt: date) -> str: """Return a datetime.date in 'Month Day, Year' (AD dates only).""" return dt.strftime("%B %d, %Y") # ----------------------- # JDN conversions (Gregorian & Julian; supports BCE via astronomical years) # ----------------------- def gregorian_to_jdn(y: int, m: int, d: int) -> int: """ Fliegel–Van Flandern algorithm for proleptic Gregorian to JDN. Works for all integer years (astronomical numbering). """ a = (14 - m) // 12 y2 = y + 4800 - a m2 = m + 12 * a - 3 return d + ((153 * m2 + 2) // 5) + 365 * y2 + (y2 // 4) - (y2 // 100) + (y2 // 400) - 32045 def jdn_to_gregorian(j: int) -> tuple[int, int, int]: """ Inverse Fliegel–Van Flandern: JDN -> proleptic Gregorian (Y, M, D). Returns astronomical year (may be <= 0 for BCE). """ f = j + 1401 + (((4 * j + 274277) // 146097) * 3) // 4 - 38 e = 4 * f + 3 g = (e % 1461) // 4 h = 5 * g + 2 D = (h % 153) // 5 + 1 M = ((h // 153 + 2) % 12) + 1 Y = e // 1461 - 4716 + (12 + 2 - M) // 12 return Y, M, D def julian_to_jdn(y: int, m: int, d: int) -> int: """ Proleptic Julian calendar to JDN (Fliegel–Van Flandern variant). Supports astronomical year numbering (y=0 => 1 BCE). """ a = (14 - m) // 12 y2 = y + 4800 - a m2 = m + 12 * a - 3 return d + ((153 * m2 + 2) // 5) + 365 * y2 + (y2 // 4) - 32083 def jdn_to_julian(j: int) -> tuple[int, int, int]: """ JDN -> proleptic Julian calendar (Y, M, D). Returns astronomical year (may be <= 0 for BCE). """ c = j + 32082 d = (4 * c + 3) // 1461 e = c - (1461 * d) // 4 m = (5 * e + 2) // 153 day = e - (153 * m + 2) // 5 + 1 month = m + 3 - 12 * (m // 10) year = d - 4800 + (m // 10) return year, month, day # Precompute anchor JDN (Gregorian anchor) _ANCHOR_JDN = gregorian_to_jdn(ANCHOR.year, ANCHOR.month, ANCHOR.day) def gregorian_from_concordia(offset: int) -> tuple[int, int, int]: """Return proleptic Gregorian (Y, M, D) for Concordia offset using JDN math (supports BCE).""" j = _ANCHOR_JDN + offset return jdn_to_gregorian(j) def julian_from_concordia(offset: int) -> tuple[int, int, int]: """Return proleptic Julian (Y, M, D) for Concordia offset using JDN math (supports BCE).""" j = _ANCHOR_JDN + offset return jdn_to_julian(j) # ----------------------- # Concordia helpers # ----------------------- def concordia_from_gregorian(greg: date) -> int: """Return Concordia offset (days since April 14, 2030) using datetime for AD dates.""" return (greg - ANCHOR).days def concordia_from_ymd_greg(y: int, m: int, d: int) -> int: """Return Concordia offset from a proleptic Gregorian Y/M/D using JDN (supports BCE).""" j = gregorian_to_jdn(y, m, d) return j - _ANCHOR_JDN def concordia_from_ymd_jul(y: int, m: int, d: int) -> int: """Return Concordia offset from a proleptic Julian Y/M/D using JDN (supports BCE).""" j = julian_to_jdn(y, m, d) return j - _ANCHOR_JDN # ----------------------- # Hebrew helpers # ----------------------- _HEB_MONTHS_NON_LEAP = [ None, "Nisan", "Iyyar", "Sivan", "Tammuz", "Av", "Elul", "Tishrei", "Cheshvan", "Kislev", "Tevet", "Shevat", "Adar" ] _HEB_MONTHS_LEAP = [ None, "Nisan", "Iyyar", "Sivan", "Tammuz", "Av", "Elul", "Tishrei", "Cheshvan", "Kislev", "Tevet", "Shevat", "Adar I", "Adar II" ] def heb_month_name(hy: int, hm: int) -> str: if not _HAVE_CONVERTDATE: return f"Month {hm}" months = _HEB_MONTHS_LEAP if hebrew.leap(hy) else _HEB_MONTHS_NON_LEAP if not (1 <= hm < len(months)): return f"Month {hm}" return months[hm] def hebrew_from_gregorian_safe(y: int, m: int, d: int) -> str: """ Try to convert Gregorian Y/M/D to a Hebrew date string. If out of range or beyond AM 6000, return 'pre-AM' or '>6000 AM (out of range)'. If convertdate not installed, return a helpful message. """ if not _HAVE_CONVERTDATE: return "(install 'convertdate' to enable Hebrew dates)" try: # convertdate expects civil (no year 0). For astronomical y=0, map to 1 BCE surrogate. yy = y if y != 0 else -1 hy, hm, hd = hebrew.from_gregorian(yy, m, d) if hy <= 0: return "pre-AM" if hy > 6000: return ">6000 AM (out of range)" return f"{hd} {heb_month_name(hy, hm)} {hy} AM" except Exception: return "pre-AM" # ----------------------- # Main loop # ----------------------- def run_calculator(): print("=== Concordia ⇄ Gregorian ⇄ Julian ⇄ Hebrew Offset Calculator ===") print(f"Anchor date (Concordia Day 0): {format_date(ANCHOR)} (Gregorian)") if not _HAVE_CONVERTDATE: print("⚠️ Hebrew conversion not available. Install with: pip install convertdate") print("Type 'exit' anytime to quit.\n") while True: mode = input("Input (G)regorian, (J)ulian, or (C)oncordia offset? ").strip().lower() if mode in ["exit", "quit"]: print("\nGoodbye!") break # --- Gregorian mode --- if mode.startswith("g"): g_str = input("Enter Gregorian date (e.g., 2029-07-26 or 'April 14 2030'): ").strip() if g_str.lower() in ["exit", "quit"]: break try: g_date = parse_date(g_str) # AD only offset = concordia_from_gregorian(g_date) y, m, d = g_date.year, g_date.month, g_date.day g_fmt = format_date(g_date) jdn = gregorian_to_jdn(y, m, d) jy, jm, jd = jdn_to_julian(jdn) except ValueError as e: print(f"❌ {e}\n") continue print(f"\nGregorian date : {g_fmt}") print(f"Julian date : {format_jul_ymd(jy, jm, jd)}") print(f"Concordia day : {offset:+d} (relative to {format_date(ANCHOR)})") print(f"Hebrew date : {hebrew_from_gregorian_safe(y, m, d)}") step_str = input("\nApply additional Concordia offset? (number or 0 to skip): ").strip() if step_str.lower() in ["exit", "quit"]: break if step_str: try: step = int(step_str) jdn2 = jdn + step y2, m2, d2 = jdn_to_gregorian(jdn2) jy2, jm2, jd2 = jdn_to_julian(jdn2) new_offset = concordia_from_ymd_greg(y2, m2, d2) print(f"\n➡ New Gregorian date : {format_greg_ymd(y2, m2, d2)}") print(f"➡ New Julian date : {format_jul_ymd(jy2, jm2, jd2)}") print(f"➡ New Concordia day : {new_offset:+d}") print(f"➡ New Hebrew date : {hebrew_from_gregorian_safe(y2, m2, d2)}\n") except ValueError: print("Invalid number — skipping adjustment.\n") # --- Julian mode --- elif mode.startswith("j"): j_str = input("Enter Julian date (e.g., '0033-04-16' or 'April 3 33'): ").strip() if j_str.lower() in ["exit", "quit"]: break try: # Parse tokens via datetime, but treat them as Julian by conversion below. dt = parse_date(j_str) # parses tokens; calendar meaning differs y, m, d = dt.year, dt.month, dt.day jdn = julian_to_jdn(y, m, d) gy, gm, gd = jdn_to_gregorian(jdn) g_fmt = format_greg_ymd(gy, gm, gd) offset = jdn - _ANCHOR_JDN except ValueError as e: print(f"❌ {e}\n") continue print(f"\nJulian date : {format_jul_ymd(y, m, d)}") print(f"Gregorian date : {g_fmt}") print(f"Concordia day : {offset:+d} (relative to {format_date(ANCHOR)})") print(f"Hebrew date : {hebrew_from_gregorian_safe(gy, gm, gd)}") step_str = input("\nApply additional offset? (number or 0 to skip): ").strip() if step_str.lower() in ["exit", "quit"]: break if step_str: try: step = int(step_str) jdn2 = jdn + step gy2, gm2, gd2 = jdn_to_gregorian(jdn2) jy2, jm2, jd2 = jdn_to_julian(jdn2) new_offset = jdn2 - _ANCHOR_JDN print(f"\n➡ New Julian date : {format_jul_ymd(jy2, jm2, jd2)}") print(f"➡ New Gregorian date : {format_greg_ymd(gy2, gm2, gd2)}") print(f"➡ New Concordia day : {new_offset:+d}") print(f"➡ New Hebrew date : {hebrew_from_gregorian_safe(gy2, gm2, gd2)}\n") except ValueError: print("Invalid number — skipping adjustment.\n") # --- Concordia mode --- elif mode.startswith("c"): c_str = input("Enter Concordia offset (e.g., 103 or -263): ").strip() if c_str.lower() in ["exit", "quit"]: break try: offset = int(c_str) except ValueError: print("❌ Invalid number.\n") continue # Use JDN to support BCE y, m, d = gregorian_from_concordia(offset) jy, jm, jd = julian_from_concordia(offset) print(f"\nConcordia day : {offset:+d}") print(f"Gregorian date : {format_greg_ymd(y, m, d)}") print(f"Julian date : {format_jul_ymd(jy, jm, jd)}") print(f"Hebrew date : {hebrew_from_gregorian_safe(y, m, d)}") step_str = input("\nApply additional offset? (number or 0 to skip): ").strip() if step_str.lower() in ["exit", "quit"]: break if step_str: try: step = int(step_str) new_offset = offset + step y2, m2, d2 = gregorian_from_concordia(new_offset) jy2, jm2, jd2 = julian_from_concordia(new_offset) print(f"\n➡ New Concordia day : {new_offset:+d}") print(f"➡ New Gregorian date : {format_greg_ymd(y2, m2, d2)}") print(f"➡ New Julian date : {format_jul_ymd(jy2, jm2, jd2)}") print(f"➡ New Hebrew date : {hebrew_from_gregorian_safe(y2, m2, d2)}\n") except ValueError: print("Invalid number — skipping adjustment.\n") else: print("Please choose 'G' for Gregorian, 'J' for Julian, or 'C' for Concordia.\n") if __name__ == "__main__": try: run_calculator() except KeyboardInterrupt: print("\n\nSession ended by user. Goodbye!")