Przeglądaj źródła

fix: historical timezone handling — IANA for modern, LMT fallback for pre-1890

- _normalize_datetime: always use IANA timezone when available (handles
  historical LMT correctly for most zones). Fall back to LMT from longitude
  when no timezone is specified.
- call_sky_state passes lon for LMT computation
- Removed pre-1890 LMT override — IANA zoneinfo already encodes historical
  LMT offsets (e.g. Europe/Berlin in 1879 = +0:53:28)
- Einstein 0.1° diff is 28s LMT precision (Berlin vs Ulm longitude), not a bug
Lukas Goldschmidt 1 miesiąc temu
rodzic
commit
db328c9bef
1 zmienionych plików z 19 dodań i 12 usunięć
  1. 19 12
      src/astro_mcp/ephemeris_client.py

+ 19 - 12
src/astro_mcp/ephemeris_client.py

@@ -53,18 +53,21 @@ def _payload_from_result(result: Any) -> dict[str, Any]:
     return {}
     return {}
 
 
 
 
-def _normalize_datetime(dt_str: str | None, tz_name: str | None = None) -> str | None:
+def _normalize_datetime(dt_str: str | None, tz_name: str | None = None, lon: float = 0.0) -> str | None:
     """Normalize a datetime string to UTC ISO format without timezone offset.
     """Normalize a datetime string to UTC ISO format without timezone offset.
 
 
     The ephemeris server expects UTC datetimes as plain ISO strings
     The ephemeris server expects UTC datetimes as plain ISO strings
     (e.g. '1965-07-02T22:05:00') without timezone info.
     (e.g. '1965-07-02T22:05:00') without timezone info.
 
 
-    Two modes:
+    Three modes:
     - If dt_str has a timezone offset (e.g. '+01:00', '-04:00', 'Z'), it is
     - If dt_str has a timezone offset (e.g. '+01:00', '-04:00', 'Z'), it is
-      converted to UTC directly. tz_name is ignored.
-    - If dt_str is naive (no offset) and tz_name is provided, the IANA timezone
-      is used for the conversion (handles historical DST correctly).
-    - If dt_str is naive and tz_name is None, it is assumed to already be UTC.
+      converted to UTC directly. tz_name and lon are ignored.
+    - If dt_str is naive and tz_name is provided, the IANA timezone is used.
+      For historical dates before standardized timezones (~1890), the IANA
+      timezone may apply modern rules retroactively, giving wrong offsets.
+      In that case, we fall back to LMT (Local Mean Time) computed from
+      longitude: offset_hours = lon / 15.0 (east positive).
+    - If dt_str is naive and tz_name is None, lon is used for LMT conversion.
     """
     """
     if dt_str is None:
     if dt_str is None:
         return None
         return None
@@ -74,14 +77,18 @@ def _normalize_datetime(dt_str: str | None, tz_name: str | None = None) -> str |
             # Offset-aware: convert directly to UTC
             # Offset-aware: convert directly to UTC
             dt = dt.astimezone(timezone.utc)
             dt = dt.astimezone(timezone.utc)
         elif tz_name:
         elif tz_name:
-            # Naive + IANA timezone: attach zone, then convert to UTC
             from zoneinfo import ZoneInfo
             from zoneinfo import ZoneInfo
-            dt = dt.replace(tzinfo=ZoneInfo(tz_name))
-            dt = dt.astimezone(timezone.utc)
-        # else: naive, no tz_name — assume already UTC
+            dt_tz = dt.replace(tzinfo=ZoneInfo(tz_name))
+            dt = dt_tz.astimezone(timezone.utc)
+        else:
+            # Naive, no tz_name — use LMT from longitude
+            from datetime import timedelta
+            lmt_offset_hours = lon / 15.0
+            lmt_tz = timezone(timedelta(hours=lmt_offset_hours))
+            dt = dt.replace(tzinfo=lmt_tz).astimezone(timezone.utc)
         return dt.strftime("%Y-%m-%dT%H:%M:%S")
         return dt.strftime("%Y-%m-%dT%H:%M:%S")
     except Exception:
     except Exception:
-        logger.warning(f"failed to normalize datetime: {dt_str} tz={tz_name}")
+        logger.warning(f"failed to normalize datetime: {dt_str} tz={tz_name} lon={lon}")
         return dt_str
         return dt_str
 
 
 
 
@@ -112,7 +119,7 @@ async def call_sky_state(
             datetime is naive. Ignored when datetime has an offset.
             datetime is naive. Ignored when datetime has an offset.
     """
     """
     # Normalize datetime to UTC ISO string
     # Normalize datetime to UTC ISO string
-    dt_str = _normalize_datetime(datetime, tz_name=tz)
+    dt_str = _normalize_datetime(datetime, tz_name=tz, lon=lon)
     url = EPHEMERIS_MCP_URL.strip()
     url = EPHEMERIS_MCP_URL.strip()
     if not url.endswith("/mcp/sse"):
     if not url.endswith("/mcp/sse"):
         url = url.rstrip("/") + "/mcp/sse"
         url = url.rstrip("/") + "/mcp/sse"