1"""UTC-Offset property type from :rfc:`5545`."""
2
3import re
4from datetime import timedelta
5from typing import Any, ClassVar
6
7from icalendar.compatibility import Self
8from icalendar.error import JCalParsingError
9from icalendar.parser import Parameters
10
11UTC_OFFSET_JCAL_REGEX = re.compile(
12 r"^(?P<sign>[+-])?(?P<hours>\d\d):(?P<minutes>\d\d)(?::(?P<seconds>\d\d))?$"
13)
14
15
16class vUTCOffset:
17 """UTC Offset
18
19 Value Name:
20 UTC-OFFSET
21
22 Purpose:
23 This value type is used to identify properties that contain
24 an offset from UTC to local time.
25
26 Format Definition:
27 This value type is defined by the following notation:
28
29 .. code-block:: text
30
31 utc-offset = time-numzone
32
33 time-numzone = ("+" / "-") time-hour time-minute [time-second]
34
35 Description:
36 The PLUS SIGN character MUST be specified for positive
37 UTC offsets (i.e., ahead of UTC). The HYPHEN-MINUS character MUST
38 be specified for negative UTC offsets (i.e., behind of UTC). The
39 value of "-0000" and "-000000" are not allowed. The time-second,
40 if present, MUST NOT be 60; if absent, it defaults to zero.
41
42 Example:
43 The following UTC offsets are given for standard time for
44 New York (five hours behind UTC) and Geneva (one hour ahead of
45 UTC):
46
47 .. code-block:: text
48
49 -0500
50
51 +0100
52
53 .. code-block:: pycon
54
55 >>> from icalendar.prop import vUTCOffset
56 >>> utc_offset = vUTCOffset.from_ical('-0500')
57 >>> utc_offset
58 datetime.timedelta(days=-1, seconds=68400)
59 >>> utc_offset = vUTCOffset.from_ical('+0100')
60 >>> utc_offset
61 datetime.timedelta(seconds=3600)
62 """
63
64 default_value: ClassVar[str] = "UTC-OFFSET"
65 params: Parameters
66
67 ignore_exceptions = False # if True, and we cannot parse this
68
69 # component, we will silently ignore
70 # it, rather than let the exception
71 # propagate upwards
72
73 def __init__(self, td: timedelta, /, params: dict[str, Any] | None = None):
74 if not isinstance(td, timedelta):
75 raise TypeError("Offset value MUST be a timedelta instance")
76 self.td = td
77 self.params = Parameters(params)
78
79 def to_ical(self) -> str:
80 """Return the ical representation."""
81 return self.format("")
82
83 def format(self, divider: str = "") -> str:
84 """Represent the value with a possible divider.
85
86 .. code-block:: pycon
87
88 >>> from icalendar import vUTCOffset
89 >>> from datetime import timedelta
90 >>> utc_offset = vUTCOffset(timedelta(hours=-5))
91 >>> utc_offset.format()
92 '-0500'
93 >>> utc_offset.format(divider=':')
94 '-05:00'
95 """
96 if self.td < timedelta(0):
97 sign = "-%s"
98 td = timedelta(0) - self.td # get timedelta relative to 0
99 else:
100 # Google Calendar rejects '0000' but accepts '+0000'
101 sign = "+%s"
102 td = self.td
103
104 days, seconds = td.days, td.seconds
105
106 hours = abs(days * 24 + seconds // 3600)
107 minutes = abs((seconds % 3600) // 60)
108 seconds = abs(seconds % 60)
109 if seconds:
110 duration = f"{hours:02}{divider}{minutes:02}{divider}{seconds:02}"
111 else:
112 duration = f"{hours:02}{divider}{minutes:02}"
113 return sign % duration
114
115 @classmethod
116 def from_ical(cls, ical):
117 if isinstance(ical, cls):
118 return ical.td
119 try:
120 sign, hours, minutes, seconds = (
121 ical[0:1],
122 int(ical[1:3]),
123 int(ical[3:5]),
124 int(ical[5:7] or 0),
125 )
126 offset = timedelta(hours=hours, minutes=minutes, seconds=seconds)
127 except Exception as e:
128 raise ValueError(f"Expected UTC offset, got: {ical}") from e
129 if not cls.ignore_exceptions and offset >= timedelta(hours=24):
130 raise ValueError(f"Offset must be less than 24 hours, was {ical}")
131 if sign == "-":
132 return -offset
133 return offset
134
135 def __eq__(self, other):
136 if not isinstance(other, vUTCOffset):
137 return False
138 return self.td == other.td
139
140 def __hash__(self):
141 return hash(self.td)
142
143 def __repr__(self):
144 return f"vUTCOffset({self.td!r})"
145
146 @classmethod
147 def examples(cls) -> list[Self]:
148 """Examples of vUTCOffset."""
149 return [
150 cls(timedelta(hours=3)),
151 cls(timedelta(0)),
152 ]
153
154 from icalendar.param import VALUE
155
156 def to_jcal(self, name: str) -> list:
157 """The jCal representation of this property according to :rfc:`7265`."""
158 return [name, self.params.to_jcal(), self.VALUE.lower(), self.format(":")]
159
160 @classmethod
161 def from_jcal(cls, jcal_property: list) -> Self:
162 """Parse jCal from :rfc:`7265`.
163
164 Parameters:
165 jcal_property: The jCal property to parse.
166
167 Raises:
168 ~error.JCalParsingError: If the provided jCal is invalid.
169 """
170 JCalParsingError.validate_property(jcal_property, cls)
171 match = UTC_OFFSET_JCAL_REGEX.match(jcal_property[3])
172 if match is None:
173 raise JCalParsingError(f"Cannot parse {jcal_property!r} as UTC-OFFSET.")
174 negative = match.group("sign") == "-"
175 hours = int(match.group("hours"))
176 minutes = int(match.group("minutes"))
177 seconds = int(match.group("seconds") or 0)
178 t = timedelta(hours=hours, minutes=minutes, seconds=seconds)
179 if negative:
180 t = -t
181 return cls(t, Parameters.from_jcal_property(jcal_property))
182
183
184__all__ = ["vUTCOffset"]