1"""DATE-TIME property type from :rfc:`5545`."""
2
3from datetime import datetime
4from typing import Any, ClassVar
5
6from icalendar.compatibility import Self
7from icalendar.error import JCalParsingError
8from icalendar.parser import Parameters
9from icalendar.parser_tools import to_unicode
10from icalendar.timezone import tzp
11from icalendar.timezone.tzid import is_utc
12
13from .base import TimeBase
14
15
16class vDatetime(TimeBase):
17 """Date-Time
18
19 Value Name:
20 DATE-TIME
21
22 Purpose:
23 This value type is used to identify values that specify a
24 precise calendar date and time of day. The format is based on
25 the ISO.8601.2004 complete representation.
26
27 Format Definition:
28 This value type is defined by the following notation:
29
30 .. code-block:: text
31
32 date-time = date "T" time
33
34 date = date-value
35 date-value = date-fullyear date-month date-mday
36 date-fullyear = 4DIGIT
37 date-month = 2DIGIT ;01-12
38 date-mday = 2DIGIT ;01-28, 01-29, 01-30, 01-31
39 ;based on month/year
40 time = time-hour time-minute time-second [time-utc]
41 time-hour = 2DIGIT ;00-23
42 time-minute = 2DIGIT ;00-59
43 time-second = 2DIGIT ;00-60
44 time-utc = "Z"
45
46 The following is the representation of the date-time format.
47
48 .. code-block:: text
49
50 YYYYMMDDTHHMMSS
51
52 Description:
53 vDatetime is timezone aware and uses a timezone library.
54 When a vDatetime object is created from an
55 ical string, you can pass a valid timezone identifier. When a
56 vDatetime object is created from a Python :mod:`datetime` object, it uses the
57 tzinfo component, if present. Otherwise a timezone-naive object is
58 created. Be aware that there are certain limitations with timezone naive
59 DATE-TIME components in the icalendar standard.
60
61 Example:
62 The following represents March 2, 2021 at 10:15 AM with local time:
63
64 .. code-block:: pycon
65
66 >>> from icalendar import vDatetime
67 >>> datetime = vDatetime.from_ical("20210302T101500")
68 >>> datetime.tzname()
69 >>> datetime.year
70 2021
71 >>> datetime.minute
72 15
73
74 The following represents March 2, 2021 at 10:15 AM in New York:
75
76 .. code-block:: pycon
77
78 >>> datetime = vDatetime.from_ical("20210302T101500", 'America/New_York')
79 >>> datetime.tzname()
80 'EST'
81
82 The following represents March 2, 2021 at 10:15 AM in Berlin:
83
84 .. code-block:: pycon
85
86 >>> from zoneinfo import ZoneInfo
87 >>> timezone = ZoneInfo("Europe/Berlin")
88 >>> vDatetime.from_ical("20210302T101500", timezone)
89 datetime.datetime(2021, 3, 2, 10, 15, tzinfo=ZoneInfo(key='Europe/Berlin'))
90 """
91
92 default_value: ClassVar[str] = "DATE-TIME"
93 params: Parameters
94
95 def __init__(self, dt: datetime, /, params: dict[str, Any] | None = None) -> None:
96 self.dt = dt
97 self.params = Parameters(params)
98 self.params.update_tzid_from(dt)
99
100 def to_ical(self):
101 dt = self.dt
102
103 s = (
104 f"{dt.year:04}{dt.month:02}{dt.day:02}"
105 f"T{dt.hour:02}{dt.minute:02}{dt.second:02}"
106 )
107 if self.is_utc():
108 s += "Z"
109 return s.encode("utf-8")
110
111 @staticmethod
112 def from_ical(ical, timezone=None):
113 """Create a datetime from the RFC string."""
114 ical = to_unicode(ical)
115 tzinfo = None
116 if isinstance(timezone, str):
117 tzinfo = tzp.timezone(timezone)
118 elif timezone is not None:
119 tzinfo = timezone
120
121 # Extract the value part if parameters are present per
122 # https://datatracker.ietf.org/doc/html/rfc5545.html#section-3.3.5
123 # Form #3: TZID=America/New_York:19980119T020000
124 ical_value = ical.rpartition(":")[2]
125
126 if len(ical_value) < 15 or ical_value[8] != "T":
127 raise ValueError(f"Wrong datetime format: {ical}")
128
129 try:
130 timetuple = (
131 int(ical_value[:4]), # year
132 int(ical_value[4:6]), # month
133 int(ical_value[6:8]), # day
134 int(ical_value[9:11]), # hour
135 int(ical_value[11:13]), # minute
136 int(ical_value[13:15]), # second
137 )
138 if tzinfo:
139 return tzp.localize(datetime(*timetuple), tzinfo)
140 if not ical_value[15:]:
141 return datetime(*timetuple)
142 if ical_value[15:] == "Z":
143 return tzp.localize_utc(datetime(*timetuple))
144 except Exception as e:
145 raise ValueError(f"Wrong datetime format: {ical}") from e
146 raise ValueError(f"Wrong datetime format: {ical}")
147
148 @classmethod
149 def examples(cls) -> list[Self]:
150 """Examples of vDatetime."""
151 return [cls(datetime(2025, 11, 10, 16, 52))]
152
153 from icalendar.param import VALUE
154
155 def to_jcal(self, name: str) -> list:
156 """The jCal representation of this property according to :rfc:`7265`."""
157 value = self.dt.strftime("%Y-%m-%dT%H:%M:%S")
158 if self.is_utc():
159 value += "Z"
160 return [name, self.params.to_jcal(exclude_utc=True), self.VALUE.lower(), value]
161
162 def is_utc(self) -> bool:
163 """Whether this datetime is UTC."""
164 return self.params.is_utc() or is_utc(self.dt)
165
166 @classmethod
167 def parse_jcal_value(cls, jcal: str) -> datetime:
168 """Parse a jCal string to a :class:`datetime.datetime`.
169
170 Raises:
171 ~error.JCalParsingError: If it can't parse a date-time value.
172 """
173 JCalParsingError.validate_value_type(jcal, str, cls)
174 utc = jcal.endswith("Z")
175 if utc:
176 jcal = jcal[:-1]
177 try:
178 dt = datetime.strptime(jcal, "%Y-%m-%dT%H:%M:%S")
179 except ValueError as e:
180 raise JCalParsingError("Cannot parse date-time.", cls, value=jcal) from e
181 if utc:
182 return tzp.localize_utc(dt)
183 return dt
184
185 @classmethod
186 def from_jcal(cls, jcal_property: list) -> Self:
187 """Parse jCal from :rfc:`7265`.
188
189 Parameters:
190 jcal_property: The jCal property to parse.
191
192 Raises:
193 ~error.JCalParsingError: If the provided jCal is invalid.
194 """
195 JCalParsingError.validate_property(jcal_property, cls)
196 params = Parameters.from_jcal_property(jcal_property)
197 with JCalParsingError.reraise_with_path_added(3):
198 dt = cls.parse_jcal_value(jcal_property[3])
199 if params.tzid:
200 dt = tzp.localize(dt, params.tzid)
201 return cls(
202 dt,
203 params=params,
204 )
205
206
207__all__ = ["vDatetime"]