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)?$"
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, params: dict[str, Any] | 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 try:
177 if isinstance(ical, bytes):
178 ical = ical.decode()
179 utc = ical.endswith("Z")
180 if utc:
181 ical = ical[:-1]
182 timetuple = (int(ical[:2]), int(ical[2:4]), int(ical[4:6]))
183 if tzinfo:
184 return tzp.localize(time(*timetuple), tzinfo)
185 if utc:
186 return tzp.localize_utc(time(*timetuple))
187 return time(*timetuple)
188 except Exception as e:
189 raise ValueError(f"Expected time, got: {ical}") from e
190
191 @classmethod
192 def examples(cls) -> list[Self]:
193 """Examples of vTime."""
194 return [cls(time(12, 30))]
195
196 from icalendar.param import VALUE
197
198 def to_jcal(self, name: str) -> list:
199 """The jCal representation of this property according to :rfc:`7265`."""
200 value = self.dt.strftime("%H:%M:%S")
201 if self.is_utc():
202 value += "Z"
203 return [name, self.params.to_jcal(exclude_utc=True), self.VALUE.lower(), value]
204
205 @classmethod
206 def parse_jcal_value(cls, jcal: str) -> time:
207 """Parse a jCal string to a :py:class:`datetime.time`.
208
209 Raises:
210 ~error.JCalParsingError: If it can't parse a time.
211 """
212 JCalParsingError.validate_value_type(jcal, str, cls)
213 match = TIME_JCAL_REGEX.match(jcal)
214 if match is None:
215 raise JCalParsingError("Cannot parse time.", cls, value=jcal)
216 hour = int(match.group("hour"))
217 minute = int(match.group("minute"))
218 second = int(match.group("second"))
219 utc = bool(match.group("utc"))
220 return time(hour, minute, second, tzinfo=timezone.utc if utc else None)
221
222 @classmethod
223 def from_jcal(cls, jcal_property: list) -> Self:
224 """Parse jCal from :rfc:`7265`.
225
226 Parameters:
227 jcal_property: The jCal property to parse.
228
229 Raises:
230 ~error.JCalParsingError: If the provided jCal is invalid.
231 """
232 JCalParsingError.validate_property(jcal_property, cls)
233 with JCalParsingError.reraise_with_path_added(3):
234 value = cls.parse_jcal_value(jcal_property[3])
235 return cls(
236 value,
237 params=Parameters.from_jcal_property(jcal_property),
238 )
239
240
241__all__ = ["vTime"]