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", )