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/79185519/which-timezones-are-equivalent

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 pprint import pprint
from typing import Callable, NamedTuple, Optional

from pytz import AmbiguousTimeError, NonExistentTimeError
from zoneinfo import ZoneInfo, available_timezones

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)
    dt += timedelta(
        hours=25
    )  # This must be big enough to be fast and small enough to identify the timeszones before it is the present year
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. """ print(create_timezones, name) 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: Optional[set[str]] = 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] print(f"{len(todo)} left to compute") print(len(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, tzs in offsets.items(): lookup[offset] = generate_tree( tzs=tzs, step=step, start=start + step, end=end, todo=todo ) return start, lookup print(f"reached end with {len(tzs)} timezones - assuming they are equivalent.") 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" print(f"The result is written to {file}.") print("lookup = ", end="") pprint(lookup) 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 = ") pprint(lookup, stream=f) f.write("\n\n__all__ = ['lookup']\n") return lookup
__all__ = ["main"] if __name__ == "__main__": from dateutil.tz import gettz from pytz import timezone from zoneinfo import ZoneInfo # add more timezone implementations if you like main( [ZoneInfo, timezone, gettz], "result", )