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