1"""DATE property type from :rfc:`5545`."""
2
3from datetime import date, datetime
4from typing import Any, ClassVar
5
6from icalendar.compatibility import Self
7from icalendar.error import JCalParsingError
8from icalendar.parser import Parameters
9
10from .base import TimeBase
11
12
13class vDate(TimeBase):
14 """Date
15
16 Value Name:
17 DATE
18
19 Purpose:
20 This value type is used to identify values that contain a
21 calendar date.
22
23 Format Definition:
24 This value type is defined by the following notation:
25
26 .. code-block:: text
27
28 date = date-value
29
30 date-value = date-fullyear date-month date-mday
31 date-fullyear = 4DIGIT
32 date-month = 2DIGIT ;01-12
33 date-mday = 2DIGIT ;01-28, 01-29, 01-30, 01-31
34 ;based on month/year
35
36 Description:
37 If the property permits, multiple "date" values are
38 specified as a COMMA-separated list of values. The format for the
39 value type is based on the [ISO.8601.2004] complete
40 representation, basic format for a calendar date. The textual
41 format specifies a four-digit year, two-digit month, and two-digit
42 day of the month. There are no separator characters between the
43 year, month, and day component text.
44
45 Example:
46 The following represents July 14, 1997:
47
48 .. code-block:: ics
49
50 19970714
51
52 .. code-block:: pycon
53
54 >>> from icalendar.prop import vDate
55 >>> date = vDate.from_ical('19970714')
56 >>> date.year
57 1997
58 >>> date.month
59 7
60 >>> date.day
61 14
62 """
63
64 default_value: ClassVar[str] = "DATE"
65 params: Parameters
66
67 def __init__(self, dt: date, params: dict[str, Any] | None = None) -> None:
68 if not isinstance(dt, date):
69 raise TypeError("Value MUST be a date instance")
70 self.dt = dt
71 self.params = Parameters(params or {})
72
73 def to_ical(self):
74 s = f"{self.dt.year:04}{self.dt.month:02}{self.dt.day:02}"
75 return s.encode("utf-8")
76
77 @staticmethod
78 def from_ical(ical):
79 # date-value = 4DIGIT 2DIGIT 2DIGIT, no separators,
80 # per https://datatracker.ietf.org/doc/html/rfc5545#section-3.3.4
81 if len(ical) != 8 or not ical.isascii() or not ical.isdigit():
82 raise ValueError(f"Wrong date format {ical}")
83 try:
84 timetuple = (
85 int(ical[:4]), # year
86 int(ical[4:6]), # month
87 int(ical[6:8]), # day
88 )
89 return date(*timetuple)
90 except Exception as e:
91 raise ValueError(f"Wrong date format {ical}") from e
92
93 @classmethod
94 def examples(cls) -> list[Self]:
95 """Examples of vDate."""
96 return [cls(date(2025, 11, 10))]
97
98 from icalendar.param import VALUE
99
100 def to_jcal(self, name: str) -> list:
101 """The jCal representation of this property according to :rfc:`7265`."""
102 return [
103 name,
104 self.params.to_jcal(),
105 self.VALUE.lower(),
106 self.dt.strftime("%Y-%m-%d"),
107 ]
108
109 @classmethod
110 def parse_jcal_value(cls, jcal: str) -> date:
111 """Parse a jCal string to a :class:`datetime.date`.
112
113 Raises:
114 ~error.JCalParsingError: If it can't parse a date.
115 """
116 JCalParsingError.validate_value_type(jcal, str, cls)
117 try:
118 return datetime.strptime(jcal, "%Y-%m-%d").date()
119 except ValueError as e:
120 raise JCalParsingError("Cannot parse date.", cls, value=jcal) from e
121
122 @classmethod
123 def from_jcal(cls, jcal_property: list) -> Self:
124 """Parse jCal from :rfc:`7265`.
125
126 Parameters:
127 jcal_property: The jCal property to parse.
128
129 Raises:
130 ~error.JCalParsingError: If the provided jCal is invalid.
131 """
132 JCalParsingError.validate_property(jcal_property, cls)
133 with JCalParsingError.reraise_with_path_added(3):
134 value = cls.parse_jcal_value(jcal_property[3])
135 return cls(
136 value,
137 params=Parameters.from_jcal_property(jcal_property),
138 )
139
140
141__all__ = ["vDate"]