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