1"""RECUR property type from :rfc:`5545`."""
2
3from typing import Any, ClassVar
4
5from icalendar.caselessdict import CaselessDict
6from icalendar.compatibility import Self
7from icalendar.error import JCalParsingError
8from icalendar.parser import Parameters
9from icalendar.parser_tools import DEFAULT_ENCODING, SEQUENCE_TYPES
10from icalendar.prop.dt import vDDDTypes
11from icalendar.prop.integer import vInt
12from icalendar.prop.recur.frequency import vFrequency
13from icalendar.prop.recur.month import vMonth
14from icalendar.prop.recur.skip import vSkip
15from icalendar.prop.recur.weekday import vWeekday
16from icalendar.prop.text import vText
17
18
19class vRecur(CaselessDict):
20 """Recurrence definition.
21
22 Property Name:
23 RRULE
24
25 Purpose:
26 This property defines a rule or repeating pattern for recurring events, to-dos,
27 journal entries, or time zone definitions.
28
29 Value Type:
30 RECUR
31
32 Property Parameters:
33 IANA and non-standard property parameters can be specified on this property.
34
35 Conformance:
36 This property can be specified in recurring "VEVENT", "VTODO", and "VJOURNAL"
37 calendar components as well as in the "STANDARD" and "DAYLIGHT" sub-components
38 of the "VTIMEZONE" calendar component, but it SHOULD NOT be specified more than once.
39 The recurrence set generated with multiple "RRULE" properties is undefined.
40
41 Description:
42 The recurrence rule, if specified, is used in computing the recurrence set.
43 The recurrence set is the complete set of recurrence instances for a calendar component.
44 The recurrence set is generated by considering the initial "DTSTART" property along
45 with the "RRULE", "RDATE", and "EXDATE" properties contained within the
46 recurring component. The "DTSTART" property defines the first instance in the
47 recurrence set. The "DTSTART" property value SHOULD be synchronized with the
48 recurrence rule, if specified. The recurrence set generated with a "DTSTART" property
49 value not synchronized with the recurrence rule is undefined.
50 The final recurrence set is generated by gathering all of the start DATE-TIME
51 values generated by any of the specified "RRULE" and "RDATE" properties, and then
52 excluding any start DATE-TIME values specified by "EXDATE" properties.
53 This implies that start DATE- TIME values specified by "EXDATE" properties take
54 precedence over those specified by inclusion properties (i.e., "RDATE" and "RRULE").
55 Where duplicate instances are generated by the "RRULE" and "RDATE" properties,
56 only one recurrence is considered. Duplicate instances are ignored.
57
58 The "DTSTART" property specified within the iCalendar object defines the first
59 instance of the recurrence. In most cases, a "DTSTART" property of DATE-TIME value
60 type used with a recurrence rule, should be specified as a date with local time
61 and time zone reference to make sure all the recurrence instances start at the
62 same local time regardless of time zone changes.
63
64 If the duration of the recurring component is specified with the "DTEND" or
65 "DUE" property, then the same exact duration will apply to all the members of the
66 generated recurrence set. Else, if the duration of the recurring component is
67 specified with the "DURATION" property, then the same nominal duration will apply
68 to all the members of the generated recurrence set and the exact duration of each
69 recurrence instance will depend on its specific start time. For example, recurrence
70 instances of a nominal duration of one day will have an exact duration of more or less
71 than 24 hours on a day where a time zone shift occurs. The duration of a specific
72 recurrence may be modified in an exception component or simply by using an
73 "RDATE" property of PERIOD value type.
74
75 Examples:
76 The following RRULE specifies daily events for 10 occurrences.
77
78 .. code-block:: text
79
80 RRULE:FREQ=DAILY;COUNT=10
81
82 Below, we parse the RRULE ical string.
83
84 .. code-block:: pycon
85
86 >>> from icalendar.prop import vRecur
87 >>> rrule = vRecur.from_ical('FREQ=DAILY;COUNT=10')
88 >>> rrule
89 vRecur({'FREQ': ['DAILY'], 'COUNT': [10]})
90
91 You can choose to add an rrule to an :class:`icalendar.cal.Event` or
92 :class:`icalendar.cal.Todo`.
93
94 .. code-block:: pycon
95
96 >>> from icalendar import Event
97 >>> event = Event()
98 >>> event.add('RRULE', 'FREQ=DAILY;COUNT=10')
99 >>> event.rrules
100 [vRecur({'FREQ': ['DAILY'], 'COUNT': [10]})]
101 """
102
103 default_value: ClassVar[str] = "RECUR"
104 params: Parameters
105
106 frequencies = [
107 "SECONDLY",
108 "MINUTELY",
109 "HOURLY",
110 "DAILY",
111 "WEEKLY",
112 "MONTHLY",
113 "YEARLY",
114 ]
115
116 # Mac iCal ignores RRULEs where FREQ is not the first rule part.
117 # Sorts parts according to the order listed in RFC 5545, section 3.3.10.
118 canonical_order = (
119 "RSCALE",
120 "FREQ",
121 "UNTIL",
122 "COUNT",
123 "INTERVAL",
124 "BYSECOND",
125 "BYMINUTE",
126 "BYHOUR",
127 "BYDAY",
128 "BYWEEKDAY",
129 "BYMONTHDAY",
130 "BYYEARDAY",
131 "BYWEEKNO",
132 "BYMONTH",
133 "BYSETPOS",
134 "WKST",
135 "SKIP",
136 )
137
138 types = CaselessDict(
139 {
140 "COUNT": vInt,
141 "INTERVAL": vInt,
142 "BYSECOND": vInt,
143 "BYMINUTE": vInt,
144 "BYHOUR": vInt,
145 "BYWEEKNO": vInt,
146 "BYMONTHDAY": vInt,
147 "BYYEARDAY": vInt,
148 "BYMONTH": vMonth,
149 "UNTIL": vDDDTypes,
150 "BYSETPOS": vInt,
151 "WKST": vWeekday,
152 "BYDAY": vWeekday,
153 "FREQ": vFrequency,
154 "BYWEEKDAY": vWeekday,
155 "SKIP": vSkip, # RFC 7529
156 "RSCALE": vText, # RFC 7529
157 }
158 )
159
160 # for reproducible serialization:
161 # RULE: if and only if it can be a list it will be a list
162 # look up in RFC
163 jcal_not_a_list = {"FREQ", "UNTIL", "COUNT", "INTERVAL", "WKST", "SKIP", "RSCALE"}
164
165 def __init__(self, *args, params: dict[str, Any] | None = None, **kwargs):
166 if args and isinstance(args[0], str):
167 # we have a string as an argument.
168 args = (self.from_ical(args[0]),) + args[1:]
169 for k, v in kwargs.items():
170 if not isinstance(v, SEQUENCE_TYPES):
171 kwargs[k] = [v]
172 super().__init__(*args, **kwargs)
173 self.params = Parameters(params)
174
175 def to_ical(self):
176 result = []
177 for key, vals in self.sorted_items():
178 typ = self.types.get(key, vText)
179 if not isinstance(vals, SEQUENCE_TYPES):
180 vals = [vals]
181 param_vals = b",".join(typ(val).to_ical() for val in vals)
182
183 # CaselessDict keys are always unicode
184 param_key = key.encode(DEFAULT_ENCODING)
185 result.append(param_key + b"=" + param_vals)
186
187 return b";".join(result)
188
189 @classmethod
190 def parse_type(cls, key, values):
191 # integers
192 parser = cls.types.get(key, vText)
193 return [parser.from_ical(v) for v in values.split(",")]
194
195 @classmethod
196 def from_ical(cls, ical: str):
197 if isinstance(ical, cls):
198 return ical
199 try:
200 recur = cls()
201 for pairs in ical.split(";"):
202 try:
203 key, vals = pairs.split("=")
204 except ValueError:
205 # E.g. incorrect trailing semicolon, like (issue #157):
206 # FREQ=YEARLY;BYMONTH=11;BYDAY=1SU;
207 continue
208 recur[key] = cls.parse_type(key, vals)
209 return cls(recur)
210 except ValueError:
211 raise
212 except Exception as e:
213 raise ValueError(f"Error in recurrence rule: {ical}") from e
214
215 @classmethod
216 def examples(cls) -> list[Self]:
217 """Examples of vRecur."""
218 return [cls.from_ical("FREQ=DAILY;COUNT=10")]
219
220 from icalendar.param import VALUE
221
222 def to_jcal(self, name: str) -> list:
223 """The jCal representation of this property according to :rfc:`7265`."""
224 recur = {}
225 for k, v in self.items():
226 key = k.lower()
227 if key.upper() in self.jcal_not_a_list:
228 value = v[0] if isinstance(v, list) and len(v) == 1 else v
229 elif not isinstance(v, list):
230 value = [v]
231 else:
232 value = v
233 recur[key] = value
234 if "until" in recur:
235 until = recur["until"]
236 until_jcal = vDDDTypes(until).to_jcal("until")
237 recur["until"] = until_jcal[-1]
238 return [name, self.params.to_jcal(), self.VALUE.lower(), recur]
239
240 @classmethod
241 def from_jcal(cls, jcal_property: list) -> Self:
242 """Parse jCal from :rfc:`7265`.
243
244 Parameters:
245 jcal_property: The jCal property to parse.
246
247 Raises:
248 ~error.JCalParsingError: If the provided jCal is invalid.
249 """
250 JCalParsingError.validate_property(jcal_property, cls)
251 params = Parameters.from_jcal_property(jcal_property)
252 if not isinstance(jcal_property[3], dict) or not all(
253 isinstance(k, str) for k in jcal_property[3]
254 ):
255 raise JCalParsingError(
256 "The recurrence rule must be a mapping with string keys.",
257 cls,
258 3,
259 value=jcal_property[3],
260 )
261 recur = {}
262 for key, value in jcal_property[3].items():
263 value_type = cls.types.get(key, vText)
264 with JCalParsingError.reraise_with_path_added(3, key):
265 if isinstance(value, list):
266 recur[key.lower()] = values = []
267 for i, v in enumerate(value):
268 with JCalParsingError.reraise_with_path_added(i):
269 values.append(value_type.parse_jcal_value(v))
270 else:
271 recur[key] = value_type.parse_jcal_value(value)
272 until = recur.get("until")
273 if until is not None and not isinstance(until, list):
274 recur["until"] = [until]
275 return cls(recur, params=params)
276
277 def __eq__(self, other: object) -> bool:
278 """self == other"""
279 if not isinstance(other, vRecur):
280 return super().__eq__(other)
281 if self.keys() != other.keys():
282 return False
283 for key in self.keys():
284 v1 = self[key]
285 v2 = other[key]
286 if not isinstance(v1, SEQUENCE_TYPES):
287 v1 = [v1]
288 if not isinstance(v2, SEQUENCE_TYPES):
289 v2 = [v2]
290 if v1 != v2:
291 return False
292 return True
293
294 __hash__ = None
295
296
297__all__ = ["vRecur"]