Skip to content

Commit a47e1d9

Browse files
committed
Timezone fixes.
1 parent d30270e commit a47e1d9

14 files changed

Lines changed: 126 additions & 100 deletions

File tree

docs/3-examples.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ native = charts.Subject(
2424

2525
The optional `time_is_dst` parameter is a boolean to clarify ambiguous times - for example 1:30am on a night when switching to US daylight savings could be in either the standard or the DST timezone. To specify which one, pass either `True` or `False` to this argument. For all other non-ambiguous times, `time_is_dst` can safely be omitted.
2626

27+
You may provide a string argument to the optional `timezone` parameter if you'd like to bypass the coordinate lookup. This will speed up chart generation and could potentially be more accurate if your own timezone data is more up to date than Immanuel's offline lookup.
28+
2729
There is also an optional `timezone_offset` parameter. This is a float which explicitly sets the UTC offset in hours for the location specified by the passed coordinates. While Immanuel will do its best to look up the correct timezone for the coordinates, it might not always be accurate, especially when boundaries or offsets have changed since the given birth date. This parameter can safely be ignored for most use cases.
2830

2931
To generate one of each type of supported chart, you could do the following:
@@ -117,7 +119,7 @@ print(natal.native.coordinates.latitude.raw)
117119
Will look like this:
118120

119121
```
120-
Sat Jan 01 2000 10:00:00 AM PST at 32N43.0, 117W9.0
122+
Sat Jan 01 2000 10:00:00 AM America/Los_Angeles at 32N43.0, 117W9.0
121123
32N43.0, 117W9.0
122124
32N43.0
123125
32.71666666666667
@@ -145,7 +147,7 @@ This will output the following JSON:
145147
{
146148
"date_time": {
147149
"datetime": "2000-01-01 10:00:00-08:00",
148-
"timezone": "PST",
150+
"timezone": "America/Los_Angeles",
149151
"ambiguous": false,
150152
"julian": 2451545.25,
151153
"deltat": 0.0007387629899254968,

docs/4-data.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ The chart native, based on the date and coordinates you will have passed in when
1616
{
1717
"date_time": {
1818
"datetime": "2000-01-01 10:00:00-08:00",
19-
"timezone": "PST",
19+
"timezone": "America/Los_Angeles",
2020
"ambiguous": false,
2121
"julian": 2451545.25,
2222
"deltat": 0.0007387629899254968,

immanuel/charts.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,17 +46,19 @@ def __init__(
4646
latitude: float | list | tuple | str,
4747
longitude: float | list | tuple | str,
4848
timezone_offset: float | None = None,
49+
timezone: str | None = None,
4950
time_is_dst: bool | None = None,
5051
) -> None:
51-
self.latitude, self.longitude = (
52-
convert.to_dec(v) for v in (latitude, longitude)
53-
)
52+
self.latitude, self.longitude = convert.coordinates(latitude, longitude)
53+
self.timezone_offset = timezone_offset
54+
self.timezone = timezone
5455
self.time_is_dst = time_is_dst
5556
self.date_time = date.to_datetime(
5657
dt=date_time,
5758
lat=self.latitude,
5859
lon=self.longitude,
5960
offset=timezone_offset,
61+
time_zone=timezone,
6062
is_dst=time_is_dst,
6163
)
6264
self.date_time_ambiguous = (
@@ -169,6 +171,8 @@ def set_wrapped_objects(self) -> None:
169171
dt=object["jd"],
170172
lat=self._native.latitude,
171173
lon=self._native.longitude,
174+
offset=self._native.timezone_offset,
175+
time_zone=self._native.timezone,
172176
)
173177
if "jd" in object
174178
else None
@@ -326,6 +330,8 @@ def set_wrapped_solar_return_date_time(self) -> None:
326330
armc=self._solar_return_armc,
327331
latitude=self._native.latitude,
328332
longitude=self._native.longitude,
333+
offset=self._native.timezone_offset,
334+
timezone=self._native.timezone,
329335
)
330336

331337

@@ -348,6 +354,8 @@ def generate(self) -> None:
348354
dt=self._date_time,
349355
lat=self._native.latitude,
350356
lon=self._native.longitude,
357+
offset=self._native.timezone_offset,
358+
time_zone=self._native.timezone,
351359
)
352360
progression_jd = date.to_jd(self._progression_date_time)
353361
progression_armc = ephemeris.get_angle(
@@ -414,6 +422,8 @@ def set_wrapped_progressed_date_time(self) -> None:
414422
armc=self._progressed_armc_longitude,
415423
latitude=self._native.latitude,
416424
longitude=self._native.longitude,
425+
offset=self._native.timezone_offset,
426+
timezone=self._native.timezone,
417427
)
418428

419429
def set_wrapped_progression_method(self) -> None:
@@ -560,13 +570,14 @@ def __init__(
560570
self,
561571
latitude: float | list | tuple | str = settings.default_latitude,
562572
longitude: float | list | tuple | str = settings.default_longitude,
573+
offset: float | None = None,
574+
timezone: str | None = None,
563575
aspects_to: Chart | None = None,
564576
houses_for_aspected: bool = False,
565577
) -> None:
566-
lat, lon = (convert.to_dec(v) for v in (latitude, longitude))
567-
timezone = date.timezone_name(lat, lon)
568-
date_time = datetime.now(tz=ZoneInfo(timezone))
569-
self._native = Subject(date_time, lat, lon)
578+
lat, lon = convert.coordinates(latitude, longitude)
579+
date_time = date.localize(datetime.now(), lat, lon, offset, timezone)
580+
self._native = Subject(date_time, lat, lon, offset, timezone)
570581
self._houses_for_aspected = houses_for_aspected
571582
super().__init__(chart.TRANSITS, aspects_to)
572583

immanuel/classes/wrap.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,10 +120,12 @@ def __init__(
120120
armc: dict | float = None,
121121
latitude: float | None = None,
122122
longitude: float | None = None,
123+
offset: float | None = None,
124+
timezone: str | None = None,
123125
time_is_dst: bool | None = None,
124126
) -> None:
125-
self.datetime = date.to_datetime(dt, latitude, longitude)
126-
self.timezone = self.datetime.tzname()
127+
self.datetime = date.to_datetime(dt, latitude, longitude, offset, timezone)
128+
self.timezone = date.timezone_name(self.datetime)
127129
self.ambiguous = date.ambiguous(self.datetime) and time_is_dst is None
128130
self.julian = date.to_jd(dt)
129131
self.deltat = ephemeris.get_deltat(self.julian)
@@ -351,6 +353,8 @@ def __init__(self, subject: "Subject") -> None:
351353
armc=armc,
352354
latitude=subject.latitude,
353355
longitude=subject.longitude,
356+
offset=subject.timezone_offset,
357+
timezone=subject.timezone,
354358
time_is_dst=subject.time_is_dst,
355359
)
356360
self.coordinates = Coordinates(

immanuel/locales/de_DE/LC_MESSAGES/immanuel.po

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -544,7 +544,7 @@ msgstr "Tägliche Häuser"
544544

545545
# Formatted sentences & words
546546

547-
# For datetimes, eg. Sun Nov 06 2022 01:30:00 AM PDT (ambiguous)
547+
# For datetimes, eg. Sun Nov 06 2022 01:30:00 AM America/Los_Angeles (ambiguous)
548548
msgid "ambiguous"
549549
msgstr "mehrdeutig"
550550

@@ -557,7 +557,7 @@ msgstr "{active} {passive} {type} innerhalb von {difference} ({movement}, {condi
557557
msgid "{name} {longitude} in {sign}"
558558
msgstr "{name} {longitude} in {sign}"
559559

560-
# For chart subjects, eg. Sat Jan 01 2000 10:00:00 AM PST at 32N43.0, 117W9.0
560+
# For chart subjects, eg. Sat Jan 01 2000 10:00:00 AM America/Los_Angeles at 32N43.0, 117W9.0
561561
msgid "{date_time} at {lat}, {lon}"
562562
msgstr "{date_time} um {lat}, {lon}" ""
563563

immanuel/locales/es_ES/LC_MESSAGES/immanuel.po

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -603,7 +603,7 @@ msgstr "Casas Diarias"
603603

604604
# Formatted sentences & words
605605

606-
# For datetimes, eg. Sun Nov 06 2022 01:30:00 AM PDT (ambiguous)
606+
# For datetimes, eg. Sun Nov 06 2022 01:30:00 AM America/Los_Angeles (ambiguous)
607607
msgid "ambiguous"
608608
msgstr "ambíguo"
609609

@@ -615,7 +615,7 @@ msgstr "{type} entre {active} y {passive} dentro de {difference} ({movement}, {c
615615
msgid "{name} {longitude} in {sign}"
616616
msgstr "{name} {longitude} en {sign}"
617617

618-
# For chart subjects, eg. Sat Jan 01 2000 10:00:00 AM PST at 32N43.0, 117W9.0
618+
# For chart subjects, eg. Sat Jan 01 2000 10:00:00 AM America/Los_Angeles at 32N43.0, 117W9.0
619619
msgid "{date_time} at {lat}, {lon}"
620620
msgstr "{date_time} en {lat}, {lon}"
621621

immanuel/locales/immanuel.pot

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -536,7 +536,7 @@ msgstr ""
536536

537537
# Formatted sentences & words
538538

539-
# For datetimes, eg. Sun Nov 06 2022 01:30:00 AM PDT (ambiguous)
539+
# For datetimes, eg. Sun Nov 06 2022 01:30:00 AM America/Los_Angeles (ambiguous)
540540
msgid "ambiguous"
541541
msgstr ""
542542

@@ -548,7 +548,7 @@ msgstr ""
548548
msgid "{name} {longitude} in {sign}"
549549
msgstr ""
550550

551-
# For chart subjects, eg. Sat Jan 01 2000 10:00:00 AM PST at 32N43.0, 117W9.0
551+
# For chart subjects, eg. Sat Jan 01 2000 10:00:00 AM America/Los_Angeles at 32N43.0, 117W9.0
552552
msgid "{date_time} at {lat}, {lon}"
553553
msgstr ""
554554

immanuel/locales/pt_BR/LC_MESSAGES/immanuel.po

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -603,7 +603,7 @@ msgstr "Casas Diárias"
603603

604604
# Formatted sentences & words
605605

606-
# For datetimes, eg. Sun Nov 06 2022 01:30:00 AM PDT (ambiguous)
606+
# For datetimes, eg. Sun Nov 06 2022 01:30:00 AM America/Los_Angeles (ambiguous)
607607
msgid "ambiguous"
608608
msgstr "ambíguo"
609609

@@ -615,7 +615,7 @@ msgstr "{type} entre {active} e {passive} dentro de {difference} ({movement}, {c
615615
msgid "{name} {longitude} in {sign}"
616616
msgstr "{name} {longitude} em {sign}"
617617

618-
# For chart subjects, eg. Sat Jan 01 2000 10:00:00 AM PST at 32N43.0, 117W9.0
618+
# For chart subjects, eg. Sat Jan 01 2000 10:00:00 AM America/Los_Angeles at 32N43.0, 117W9.0
619619
msgid "{date_time} at {lat}, {lon}"
620620
msgstr "{date_time} em {lat}, {lon}"
621621

immanuel/tools/convert.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ def string_to_dec(string: str) -> float:
9999
return dms_to_dec(["-" if char in "SW" or floats[0] < 0 else "+", *floats])
100100

101101

102-
def to_dec(value: float | list | tuple | str) -> float:
102+
def to_dec(value: float | list | tuple | str) -> float | None:
103103
"""If the input type is unknown, this will guess and convert."""
104104
if isinstance(value, float):
105105
return value
@@ -115,7 +115,7 @@ def to_dec(value: float | list | tuple | str) -> float:
115115

116116
def to_dms(
117117
value: float | list | tuple | str, round_to: tuple = ROUND_SECOND, pad_rounded=False
118-
) -> tuple:
118+
) -> tuple | None:
119119
"""If the input type is unknown, this will guess and convert."""
120120
if isinstance(value, float):
121121
return dec_to_dms(value, round_to, pad_rounded)
@@ -134,7 +134,7 @@ def to_string(
134134
format: int = FORMAT_DMS,
135135
round_to: tuple = ROUND_SECOND,
136136
pad_rounded: bool | None = None,
137-
) -> str:
137+
) -> str | None:
138138
"""If the input type is unknown, this will guess and convert."""
139139
if isinstance(value, float):
140140
return dec_to_string(value, format, round_to, pad_rounded)
@@ -148,6 +148,13 @@ def to_string(
148148
return None
149149

150150

151+
def coordinates(
152+
lat: float | list | tuple | str, lon: float | list | tuple | str
153+
) -> tuple | None:
154+
"""Shortcut function to convert latitude and longitude to decimal."""
155+
return to_dec(lat), to_dec(lon)
156+
157+
151158
def _dms_to_string_format_dms(dms: list | tuple) -> str:
152159
"""Returns DMS in degree/minute/second format."""
153160
symbols = ("\N{DEGREE SIGN}", "'", '"')

immanuel/tools/date.py

Lines changed: 52 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
1515
"""
1616

17-
from datetime import datetime, timedelta, timezone
17+
from datetime import datetime, timedelta, timezone, tzinfo
1818
from zoneinfo import ZoneInfo
1919

2020
import swisseph as swe
@@ -23,44 +23,6 @@
2323
from immanuel.tools import convert
2424

2525

26-
def ambiguous(dt: datetime) -> bool:
27-
"""Returns whether an aware datetime is ambiguous."""
28-
return tz.datetime_ambiguous(dt)
29-
30-
31-
def timezone_name(lat: float, lon: float) -> str:
32-
"""Returns a timezone string based on decimal lat/lon coordinates."""
33-
from timezonefinder import TimezoneFinder
34-
return TimezoneFinder().timezone_at(lat=lat, lng=lon)
35-
36-
37-
def get_timezone(lat: float | None, lon: float | None, offset: float | None, time_zone: str | None) -> ZoneInfo | timezone | None:
38-
"""Returns a timezone object based on either decimal lat/lon
39-
coordinates or an explicit UTC offset."""
40-
if time_zone is not None:
41-
return ZoneInfo(time_zone)
42-
if offset is not None:
43-
return timezone(timedelta(hours=offset))
44-
if lat is not None and lon is not None:
45-
return ZoneInfo(timezone_name(lat, lon))
46-
return None
47-
48-
49-
def localize(
50-
dt: datetime,
51-
lat: float | None = None,
52-
lon: float | None = None,
53-
offset: float | None = None,
54-
time_zone: str | None = None,
55-
is_dst: bool | None = None,
56-
) -> datetime:
57-
"""Localizes a naive datetime based on either decimal lat/lon
58-
coordinates or an explicit UTC offset."""
59-
return dt.replace(
60-
tzinfo=get_timezone(lat, lon, offset, time_zone), fold=1 if is_dst is False else 0
61-
)
62-
63-
6426
def to_datetime(
6527
dt: str | float | datetime,
6628
lat: float | None = None,
@@ -84,7 +46,9 @@ def to_datetime(
8446
time = convert.dec_to_dms(ut[3])[1:]
8547
date_time = datetime(*ut[:3], *time, tzinfo=ZoneInfo("UTC"))
8648
return (
87-
date_time if no_tz else date_time.astimezone(get_timezone(lat, lon, offset, time_zone))
49+
date_time
50+
if no_tz
51+
else date_time.astimezone(get_timezone(lat, lon, offset, time_zone))
8852
)
8953
if isinstance(dt, datetime):
9054
if no_tz:
@@ -123,3 +87,51 @@ def to_jd(
12387
("+", date_time_utc.hour, date_time_utc.minute, date_time_utc.second)
12488
)
12589
return swe.julday(*date_time_utc.timetuple()[0:3], hour)
90+
91+
92+
def localize(
93+
dt: datetime,
94+
lat: float | None = None,
95+
lon: float | None = None,
96+
offset: float | None = None,
97+
time_zone: str | None = None,
98+
is_dst: bool | None = None,
99+
) -> datetime:
100+
"""Localizes a naive datetime based on either decimal lat/lon
101+
coordinates or an explicit UTC offset."""
102+
return dt.replace(
103+
tzinfo=get_timezone(lat, lon, offset, time_zone),
104+
fold=1 if is_dst is False else 0,
105+
)
106+
107+
108+
def get_timezone(
109+
lat: float | None, lon: float | None, offset: float | None, time_zone: str | None
110+
) -> tzinfo | None:
111+
"""Returns a timezone object based on either decimal lat/lon
112+
coordinates or an explicit UTC offset."""
113+
if time_zone is not None:
114+
return ZoneInfo(time_zone)
115+
if offset is not None:
116+
return timezone(timedelta(hours=offset))
117+
if lat is not None and lon is not None:
118+
return ZoneInfo(timezone_lookup(lat, lon))
119+
return None
120+
121+
122+
def timezone_lookup(lat: float, lon: float) -> str:
123+
"""Returns a timezone string based on decimal lat/lon coordinates."""
124+
print("Looking up timezone")
125+
from timezonefinder import TimezoneFinder
126+
127+
return TimezoneFinder().timezone_at(lat=lat, lng=lon)
128+
129+
130+
def timezone_name(dt: datetime) -> str | None:
131+
"""Returns a timezone name based on a datetime object."""
132+
return str(dt.tzinfo) if dt.tzinfo is not None else None
133+
134+
135+
def ambiguous(dt: datetime) -> bool:
136+
"""Returns whether an aware datetime is ambiguous."""
137+
return tz.datetime_ambiguous(dt)

0 commit comments

Comments
 (0)