1"""DURATION property type from :rfc:`5545`."""
2
3import re
4from datetime import timedelta
5from typing import Any, ClassVar
6
7from icalendar.compatibility import Self
8from icalendar.error import InvalidCalendar, JCalParsingError
9from icalendar.parser import Parameters
10
11from .base import TimeBase
12
13DURATION_REGEX = re.compile(
14 r"([-+]?)P(?:(\d+)W)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$"
15)
16
17
18class vDuration(TimeBase):
19 """Duration
20
21 Value Name:
22 DURATION
23
24 Purpose:
25 This value type is used to identify properties that contain
26 a duration of time.
27
28 Format Definition:
29 This value type is defined by the following notation:
30
31 .. code-block:: text
32
33 dur-value = (["+"] / "-") "P" (dur-date / dur-time / dur-week)
34
35 dur-date = dur-day [dur-time]
36 dur-time = "T" (dur-hour / dur-minute / dur-second)
37 dur-week = 1*DIGIT "W"
38 dur-hour = 1*DIGIT "H" [dur-minute]
39 dur-minute = 1*DIGIT "M" [dur-second]
40 dur-second = 1*DIGIT "S"
41 dur-day = 1*DIGIT "D"
42
43 Description:
44 If the property permits, multiple "duration" values are
45 specified by a COMMA-separated list of values. The format is
46 based on the [ISO.8601.2004] complete representation basic format
47 with designators for the duration of time. The format can
48 represent nominal durations (weeks and days) and accurate
49 durations (hours, minutes, and seconds). Note that unlike
50 [ISO.8601.2004], this value type doesn't support the "Y" and "M"
51 designators to specify durations in terms of years and months.
52 The duration of a week or a day depends on its position in the
53 calendar. In the case of discontinuities in the time scale, such
54 as the change from standard time to daylight time and back, the
55 computation of the exact duration requires the subtraction or
56 addition of the change of duration of the discontinuity. Leap
57 seconds MUST NOT be considered when computing an exact duration.
58 When computing an exact duration, the greatest order time
59 components MUST be added first, that is, the number of days MUST
60 be added first, followed by the number of hours, number of
61 minutes, and number of seconds.
62
63 Example:
64 A duration of 15 days, 5 hours, and 20 seconds would be:
65
66 .. code-block:: text
67
68 P15DT5H0M20S
69
70 A duration of 7 weeks would be:
71
72 .. code-block:: text
73
74 P7W
75
76 .. code-block:: pycon
77
78 >>> from icalendar.prop import vDuration
79 >>> duration = vDuration.from_ical('P15DT5H0M20S')
80 >>> duration
81 datetime.timedelta(days=15, seconds=18020)
82 >>> duration = vDuration.from_ical('P7W')
83 >>> duration
84 datetime.timedelta(days=49)
85 """
86
87 default_value: ClassVar[str] = "DURATION"
88 params: Parameters
89
90 def __init__(self, td: timedelta | str, /, params: dict[str, Any] | None = None):
91 if isinstance(td, str):
92 td = vDuration.from_ical(td)
93 if not isinstance(td, timedelta):
94 raise TypeError("Value MUST be a timedelta instance")
95 self.td = td
96 self.params = Parameters(params)
97
98 def to_ical(self):
99 sign = ""
100 td = self.td
101 if td.days < 0:
102 sign = "-"
103 td = -td
104 timepart = ""
105 if td.seconds:
106 timepart = "T"
107 hours = td.seconds // 3600
108 minutes = td.seconds % 3600 // 60
109 seconds = td.seconds % 60
110 if hours:
111 timepart += f"{hours}H"
112 if minutes or (hours and seconds):
113 timepart += f"{minutes}M"
114 if seconds:
115 timepart += f"{seconds}S"
116 if td.days == 0 and timepart:
117 return str(sign).encode("utf-8") + b"P" + str(timepart).encode("utf-8")
118 return (
119 str(sign).encode("utf-8")
120 + b"P"
121 + str(abs(td.days)).encode("utf-8")
122 + b"D"
123 + str(timepart).encode("utf-8")
124 )
125
126 @staticmethod
127 def from_ical(ical):
128 match = DURATION_REGEX.match(ical)
129 if not match:
130 raise InvalidCalendar(f"Invalid iCalendar duration: {ical}")
131
132 sign, weeks, days, hours, minutes, seconds = match.groups()
133 value = timedelta(
134 weeks=int(weeks or 0),
135 days=int(days or 0),
136 hours=int(hours or 0),
137 minutes=int(minutes or 0),
138 seconds=int(seconds or 0),
139 )
140
141 if sign == "-":
142 value = -value
143
144 return value
145
146 @property
147 def dt(self) -> timedelta:
148 """The time delta for compatibility."""
149 return self.td
150
151 @classmethod
152 def examples(cls) -> list[Self]:
153 """Examples of vDuration."""
154 return [cls(timedelta(1, 99))]
155
156 from icalendar.param import VALUE
157
158 def to_jcal(self, name: str) -> list:
159 """The jCal representation of this property according to :rfc:`7265`."""
160 return [
161 name,
162 self.params.to_jcal(),
163 self.VALUE.lower(),
164 self.to_ical().decode(),
165 ]
166
167 @classmethod
168 def parse_jcal_value(cls, jcal: str) -> timedelta:
169 """Parse a jCal string to a :py:class:`datetime.timedelta`.
170
171 Raises:
172 ~error.JCalParsingError: If it can't parse a duration."""
173 JCalParsingError.validate_value_type(jcal, str, cls)
174 try:
175 return cls.from_ical(jcal)
176 except (ValueError, InvalidCalendar) as e:
177 raise JCalParsingError("Cannot parse duration.", cls, value=jcal) from e
178
179 @classmethod
180 def from_jcal(cls, jcal_property: list) -> Self:
181 """Parse jCal from :rfc:`7265`.
182
183 Parameters:
184 jcal_property: The jCal property to parse.
185
186 Raises:
187 ~error.JCalParsingError: If the provided jCal is invalid.
188 """
189 JCalParsingError.validate_property(jcal_property, cls)
190 with JCalParsingError.reraise_with_path_added(3):
191 duration = cls.parse_jcal_value(jcal_property[3])
192 return cls(
193 duration,
194 Parameters.from_jcal_property(jcal_property),
195 )
196
197
198__all__ = ["vDuration"]