1"""DATE property type from :rfc:`5545`."""
2
3from __future__ import annotations
4
5from datetime import date, datetime
6from typing import TYPE_CHECKING, Any, ClassVar
7from xml.etree.ElementTree import Element, SubElement
8
9from icalendar.error import ICalParsingError, JCalParsingError, XCalParsingError
10from icalendar.parser import Parameters
11from icalendar.parser.xcal.match import XCalRegexMatcher
12from icalendar.parser.xcal.wrapper import from_xcal_wrapper
13
14from .base import TimeBase
15
16if TYPE_CHECKING:
17 from icalendar.compatibility import Self
18 from icalendar.parser.xcal.adapter import ElementAdapter
19 from icalendar.parser.xcal.base import XCalParser
20
21XCAL_DATE_REGEX = XCalRegexMatcher(
22 r"(\d\d\d\d)-(\d\d)-(\d\d)", "Expected date format YYYY-MM-DD"
23)
24
25
26class vDate(TimeBase):
27 """Date
28
29 Value Name:
30 DATE
31
32 Purpose:
33 This value type is used to identify values that contain a
34 calendar date.
35
36 Format Definition:
37 This value type is defined by the following notation:
38
39 .. code-block:: text
40
41 date = date-value
42
43 date-value = date-fullyear date-month date-mday
44 date-fullyear = 4DIGIT
45 date-month = 2DIGIT ;01-12
46 date-mday = 2DIGIT ;01-28, 01-29, 01-30, 01-31
47 ;based on month/year
48
49 Description:
50 If the property permits, multiple "date" values are
51 specified as a COMMA-separated list of values. The format for the
52 value type is based on the [ISO.8601.2004] complete
53 representation, basic format for a calendar date. The textual
54 format specifies a four-digit year, two-digit month, and two-digit
55 day of the month. There are no separator characters between the
56 year, month, and day component text.
57
58 Example:
59 The following represents July 14, 1997:
60
61 .. code-block:: ics
62
63 19970714
64
65 .. code-block:: pycon
66
67 >>> from icalendar.prop import vDate
68 >>> date = vDate.from_ical('19970714')
69 >>> date.year
70 1997
71 >>> date.month
72 7
73 >>> date.day
74 14
75 """
76
77 default_value: ClassVar[str] = "DATE"
78 params: Parameters
79
80 def __init__(self, dt: date, params: dict[str, Any] | None = None) -> None:
81 if not isinstance(dt, date):
82 raise TypeError("Value MUST be a date instance")
83 self.dt = dt
84 self.params = Parameters(params or {})
85
86 def to_ical(self) -> bytes:
87 s = f"{self.dt.year:04}{self.dt.month:02}{self.dt.day:02}"
88 return s.encode("utf-8")
89
90 @staticmethod
91 def from_ical(ical: str) -> date:
92 """Parse an iCalendar DATE value.
93
94 Parameters:
95 ical (str | bytes): Required. The DATE value in ``YYYYMMDD`` format.
96
97 Raises:
98 ~icalendar.error.ICalParsingError: If the value is not a valid DATE.
99 """
100 # date-value = 4DIGIT 2DIGIT 2DIGIT, no separators,
101 # per https://datatracker.ietf.org/doc/html/rfc5545#section-3.3.4
102 if len(ical) != 8 or not ical.isascii() or not ical.isdigit():
103 raise ICalParsingError(
104 "Wrong date format",
105 value=ical,
106 )
107 try:
108 timetuple = (
109 int(ical[:4]), # year
110 int(ical[4:6]), # month
111 int(ical[6:8]), # day
112 )
113 return date(*timetuple)
114 except Exception as e:
115 raise ICalParsingError(
116 "Wrong date format",
117 value=ical,
118 ) from e
119
120 @classmethod
121 def examples(cls) -> list[Self]:
122 """Examples of vDate."""
123 return [cls(date(2025, 11, 10))]
124
125 from icalendar.param import VALUE
126
127 def to_jcal(self, name: str) -> list:
128 """The jCal representation of this property according to :rfc:`7265`."""
129 return [
130 name,
131 self.params.to_jcal(),
132 self.VALUE.lower(),
133 self.dt.strftime("%Y-%m-%d"),
134 ]
135
136 @classmethod
137 def parse_jcal_value(cls, jcal: str) -> date:
138 """Parse a jCal string to a :class:`datetime.date`.
139
140 Raises:
141 ~icalendar.error.JCalParsingError: If it can't parse a date.
142 """
143 JCalParsingError.validate_value_type(jcal, str, cls)
144 try:
145 return datetime.strptime(jcal, "%Y-%m-%d").date()
146 except ValueError as e:
147 raise JCalParsingError("Cannot parse date.", cls, value=jcal) from e
148
149 @classmethod
150 def from_jcal(cls, jcal_property: list) -> Self:
151 """Parse jCal from :rfc:`7265`.
152
153 Parameters:
154 jcal_property: The jCal property to parse.
155
156 Raises:
157 ~icalendar.error.JCalParsingError: If the provided jCal is invalid.
158 """
159 JCalParsingError.validate_property(jcal_property, cls)
160 with JCalParsingError.reraise_with_path_added(3):
161 value = cls.parse_jcal_value(jcal_property[3])
162 return cls(
163 value,
164 params=Parameters.from_jcal_property(jcal_property),
165 )
166
167 def to_xcal(self, element: Element) -> None:
168 """The xCal representation of this property according to :rfc:`6321`."""
169 self.params.to_xcal(element)
170 element = SubElement(element, "date")
171 element.text = self.dt.strftime("%Y-%m-%d")
172
173 @classmethod
174 def from_xcal_element(cls, element: ElementAdapter) -> date:
175 """Parse xCal from :rfc:`6321`.
176
177 Parameters:
178 element: The xCal element to parse.
179
180 Returns:
181 The date object.
182
183 Raises:
184 ~error.XCalParsingError: If the provided xCal is invalid.
185 """
186 year, month, day = XCAL_DATE_REGEX.groups(element)
187 try:
188 return date(int(year), int(month), int(day))
189 except ValueError as e:
190 raise XCalParsingError(
191 "Date is out of range", element.get_xsd_token(), element
192 ) from e
193
194 @classmethod
195 @from_xcal_wrapper
196 def from_xcal(cls, parser: XCalParser, params: Parameters) -> Self:
197 """Parse xCal from :rfc:`6321`.
198
199 Parameters:
200 xml: The XML to parse or a parser.
201
202 Raises:
203 ~error.XCalParsingError: If the provided xCal is invalid.
204 """
205 element = parser.parse_tag(cls.default_value)
206 d = cls.from_xcal_element(element)
207 return cls(
208 d,
209 params=params,
210 )
211
212 @property
213 def ical_value(self) -> date:
214 """Returns the date as a date value, in year-month-day format.
215
216 .. seealso::
217
218 :rfc:`5545#section-3.3.4` for the ``DATE`` value type.
219
220 """
221 return self.dt
222
223
224__all__ = ["vDate"]