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