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:: ics
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__(
166 self, *args: Any, params: dict[str, Any] | None = None, **kwargs: Any
167 ) -> None:
168 if args and isinstance(args[0], str):
169 # we have a string as an argument.
170 args = (self.from_ical(args[0]),) + args[1:]
171 for k, v in kwargs.items():
172 if not isinstance(v, SEQUENCE_TYPES):
173 kwargs[k] = [v]
174 super().__init__(*args, **kwargs)
175 self.params = Parameters(params)
176
177 def to_ical(self):
178 result = []
179 for key, vals in self.sorted_items():
180 typ = self.types.get(key, vText)
181 if not isinstance(vals, SEQUENCE_TYPES):
182 vals = [vals]
183 param_vals = b",".join(typ(val).to_ical() for val in vals)
184
185 # CaselessDict keys are always unicode
186 param_key = key.encode(DEFAULT_ENCODING)
187 result.append(param_key + b"=" + param_vals)
188
189 return b";".join(result)
190
191 @classmethod
192 def parse_type(cls, key, values):
193 # integers
194 parser = cls.types.get(key, vText)
195 return [parser.from_ical(v) for v in values.split(",")]
196
197 @classmethod
198 def from_ical(cls, ical: str):
199 if isinstance(ical, cls):
200 return ical
201 try:
202 recur = cls()
203 for pairs in ical.split(";"):
204 try:
205 key, vals = pairs.split("=")
206 except ValueError:
207 # E.g. incorrect trailing semicolon, like (issue #157):
208 # FREQ=YEARLY;BYMONTH=11;BYDAY=1SU;
209 continue
210 recur[key] = cls.parse_type(key, vals)
211 return cls(recur)
212 except ValueError:
213 raise
214 except Exception as e:
215 raise ValueError(f"Error in recurrence rule: {ical}") from e
216
217 @classmethod
218 def examples(cls) -> list[Self]:
219 """Examples of vRecur."""
220 return [cls.from_ical("FREQ=DAILY;COUNT=10")]
221
222 from icalendar.param import VALUE
223
224 def to_jcal(self, name: str) -> list:
225 """The jCal representation of this property according to :rfc:`7265`."""
226 recur = {}
227 for k, v in self.items():
228 key = k.lower()
229 if key.upper() in self.jcal_not_a_list:
230 value = v[0] if isinstance(v, list) and len(v) == 1 else v
231 elif not isinstance(v, list):
232 value = [v]
233 else:
234 value = v
235 recur[key] = value
236 if "until" in recur:
237 until = recur["until"]
238 until_jcal = vDDDTypes(until).to_jcal("until")
239 recur["until"] = until_jcal[-1]
240 return [name, self.params.to_jcal(), self.VALUE.lower(), recur]
241
242 @classmethod
243 def from_jcal(cls, jcal_property: list) -> Self:
244 """Parse jCal from :rfc:`7265`.
245
246 Parameters:
247 jcal_property: The jCal property to parse.
248
249 Raises:
250 ~error.JCalParsingError: If the provided jCal is invalid.
251 """
252 JCalParsingError.validate_property(jcal_property, cls)
253 params = Parameters.from_jcal_property(jcal_property)
254 if not isinstance(jcal_property[3], dict) or not all(
255 isinstance(k, str) for k in jcal_property[3]
256 ):
257 raise JCalParsingError(
258 "The recurrence rule must be a mapping with string keys.",
259 cls,
260 3,
261 value=jcal_property[3],
262 )
263 recur = {}
264 for key, value in jcal_property[3].items():
265 JCalParsingError.validate_jcal_token(
266 key, "recurrence rule part name", cls, path=[3, key]
267 )
268 value_type = cls.types.get(key, vText)
269 with JCalParsingError.reraise_with_path_added(3, key):
270 if isinstance(value, list):
271 recur[key.lower()] = values = []
272 for i, v in enumerate(value):
273 with JCalParsingError.reraise_with_path_added(i):
274 values.append(value_type.parse_jcal_value(v))
275 else:
276 recur[key] = value_type.parse_jcal_value(value)
277 until = recur.get("until")
278 if until is not None and not isinstance(until, list):
279 recur["until"] = [until]
280 return cls(recur, params=params)
281
282 def __eq__(self, other: object) -> bool:
283 """self == other"""
284 if not isinstance(other, vRecur):
285 return super().__eq__(other)
286 if self.keys() != other.keys():
287 return False
288 for key in self.keys():
289 v1 = self[key]
290 v2 = other[key]
291 if not isinstance(v1, SEQUENCE_TYPES):
292 v1 = [v1]
293 if not isinstance(v2, SEQUENCE_TYPES):
294 v2 = [v2]
295 if v1 != v2:
296 return False
297 return True
298
299 __hash__ = None
300
301
302__all__ = ["vRecur"]