1"""This module identifies timezones.
2
3Normally, timezones have ids.
4This is a way to access the ids if you have a
5datetime.tzinfo object.
6"""
7
8from __future__ import annotations
9
10from collections import defaultdict
11from datetime import date, time, timezone
12from pathlib import Path
13from typing import TYPE_CHECKING
14
15from dateutil.tz import tz
16
17from icalendar.timezone import equivalent_timezone_ids_result
18from icalendar.timezone.windows_to_olson import WINDOWS_TO_OLSON
19from icalendar.tools import is_date
20
21try:
22 from dateutil.tz.win import tzwin as _tzwin
23except ImportError:
24 _tzwin = None # not on Windows
25
26if TYPE_CHECKING:
27 from datetime import datetime, tzinfo
28
29DATEUTIL_UTC = tz.gettz("UTC")
30DATEUTIL_UTC_PATH: str | None = getattr(DATEUTIL_UTC, "_filename", None)
31DATEUTIL_ZONEINFO_PATH = (
32 None if DATEUTIL_UTC_PATH is None else Path(DATEUTIL_UTC_PATH).parent
33)
34
35
36def tzids_from_tzinfo(tzinfo: tzinfo | None) -> tuple[str]:
37 """Get several timezone ids if we can identify the timezone.
38
39 >>> import zoneinfo
40 >>> from icalendar.timezone.tzid import tzids_from_tzinfo
41 >>> tzids_from_tzinfo(zoneinfo.ZoneInfo("Arctic/Longyearbyen"))
42 ('Arctic/Longyearbyen', 'Atlantic/Jan_Mayen', 'Europe/Berlin', 'Europe/Budapest', 'Europe/Copenhagen', 'Europe/Oslo', 'Europe/Stockholm', 'Europe/Vienna')
43 >>> from dateutil.tz import gettz
44 >>> tzids_from_tzinfo(gettz("Europe/Berlin"))
45 ('Europe/Berlin', 'Arctic/Longyearbyen', 'Atlantic/Jan_Mayen', 'Europe/Budapest', 'Europe/Copenhagen', 'Europe/Oslo', 'Europe/Stockholm', 'Europe/Vienna')
46
47 """ # The example might need to change if you recreate the lookup tree # noqa: E501
48 if tzinfo is None:
49 return ()
50 if isinstance(tzinfo, timezone):
51 # fixed offset timezone with name
52 return get_equivalent_tzids(tzinfo.tzname(None))
53 if hasattr(tzinfo, "zone"):
54 return get_equivalent_tzids(tzinfo.zone) # pytz implementation
55 if hasattr(tzinfo, "key"):
56 return get_equivalent_tzids(tzinfo.key) # ZoneInfo implementation
57 if isinstance(tzinfo, tz._tzicalvtz): # noqa: SLF001
58 return get_equivalent_tzids(tzinfo._tzid) # noqa: SLF001
59 if isinstance(tzinfo, tz.tzstr):
60 return get_equivalent_tzids(tzinfo._s) # noqa: SLF001
61 if _tzwin is not None and isinstance(tzinfo, _tzwin):
62 olson = WINDOWS_TO_OLSON.get(tzinfo._name) # noqa: SLF001
63 if olson is not None:
64 return get_equivalent_tzids(olson)
65 return get_equivalent_tzids(tzinfo._name) # noqa: SLF001
66 if hasattr(tzinfo, "_filename"): # dateutil.tz.tzfile # noqa: SIM102
67 if DATEUTIL_ZONEINFO_PATH is not None:
68 # tzfile('/usr/share/zoneinfo/Europe/Berlin')
69 path = tzinfo._filename # noqa: SLF001
70 if path.startswith(str(DATEUTIL_ZONEINFO_PATH)):
71 tzid = str(Path(path).relative_to(DATEUTIL_ZONEINFO_PATH))
72 return get_equivalent_tzids(tzid)
73 return get_equivalent_tzids(path)
74 if isinstance(tzinfo, tz.tzutc):
75 return get_equivalent_tzids("UTC")
76 return ()
77
78
79def tzid_from_tzinfo(tzinfo: tzinfo | None) -> str | None:
80 """Retrieve the timezone id from the tzinfo object.
81
82 Some timezones are equivalent.
83 Thus, we might return one ID that is equivalent to others.
84 """
85 tzids = tzids_from_tzinfo(tzinfo)
86 if "UTC" in tzids:
87 return "UTC"
88 if not tzids:
89 return None
90 return tzids[0]
91
92
93def tzid_from_dt(dt: datetime) -> str | None:
94 """Retrieve the timezone id from the datetime object."""
95 tzid = tzid_from_tzinfo(dt.tzinfo)
96 if tzid is None:
97 return dt.tzname()
98 return tzid
99
100
101_EQUIVALENT_IDS: dict[str, set[str]] = defaultdict(set)
102
103
104def _add_equivalent_ids(value: tuple | dict | set):
105 """This adds equivalent ids/
106
107 As soon as one timezone implementation used claims their equivalence,
108 they are considered equivalent.
109 Have a look at icalendar.timezone.equivalent_timezone_ids.
110 """
111 if isinstance(value, set):
112 for tzid in value:
113 _EQUIVALENT_IDS[tzid].update(value)
114 elif isinstance(value, tuple):
115 _add_equivalent_ids(value[1])
116 elif isinstance(value, dict):
117 for value2 in value.values():
118 _add_equivalent_ids(value2)
119 else:
120 raise TypeError(
121 f"Expected tuple, dict or set, not {value.__class__.__name__}: {value!r}"
122 )
123
124
125_add_equivalent_ids(equivalent_timezone_ids_result.lookup)
126
127
128def get_equivalent_tzids(tzid: str) -> tuple[str]:
129 """This returns the tzids which are equivalent to this one."""
130 ids = _EQUIVALENT_IDS.get(tzid, set())
131 return (tzid,) + tuple(sorted(ids - {tzid}))
132
133
134def is_utc(t: datetime | time | date | tzinfo) -> bool:
135 """Whether this date is in UTC."""
136 if is_date(t):
137 return False
138 tzid = tzid_from_dt(t) if hasattr(t, "tzinfo") else tzid_from_tzinfo(t)
139 return tzid == "UTC"
140
141
142__all__ = ["is_utc", "tzid_from_dt", "tzid_from_tzinfo", "tzids_from_tzinfo"]