feat(prezentacja): strefa czasowa z lokalizacji — DST-świadomy offset (PRE-03)
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 11m2s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m56s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 34s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 23s
build / build (push) Successful in 1m40s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 11m12s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m52s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 32s
Testy / Kontrola składni wszystkich warstw (push) Successful in 19s
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 11m2s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m56s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 34s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 23s
build / build (push) Successful in 1m40s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 11m12s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m52s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 32s
Testy / Kontrola składni wszystkich warstw (push) Successful in 19s
„Logika dwóch lokalizacji": dotąd offset GMT był ręcznym polem (PRE-19) — trzeba
było go znać i samemu pamiętać o czasie letnim. Teraz liczymy go z lokalizacji.
Sedno wymagania: strefę ustalamy RAZ i trzymamy jako stałą liczbę, żeby drobna
zmiana współrzędnych nie przerzuciła DST i nie „przeskoczyła" Ascendenta na
sąsiedni znak. „Większa miejscowość z bazy" okazuje się zbędna — strefa IANA jest
i tak regionalna, więc wioska daje tę samą strefę co pobliskie miasto.
Jak:
- `timezone.py`: współrzędne → strefa IANA (tzfpy, OFFLINE — bez sieci), a z niej
offset DLA DATY URODZENIA. `zoneinfo`/`tzdata` znają reguły historyczne i DST:
Kraków 1984 to +1h zimą, +2h latem; Katmandu +5:45. Degraduje się do None (brak
biblioteki / punkt bez strefy / zła data) — wtedy zostaje ręczny offset.
- Endpoint `GET /timezone?lat&lon&date&time` → {tz, offset, dst, label}. 404, gdy
nie da się ustalić.
- `geo.js`: po wyborze miejsca (mapa / wyszukiwarka / „Tu i teraz") oraz przy
zmianie DATY (bo DST zależy od pory roku) pobiera offset i wypełnia pole
tz_offset, pokazując wykrytą strefę („Wykryto: Europe/Warsaw · +2:00 (czas
letni)"). Pole zostaje edytowalne. Na wejściu podpowiada tylko gdy offset
wygląda na nieustawiony — nie nadpisuje wartości ręcznie wpisanej i wysłanej.
Zależności (lekkie, offline): tzfpy (wheel Rust) + tzdata (dla zoneinfo w slim-obrazie).
Weryfikacja: żywy serwer — Kraków 1984-06 → +2:00 (czas letni), 1984-01 → +1:00,
Katmandu → +5:45. Testy: +12 strefa (moduł + endpoint), +5 JS. Prezentacja 187.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit was merged in pull request #43.
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
"""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}
|
||||
Reference in New Issue
Block a user