Source code for icalendar.timezone.equivalent_timezone_ids
"""This module helps identifying the timezone ids and where they differ.
The algorithm: We use the tzname and the utcoffset for each hour from
1970 - 2030.
We make a big map.
If they are equivalent, they are equivalent within the time that is mostly used.
You can regenerate the information from this module.
See also:
- https://stackoverflow.com/questions/79171631/how-do-i-determine-whether-a-zoneinfo-is-an-alias/79171734#79171734
Run this module:
python -m icalendar.timezone.equivalent_timezone_ids
"""
from __future__ import annotations
from collections import defaultdict
from datetime import datetime, timedelta, tzinfo
from pathlib import Path
from typing import TYPE_CHECKING, NamedTuple
from zoneinfo import ZoneInfo, available_timezones
from pytz import AmbiguousTimeError, NonExistentTimeError
if TYPE_CHECKING:
from collections.abc import Callable
START = datetime(1970, 1, 1) # noqa: DTZ001
END = datetime(2020, 1, 1) # noqa: DTZ001
DISTANCE_FROM_TIMEZONE_CHANGE = timedelta(hours=12)
DTS = []
dt = START
while dt <= END:
DTS.append(dt)
# This must be big enough to be fast and small enough to identify the timeszones
# before it is the present year
dt += timedelta(hours=25)
del dt
[docs]
def main(
create_timezones: list[Callable[[str], tzinfo]],
name: str,
):
"""Generate a lookup table for timezone information if unknown timezones.
We cannot create one lookup for all because they seem to be all equivalent
if we mix timezone implementations.
"""
unsorted_tzids = available_timezones()
unsorted_tzids.remove("localtime")
unsorted_tzids.remove("Factory")
class TZ(NamedTuple):
tz: tzinfo
id: str
tzs = [
TZ(create_timezone(tzid), tzid)
for create_timezone in create_timezones
for tzid in unsorted_tzids
]
def generate_tree(
tzs: list[TZ],
step: timedelta = timedelta(hours=1),
start: datetime = START,
end: datetime = END,
todo: set[str] | None = None,
) -> tuple[datetime, dict[timedelta, set[str]]] | set[str]: # should be recursive
"""Generate a lookup tree."""
if todo is None:
todo = [tz.id for tz in tzs]
if len(tzs) == 0:
raise ValueError("tzs cannot be empty")
if len(tzs) == 1:
todo.remove(tzs[0].id)
return {tzs[0].id}
while start < end:
offsets: dict[timedelta, list[TZ]] = defaultdict(list)
try:
# if we are around a timezone change, we must move on
# see https://github.com/collective/icalendar/issues/776
around_tz_change = not all(
tz.tz.utcoffset(start)
== tz.tz.utcoffset(start - DISTANCE_FROM_TIMEZONE_CHANGE)
== tz.tz.utcoffset(start + DISTANCE_FROM_TIMEZONE_CHANGE)
for tz in tzs
)
except (NonExistentTimeError, AmbiguousTimeError):
around_tz_change = True
if around_tz_change:
start += DISTANCE_FROM_TIMEZONE_CHANGE
continue
for tz in tzs:
offsets[tz.tz.utcoffset(start)].append(tz)
if len(offsets) == 1:
start += step
continue
lookup = {}
for offset, tz2 in offsets.items():
lookup[offset] = generate_tree(
tzs=tz2, step=step, start=start + step, end=end, todo=todo
)
return start, lookup
result = set()
for tz in tzs:
result.add(tz.id)
todo.remove(tz.id)
return result
lookup = generate_tree(tzs, step=timedelta(hours=33))
file = Path(__file__).parent / f"equivalent_timezone_ids_{name}.py"
with file.open("w") as f:
f.write(
f"'''This file is automatically generated by {Path(__file__).name}'''\n"
)
f.write("import datetime\n\n")
f.write("\nlookup = ")
f.write("\n\n__all__ = ['lookup']\n")
return lookup
__all__ = ["main"]
if __name__ == "__main__":
from zoneinfo import ZoneInfo
from dateutil.tz import gettz
from pytz import timezone
# add more timezone implementations if you like
main(
[ZoneInfo, timezone, gettz],
"result",
)