"""Strefa czasowa z lokalizacji (PRE-03) — „logika dwóch lokalizacji". Cel po ludzku: żeby użytkownik nie musiał znać offsetu GMT ani pamiętać o czasie letnim. Z dokładnych współrzędnych bierzemy strefę IANA (`tzfpy`, OFFLINE — bez zapytań do sieci, więc działa też bez internetu), a z niej — offset DLA DATY URODZENIA. `zoneinfo` zna reguły historyczne i DST, więc np. Kraków 1984 to +1h zimą, +2h latem, a Katmandu to +5:45. Sedno PRE-03: offset ustalamy RAZ i trzymamy jako stałą liczbę w formularzu — przeliczenie dla dokładnej lokalizacji już nim nie rusza. Dzięki temu drobna zmiana współrzędnych nie przerzuca strefy ani czasu letniego i nie „przeskakuje" Ascendenta na sąsiedni znak. „Większa miejscowość" z wymagania jest zbędna: strefa IANA jest i tak regionalna, więc wioska daje tę samą strefę co pobliskie miasto. """ from __future__ import annotations from datetime import datetime from zoneinfo import ZoneInfo, ZoneInfoNotFoundError try: # zależność opcjonalna — brak biblioteki from tzfpy import get_tz # nie może wywalić strony, tylko wyłącza podpowiedź except Exception: # pragma: no cover get_tz = None def _fmt_offset(hours: float) -> str: sign = "+" if hours >= 0 else "−" total = int(round(abs(hours) * 60)) return f"{sign}{total // 60}:{total % 60:02d}" def resolve(lat: float, lon: float, date: str, time: str = "12:00") -> dict | None: """(lat, lon, data lokalna, godzina lokalna) → {tz, offset, dst, label}. Zwraca None, gdy strefy nie da się ustalić (brak biblioteki, punkt bez strefy, niepoprawna data) — wtedy zostaje ręczny offset, nic się nie psuje. `offset` jest w godzinach (np. 5.75 dla +5:45), DST-świadomy dla podanej daty. """ if get_tz is None: return None try: tz = get_tz(float(lon), float(lat)) # UWAGA: tzfpy przyjmuje (lon, lat) except Exception: return None if not tz: return None try: zone = ZoneInfo(tz) except (ZoneInfoNotFoundError, ValueError): return None # Wall-time w tej strefie dla podanej daty → offset i czy czas letni jest aktywny. try: local = datetime.fromisoformat(f"{date}T{time or '12:00'}") except ValueError: try: local = datetime.fromisoformat(date).replace(hour=12) except ValueError: return None local = local.replace(tzinfo=zone) off = local.utcoffset() if off is None: return None hours = round(off.total_seconds() / 3600.0, 2) dst = bool(local.dst()) label = f"{tz} · {_fmt_offset(hours)}" + (" (czas letni)" if dst else "") return {"tz": tz, "offset": hours, "dst": dst, "label": label}