1"""TIME property type from :rfc:`5545`."""
2
3import re
4from datetime import datetime, time, timezone, tzinfo
5from typing import Any, ClassVar
6from xml.etree.ElementTree import Element, SubElement
7
8from icalendar.compatibility import Self
9from icalendar.error import ICalParsingError, JCalParsingError, XCalParsingError
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
14from icalendar.timezone import tzp
15from icalendar.timezone.tzid import is_utc
16
17from .base import TimeBase
18
19TIME_JCAL_REGEX = re.compile(
20 r"^(?P<hour>[0-9]{2}):(?P<minute>[0-9]{2}):(?P<second>[0-9]{2})(?P<utc>Z)?\Z"
21)
22
23XCAL_TIME_REGEX = XCalRegexMatcher(
24 TIME_JCAL_REGEX,
25 "Expected time format HH:MM:SS or HH:MM:SSZ",
26)
27
28
29class vTime(TimeBase):
30 """Time
31
32 Value Name:
33 TIME
34
35 Purpose:
36 This value type is used to identify values that contain a
37 time of day.
38
39 Format Definition:
40 This value type is defined by the following notation:
41
42 .. code-block:: text
43
44 time = time-hour time-minute time-second [time-utc]
45
46 time-hour = 2DIGIT ;00-23
47 time-minute = 2DIGIT ;00-59
48 time-second = 2DIGIT ;00-60
49 ;The "60" value is used to account for positive "leap" seconds.
50
51 time-utc = "Z"
52
53 Description:
54 If the property permits, multiple "time" values are
55 specified by a COMMA-separated list of values. No additional
56 content value encoding (i.e., BACKSLASH character encoding, see
57 vText) is defined for this value type.
58
59 The "TIME" value type is used to identify values that contain a
60 time of day. The format is based on the [ISO.8601.2004] complete
61 representation, basic format for a time of day. The text format
62 consists of a two-digit, 24-hour of the day (i.e., values 00-23),
63 two-digit minute in the hour (i.e., values 00-59), and two-digit
64 seconds in the minute (i.e., values 00-60). The seconds value of
65 60 MUST only be used to account for positive "leap" seconds.
66 Fractions of a second are not supported by this format.
67
68 In parallel to the "DATE-TIME" definition above, the "TIME" value
69 type expresses time values in three forms:
70
71 The form of time with UTC offset MUST NOT be used. For example,
72 the following is not valid for a time value:
73
74 .. code-block:: ics
75
76 230000-0800 ;Invalid time format
77
78 **FORM #1 LOCAL TIME**
79
80 The local time form is simply a time value that does not contain
81 the UTC designator nor does it reference a time zone. For
82 example, 11:00 PM:
83
84 .. code-block:: ics
85
86 230000
87
88 Time values of this type are said to be "floating" and are not
89 bound to any time zone in particular. They are used to represent
90 the same hour, minute, and second value regardless of which time
91 zone is currently being observed. For example, an event can be
92 defined that indicates that an individual will be busy from 11:00
93 AM to 1:00 PM every day, no matter which time zone the person is
94 in. In these cases, a local time can be specified. The recipient
95 of an iCalendar object with a property value consisting of a local
96 time, without any relative time zone information, SHOULD interpret
97 the value as being fixed to whatever time zone the "ATTENDEE" is
98 in at any given moment. This means that two "Attendees", may
99 participate in the same event at different UTC times; floating
100 time SHOULD only be used where that is reasonable behavior.
101
102 In most cases, a fixed time is desired. To properly communicate a
103 fixed time in a property value, either UTC time or local time with
104 time zone reference MUST be specified.
105
106 The use of local time in a TIME value without the "TZID" property
107 parameter is to be interpreted as floating time, regardless of the
108 existence of "VTIMEZONE" calendar components in the iCalendar
109 object.
110
111 **FORM #2: UTC TIME**
112
113 UTC time, or absolute time, is identified by a LATIN CAPITAL
114 LETTER Z suffix character, the UTC designator, appended to the
115 time value. For example, the following represents 07:00 AM UTC:
116
117 .. code-block:: ics
118
119 070000Z
120
121 The "TZID" property parameter MUST NOT be applied to TIME
122 properties whose time values are specified in UTC.
123
124 **FORM #3: LOCAL TIME AND TIME ZONE REFERENCE**
125
126 The local time with reference to time zone information form is
127 identified by the use the "TZID" property parameter to reference
128 the appropriate time zone definition.
129
130 Example:
131 The following represents 8:30 AM in New York in winter,
132 five hours behind UTC, in each of the three formats:
133
134 .. code-block:: ics
135
136 083000
137 133000Z
138 TZID=America/New_York:083000
139 """
140
141 default_value: ClassVar[str] = "TIME"
142 params: Parameters
143
144 def __init__(self, *args: Any, params: dict[str, Any] | None = None) -> None:
145 if len(args) == 1:
146 if not isinstance(args[0], (time, datetime)):
147 raise ValueError(f"Expected a datetime.time, got: {args[0]}")
148 self.dt = args[0]
149 else:
150 self.dt = time(*args)
151 self.params = Parameters(params or {})
152 self.params.update_tzid_from(self.dt)
153
154 def to_ical(self):
155 value = self.dt.strftime("%H%M%S")
156 if self.is_utc():
157 value += "Z"
158 return value
159
160 def is_utc(self) -> bool:
161 """Whether this time is UTC."""
162 return self.params.is_utc() or is_utc(self.dt)
163
164 @staticmethod
165 def from_ical(ical: str, timezone: str | None | tzinfo = None) -> time:
166 """Convert an ical string into a time.
167
168 This parses the time forms defined in :rfc:`5545#section-3.3.12`:
169
170 - Local time (floating)
171 - UTC time
172 - Local time with time zone reference
173
174 Returns:
175 A :class:`datetime.time` object representing the parsed time, with
176 timezone information if applicable.
177
178 Raises:
179 ~icalendar.error.ICalParsingError: If the time cannot be parsed.
180 """
181 tzinfo = None
182 if isinstance(timezone, str):
183 tzinfo = tzp.timezone(timezone)
184 elif timezone is not None:
185 tzinfo = timezone
186
187 if isinstance(ical, bytes):
188 ical = ical.decode()
189
190 original_ical = ical
191
192 # Extract the value part if parameters are present per
193 # https://datatracker.ietf.org/doc/html/rfc5545.html#section-3.3.5
194 # Form #3: TZID=America/New_York:083000
195 ical = ical.rpartition(":")[2]
196
197 if utc := ical.endswith("Z"):
198 ical = ical[:-1]
199
200 # time = time-hour time-minute time-second [time-utc], six digits,
201 # per https://datatracker.ietf.org/doc/html/rfc5545#section-3.3.12
202 if len(ical) != 6 or not ical.isascii() or not ical.isdigit():
203 raise ICalParsingError(
204 "Expected time",
205 value=original_ical,
206 )
207
208 try:
209 result = time(int(ical[:2]), int(ical[2:4]), int(ical[4:6]))
210 except Exception as e:
211 raise ICalParsingError(
212 "Expected time",
213 value=original_ical,
214 ) from e
215
216 if tzinfo:
217 return tzp.localize(result, tzinfo)
218
219 if utc:
220 return tzp.localize_utc(result)
221
222 return result
223
224 @classmethod
225 def examples(cls) -> list[Self]:
226 """Examples of vTime."""
227 return [cls(time(12, 30))]
228
229 from icalendar.param import VALUE
230
231 def to_jcal(self, name: str) -> list:
232 """The jCal representation of this property according to :rfc:`7265`."""
233 value = self.dt.strftime("%H:%M:%S")
234 if self.is_utc():
235 value += "Z"
236 return [name, self.params.to_jcal(exclude_utc=True), self.VALUE.lower(), value]
237
238 @classmethod
239 def parse_jcal_value(cls, jcal: str) -> time:
240 """Parse a jCal string to a :class:`datetime.time`.
241
242 Raises:
243 ~icalendar.error.JCalParsingError: If it can't parse a time.
244 """
245 JCalParsingError.validate_value_type(jcal, str, cls)
246 match = TIME_JCAL_REGEX.match(jcal)
247 if match is None:
248 raise JCalParsingError("Cannot parse time.", cls, value=jcal)
249 hour = int(match.group("hour"))
250 minute = int(match.group("minute"))
251 second = int(match.group("second"))
252 utc = bool(match.group("utc"))
253 return time(hour, minute, second, tzinfo=timezone.utc if utc else None)
254
255 @classmethod
256 def from_jcal(cls, jcal_property: list) -> Self:
257 """Parse jCal from :rfc:`7265`.
258
259 Parameters:
260 jcal_property: The jCal property to parse.
261
262 Raises:
263 ~icalendar.error.JCalParsingError: If the provided jCal is invalid.
264 """
265 JCalParsingError.validate_property(jcal_property, cls)
266 with JCalParsingError.reraise_with_path_added(3):
267 value = cls.parse_jcal_value(jcal_property[3])
268 return cls(
269 value,
270 params=Parameters.from_jcal_property(jcal_property),
271 )
272
273 def to_xcal(self, element: Element) -> None:
274 """The xCal representation of this property according to :rfc:`6321`."""
275 self.params.to_xcal(element)
276 element = SubElement(element, self.default_value.lower())
277 text = self.dt.strftime("%H:%M:%S")
278 if is_utc(self.dt):
279 text += "Z"
280 element.text = text
281
282 @classmethod
283 @from_xcal_wrapper
284 def from_xcal(cls, parser: XCalParser, params: Parameters) -> Self:
285 """Parse xCal from :rfc:`6321`.
286
287 Parameters:
288 xml: The XML to parse or a parser.
289
290 Raises:
291 ~error.XCalParsingError: If the provided xCal is invalid.
292 """
293 element = parser.parse_tag(cls.default_value)
294 hour, minute, second, utc = XCAL_TIME_REGEX.groups(element)
295 if second == "60":
296 # This is wrong but we have no way to represent this
297 second = 59
298 try:
299 dt = time(int(hour), int(minute), int(second))
300 except ValueError as e:
301 raise XCalParsingError(
302 "Time is out of range", element.get_xsd_token(), element
303 ) from e
304 tzid = params.tzid
305 if utc:
306 if tzid:
307 raise XCalParsingError(f"Cannot mix {tzid} with UTC", None, element)
308 dt = tzp.localize_utc(dt)
309 elif tzid:
310 dt = tzp.localize(dt, tzid)
311 return cls(dt, params=params)
312
313
314__all__ = ["vTime"]