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