Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/dateutil/rrule.py: 32%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""
2The rrule module offers a small, complete, and very fast, implementation of
3the recurrence rules documented in the
4`iCalendar RFC <https://tools.ietf.org/html/rfc5545>`_,
5including support for caching of results.
6"""
8import _thread
9import calendar
10import datetime
11import heapq
12import itertools
13import math
14import re
15import sys
16from functools import wraps
17# For warning about deprecation of until and count
18from warnings import warn
20from ._common import weekday as weekdaybase
22__all__ = ["rrule", "rruleset", "rrulestr",
23 "YEARLY", "MONTHLY", "WEEKLY", "DAILY",
24 "HOURLY", "MINUTELY", "SECONDLY",
25 "MO", "TU", "WE", "TH", "FR", "SA", "SU"]
27# Every mask is 7 days longer to handle cross-year weekly periods.
28M366MASK = tuple([1]*31+[2]*29+[3]*31+[4]*30+[5]*31+[6]*30 +
29 [7]*31+[8]*31+[9]*30+[10]*31+[11]*30+[12]*31+[1]*7)
30M365MASK = list(M366MASK)
31M29, M30, M31 = list(range(1, 30)), list(range(1, 31)), list(range(1, 32))
32MDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7])
33MDAY365MASK = list(MDAY366MASK)
34M29, M30, M31 = list(range(-29, 0)), list(range(-30, 0)), list(range(-31, 0))
35NMDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7])
36NMDAY365MASK = list(NMDAY366MASK)
37M366RANGE = (0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366)
38M365RANGE = (0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365)
39WDAYMASK = [0, 1, 2, 3, 4, 5, 6]*55
40del M29, M30, M31, M365MASK[59], MDAY365MASK[59], NMDAY365MASK[31]
41MDAY365MASK = tuple(MDAY365MASK)
42M365MASK = tuple(M365MASK)
44FREQNAMES = ['YEARLY', 'MONTHLY', 'WEEKLY', 'DAILY', 'HOURLY', 'MINUTELY', 'SECONDLY']
46(YEARLY,
47 MONTHLY,
48 WEEKLY,
49 DAILY,
50 HOURLY,
51 MINUTELY,
52 SECONDLY) = list(range(7))
54# Imported on demand.
55easter = None
56parser = None
59class weekday(weekdaybase):
60 """
61 This version of weekday does not allow n = 0.
62 """
63 def __init__(self, wkday, n=None):
64 if n == 0:
65 raise ValueError("Can't create weekday with n==0")
67 super().__init__(wkday, n)
70MO, TU, WE, TH, FR, SA, SU = weekdays = tuple(weekday(x) for x in range(7))
73def _invalidates_cache(f):
74 """
75 Decorator for rruleset methods which may invalidate the
76 cached length.
77 """
78 @wraps(f)
79 def inner_func(self, *args, **kwargs):
80 rv = f(self, *args, **kwargs)
81 self._invalidate_cache()
82 return rv
84 return inner_func
87class rrulebase:
88 def __init__(self, cache=False):
89 if cache:
90 self._cache = []
91 self._cache_lock = _thread.allocate_lock()
92 self._invalidate_cache()
93 else:
94 self._cache = None
95 self._cache_complete = False
96 self._len = None
98 def __reduce_ex__(self, protocol):
99 rv = super().__reduce_ex__(protocol)
100 state, slots = rv[2], None
101 if isinstance(state, tuple):
102 state, slots = state
103 if self._cache is not None:
104 state = dict(state)
105 state.pop("_cache_lock", None)
106 state.pop("_cache_gen", None)
107 state["_cache"] = []
108 if slots is not None:
109 state = (state, slots)
110 return rv[:2] + (state,) + rv[3:]
112 def __setstate__(self, state):
113 if isinstance(state, tuple):
114 state, slots = state
115 for name, value in slots.items():
116 setattr(self, name, value)
117 self.__dict__.update(state)
118 if self._cache is not None:
119 self._cache_lock = _thread.allocate_lock()
120 self._invalidate_cache()
122 def __iter__(self):
123 if self._cache_complete:
124 return iter(self._cache)
125 elif self._cache is None:
126 return self._iter()
127 else:
128 return self._iter_cached()
130 def _invalidate_cache(self):
131 if self._cache is not None:
132 self._cache = []
133 self._cache_complete = False
134 self._cache_gen = self._iter()
136 if self._cache_lock.locked():
137 self._cache_lock.release()
139 self._len = None
141 def _iter_cached(self):
142 i = 0
143 gen = self._cache_gen
144 cache = self._cache
145 lock = self._cache_lock
146 while gen:
147 if i == len(cache):
148 with lock:
149 if self._cache_complete:
150 break
151 try:
152 for j in range(10):
153 cache.append(next(gen))
154 except StopIteration:
155 self._cache_gen = gen = None
156 self._cache_complete = True
157 break
158 yield cache[i]
159 i += 1
160 while i < self._len:
161 yield cache[i]
162 i += 1
164 def __getitem__(self, item):
165 if self._cache_complete:
166 return self._cache[item]
167 elif isinstance(item, slice):
168 if item.step and item.step < 0:
169 return list(iter(self))[item]
170 else:
171 return list(itertools.islice(self,
172 item.start or 0,
173 item.stop or sys.maxsize,
174 item.step or 1))
175 elif item >= 0:
176 gen = iter(self)
177 try:
178 for i in range(item+1):
179 res = next(gen)
180 except StopIteration:
181 raise IndexError
182 return res
183 else:
184 return list(iter(self))[item]
186 def __contains__(self, item):
187 if self._cache_complete:
188 return item in self._cache
189 else:
190 for i in self:
191 if i == item:
192 return True
193 elif i > item:
194 return False
195 return False
197 # __len__() introduces a large performance penalty.
198 def count(self):
199 """ Returns the number of recurrences in this set. It will have go
200 through the whole recurrence, if this hasn't been done before. """
201 if self._len is None:
202 for x in self:
203 pass
204 return self._len
206 def before(self, dt, inc=False):
207 """ Returns the last recurrence before the given datetime instance. The
208 inc keyword defines what happens if dt is an occurrence. With
209 inc=True, if dt itself is an occurrence, it will be returned. """
210 if self._cache_complete:
211 gen = self._cache
212 else:
213 gen = self
214 last = None
215 if inc:
216 for i in gen:
217 if i > dt:
218 break
219 last = i
220 else:
221 for i in gen:
222 if i >= dt:
223 break
224 last = i
225 return last
227 def after(self, dt, inc=False):
228 """ Returns the first recurrence after the given datetime instance. The
229 inc keyword defines what happens if dt is an occurrence. With
230 inc=True, if dt itself is an occurrence, it will be returned. """
231 if self._cache_complete:
232 gen = self._cache
233 else:
234 gen = self
235 if inc:
236 for i in gen:
237 if i >= dt:
238 return i
239 else:
240 for i in gen:
241 if i > dt:
242 return i
243 return None
245 def xafter(self, dt, count=None, inc=False):
246 """
247 Generator which yields up to `count` recurrences after the given
248 datetime instance, equivalent to `after`.
250 :param dt:
251 The datetime at which to start generating recurrences.
253 :param count:
254 The maximum number of recurrences to generate. If `None` (default),
255 dates are generated until the recurrence rule is exhausted.
257 :param inc:
258 If `dt` is an instance of the rule and `inc` is `True`, it is
259 included in the output.
261 :yields: Yields a sequence of `datetime` objects.
262 """
264 if self._cache_complete:
265 gen = self._cache
266 else:
267 gen = self
269 # Select the comparison function
270 if inc:
271 comp = lambda dc, dtc: dc >= dtc
272 else:
273 comp = lambda dc, dtc: dc > dtc
275 # Generate dates
276 n = 0
277 for d in gen:
278 if comp(d, dt):
279 if count is not None:
280 n += 1
281 if n > count:
282 break
284 yield d
286 def between(self, after, before, inc=False, count=1):
287 """ Returns all the occurrences of the rrule between after and before.
288 The inc keyword defines what happens if after and/or before are
289 themselves occurrences. With inc=True, they will be included in the
290 list, if they are found in the recurrence set. """
291 if self._cache_complete:
292 gen = self._cache
293 else:
294 gen = self
295 started = False
296 l = []
297 if inc:
298 for i in gen:
299 if i > before:
300 break
301 elif not started:
302 if i >= after:
303 started = True
304 l.append(i)
305 else:
306 l.append(i)
307 else:
308 for i in gen:
309 if i >= before:
310 break
311 elif not started:
312 if i > after:
313 started = True
314 l.append(i)
315 else:
316 l.append(i)
317 return l
320class rrule(rrulebase):
321 """
322 That's the base of the rrule operation. It accepts all the keywords
323 defined in the RFC as its constructor parameters (except byday,
324 which was renamed to byweekday) and more. The constructor prototype is::
326 rrule(freq)
328 Where freq must be one of YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY,
329 or SECONDLY.
331 .. note::
332 Per RFC section 3.3.10, recurrence instances falling on invalid dates
333 and times are ignored rather than coerced:
335 Recurrence rules may generate recurrence instances with an invalid
336 date (e.g., February 30) or nonexistent local time (e.g., 1:30 AM
337 on a day where the local time is moved forward by an hour at 1:00
338 AM). Such recurrence instances MUST be ignored and MUST NOT be
339 counted as part of the recurrence set.
341 This can lead to possibly surprising behavior when, for example, the
342 start date occurs at the end of the month:
344 >>> from dateutil.rrule import rrule, MONTHLY
345 >>> from datetime import datetime
346 >>> start_date = datetime(2014, 12, 31)
347 >>> list(rrule(freq=MONTHLY, count=4, dtstart=start_date))
348 ... # doctest: +NORMALIZE_WHITESPACE
349 [datetime.datetime(2014, 12, 31, 0, 0),
350 datetime.datetime(2015, 1, 31, 0, 0),
351 datetime.datetime(2015, 3, 31, 0, 0),
352 datetime.datetime(2015, 5, 31, 0, 0)]
354 Additionally, it supports the following keyword arguments:
356 :param dtstart:
357 The recurrence start. Besides being the base for the recurrence,
358 missing parameters in the final recurrence instances will also be
359 extracted from this date. If not given, datetime.now() will be used
360 instead.
361 :param interval:
362 The interval between each freq iteration. For example, when using
363 YEARLY, an interval of 2 means once every two years, but with HOURLY,
364 it means once every two hours. The default interval is 1.
365 :param wkst:
366 The week start day. Must be one of the MO, TU, WE constants, or an
367 integer, specifying the first day of the week. This will affect
368 recurrences based on weekly periods. The default week start is got
369 from calendar.firstweekday(), and may be modified by
370 calendar.setfirstweekday().
371 :param count:
372 If given, this determines how many occurrences will be generated.
374 .. note::
375 As of version 2.5.0, the use of the keyword ``until`` in conjunction
376 with ``count`` is deprecated, to make sure ``dateutil`` is fully
377 compliant with `RFC-5545 Sec. 3.3.10 <https://tools.ietf.org/
378 html/rfc5545#section-3.3.10>`_. Therefore, ``until`` and ``count``
379 **must not** occur in the same call to ``rrule``.
380 :param until:
381 If given, this must be a datetime instance specifying the upper-bound
382 limit of the recurrence. The last recurrence in the rule is the greatest
383 datetime that is less than or equal to the value specified in the
384 ``until`` parameter.
386 .. note::
387 As of version 2.5.0, the use of the keyword ``until`` in conjunction
388 with ``count`` is deprecated, to make sure ``dateutil`` is fully
389 compliant with `RFC-5545 Sec. 3.3.10 <https://tools.ietf.org/
390 html/rfc5545#section-3.3.10>`_. Therefore, ``until`` and ``count``
391 **must not** occur in the same call to ``rrule``.
392 :param bysetpos:
393 If given, it must be either an integer, or a sequence of integers,
394 positive or negative. Each given integer will specify an occurrence
395 number, corresponding to the nth occurrence of the rule inside the
396 frequency period. For example, a bysetpos of -1 if combined with a
397 MONTHLY frequency, and a byweekday of (MO, TU, WE, TH, FR), will
398 result in the last work day of every month.
399 :param bymonth:
400 If given, it must be either an integer, or a sequence of integers,
401 meaning the months to apply the recurrence to.
402 :param bymonthday:
403 If given, it must be either an integer, or a sequence of integers,
404 meaning the month days to apply the recurrence to.
405 :param byyearday:
406 If given, it must be either an integer, or a sequence of integers,
407 meaning the year days to apply the recurrence to.
408 :param byeaster:
409 If given, it must be either an integer, or a sequence of integers,
410 positive or negative. Each integer will define an offset from the
411 Easter Sunday. Passing the offset 0 to byeaster will yield the Easter
412 Sunday itself. This is an extension to the RFC specification.
413 :param byweekno:
414 If given, it must be either an integer, or a sequence of integers,
415 meaning the week numbers to apply the recurrence to. Week numbers
416 have the meaning described in ISO8601, that is, the first week of
417 the year is that containing at least four days of the new year.
418 :param byweekday:
419 If given, it must be either an integer (0 == MO), a sequence of
420 integers, one of the weekday constants (MO, TU, etc), or a sequence
421 of these constants. When given, these variables will define the
422 weekdays where the recurrence will be applied. It's also possible to
423 use an argument n for the weekday instances, which will mean the nth
424 occurrence of this weekday in the period. For example, with MONTHLY,
425 or with YEARLY and BYMONTH, using FR(+1) in byweekday will specify the
426 first friday of the month where the recurrence happens. Notice that in
427 the RFC documentation, this is specified as BYDAY, but was renamed to
428 avoid the ambiguity of that keyword.
429 :param byhour:
430 If given, it must be either an integer, or a sequence of integers,
431 meaning the hours to apply the recurrence to.
432 :param byminute:
433 If given, it must be either an integer, or a sequence of integers,
434 meaning the minutes to apply the recurrence to.
435 :param bysecond:
436 If given, it must be either an integer, or a sequence of integers,
437 meaning the seconds to apply the recurrence to.
438 :param cache:
439 If given, it must be a boolean value specifying to enable or disable
440 caching of results. If you will use the same rrule instance multiple
441 times, enabling caching will improve the performance considerably.
442 """
443 def __init__(self, freq, dtstart=None,
444 interval=1, wkst=None, count=None, until=None, bysetpos=None,
445 bymonth=None, bymonthday=None, byyearday=None, byeaster=None,
446 byweekno=None, byweekday=None,
447 byhour=None, byminute=None, bysecond=None,
448 cache=False):
449 super().__init__(cache)
450 global easter
451 if not dtstart:
452 if until and until.tzinfo:
453 dtstart = datetime.datetime.now(tz=until.tzinfo).replace(microsecond=0)
454 else:
455 dtstart = datetime.datetime.now().replace(microsecond=0)
456 elif not isinstance(dtstart, datetime.datetime):
457 dtstart = datetime.datetime.fromordinal(dtstart.toordinal())
458 else:
459 dtstart = dtstart.replace(microsecond=0)
460 self._dtstart = dtstart
461 self._tzinfo = dtstart.tzinfo
462 self._freq = freq
463 self._interval = interval
464 self._count = count
466 # Cache the original byxxx rules, if they are provided, as the _byxxx
467 # attributes do not necessarily map to the inputs, and this can be
468 # a problem in generating the strings. Only store things if they've
469 # been supplied (the string retrieval will just use .get())
470 self._original_rule = {}
472 if until and not isinstance(until, datetime.datetime):
473 until = datetime.datetime.fromordinal(until.toordinal())
474 self._until = until
476 if self._dtstart and self._until:
477 if (self._dtstart.tzinfo is not None) != (self._until.tzinfo is not None):
478 # According to RFC5545 Section 3.3.10:
479 # https://tools.ietf.org/html/rfc5545#section-3.3.10
480 #
481 # > If the "DTSTART" property is specified as a date with UTC
482 # > time or a date with local time and time zone reference,
483 # > then the UNTIL rule part MUST be specified as a date with
484 # > UTC time.
485 raise ValueError(
486 'RRULE UNTIL values must be specified in UTC when DTSTART '
487 'is timezone-aware'
488 )
490 if count is not None and until:
491 warn("Using both 'count' and 'until' is inconsistent with RFC 5545"
492 " and has been deprecated in dateutil. Future versions will "
493 "raise an error.", DeprecationWarning)
495 if wkst is None:
496 self._wkst = calendar.firstweekday()
497 elif isinstance(wkst, int):
498 self._wkst = wkst
499 else:
500 self._wkst = wkst.weekday
502 if bysetpos is None:
503 self._bysetpos = None
504 elif isinstance(bysetpos, int):
505 if bysetpos == 0 or not (-366 <= bysetpos <= 366):
506 raise ValueError("bysetpos must be between 1 and 366, "
507 "or between -366 and -1")
508 self._bysetpos = (bysetpos,)
509 else:
510 self._bysetpos = tuple(bysetpos)
511 for pos in self._bysetpos:
512 if pos == 0 or not (-366 <= pos <= 366):
513 raise ValueError("bysetpos must be between 1 and 366, "
514 "or between -366 and -1")
516 if self._bysetpos:
517 self._original_rule['bysetpos'] = self._bysetpos
519 if (byweekno is None and byyearday is None and bymonthday is None and
520 byweekday is None and byeaster is None):
521 if freq == YEARLY:
522 if bymonth is None:
523 bymonth = dtstart.month
524 self._original_rule['bymonth'] = None
525 bymonthday = dtstart.day
526 self._original_rule['bymonthday'] = None
527 elif freq == MONTHLY:
528 bymonthday = dtstart.day
529 self._original_rule['bymonthday'] = None
530 elif freq == WEEKLY:
531 byweekday = dtstart.weekday()
532 self._original_rule['byweekday'] = None
534 # bymonth
535 if bymonth is None:
536 self._bymonth = None
537 else:
538 if isinstance(bymonth, int):
539 bymonth = (bymonth,)
541 self._bymonth = tuple(sorted(set(bymonth)))
543 if 'bymonth' not in self._original_rule:
544 self._original_rule['bymonth'] = self._bymonth
546 # byyearday
547 if byyearday is None:
548 self._byyearday = None
549 else:
550 if isinstance(byyearday, int):
551 byyearday = (byyearday,)
553 self._byyearday = tuple(sorted(set(byyearday)))
554 self._original_rule['byyearday'] = self._byyearday
556 # byeaster
557 if byeaster is not None:
558 if not easter:
559 from dateutil import easter
560 if isinstance(byeaster, int):
561 self._byeaster = (byeaster,)
562 else:
563 self._byeaster = tuple(sorted(byeaster))
565 self._original_rule['byeaster'] = self._byeaster
566 else:
567 self._byeaster = None
569 # bymonthday
570 if bymonthday is None:
571 self._bymonthday = ()
572 self._bynmonthday = ()
573 else:
574 if isinstance(bymonthday, int):
575 bymonthday = (bymonthday,)
577 bymonthday = set(bymonthday) # Ensure it's unique
579 self._bymonthday = tuple(sorted(x for x in bymonthday if x > 0))
580 self._bynmonthday = tuple(sorted(x for x in bymonthday if x < 0))
582 # Storing positive numbers first, then negative numbers
583 if 'bymonthday' not in self._original_rule:
584 self._original_rule['bymonthday'] = tuple(
585 itertools.chain(self._bymonthday, self._bynmonthday))
587 # byweekno
588 if byweekno is None:
589 self._byweekno = None
590 else:
591 if isinstance(byweekno, int):
592 byweekno = (byweekno,)
594 self._byweekno = tuple(sorted(set(byweekno)))
596 self._original_rule['byweekno'] = self._byweekno
598 # byweekday / bynweekday
599 if byweekday is None:
600 self._byweekday = None
601 self._bynweekday = None
602 else:
603 # If it's one of the valid non-sequence types, convert to a
604 # single-element sequence before the iterator that builds the
605 # byweekday set.
606 if isinstance(byweekday, int) or hasattr(byweekday, "n"):
607 byweekday = (byweekday,)
609 self._byweekday = set()
610 self._bynweekday = set()
611 for wday in byweekday:
612 if isinstance(wday, int):
613 self._byweekday.add(wday)
614 elif not wday.n or freq > MONTHLY:
615 self._byweekday.add(wday.weekday)
616 else:
617 self._bynweekday.add((wday.weekday, wday.n))
619 if not self._byweekday:
620 self._byweekday = None
621 elif not self._bynweekday:
622 self._bynweekday = None
624 if self._byweekday is not None:
625 self._byweekday = tuple(sorted(self._byweekday))
626 orig_byweekday = [weekday(x) for x in self._byweekday]
627 else:
628 orig_byweekday = ()
630 if self._bynweekday is not None:
631 self._bynweekday = tuple(sorted(self._bynweekday))
632 orig_bynweekday = [weekday(*x) for x in self._bynweekday]
633 else:
634 orig_bynweekday = ()
636 if 'byweekday' not in self._original_rule:
637 self._original_rule['byweekday'] = tuple(itertools.chain(
638 orig_byweekday, orig_bynweekday))
640 # byhour
641 if byhour is None:
642 if freq < HOURLY:
643 self._byhour = {dtstart.hour}
644 else:
645 self._byhour = None
646 else:
647 if isinstance(byhour, int):
648 byhour = (byhour,)
650 if freq == HOURLY:
651 self._byhour = self.__construct_byset(start=dtstart.hour,
652 byxxx=byhour,
653 base=24)
654 else:
655 self._byhour = set(byhour)
657 self._byhour = tuple(sorted(self._byhour))
658 self._original_rule['byhour'] = self._byhour
660 # byminute
661 if byminute is None:
662 if freq < MINUTELY:
663 self._byminute = {dtstart.minute}
664 else:
665 self._byminute = None
666 else:
667 if isinstance(byminute, int):
668 byminute = (byminute,)
670 if freq == MINUTELY:
671 self._byminute = self.__construct_byset(start=dtstart.minute,
672 byxxx=byminute,
673 base=60)
674 else:
675 self._byminute = set(byminute)
677 self._byminute = tuple(sorted(self._byminute))
678 self._original_rule['byminute'] = self._byminute
680 # bysecond
681 if bysecond is None:
682 if freq < SECONDLY:
683 self._bysecond = ((dtstart.second,))
684 else:
685 self._bysecond = None
686 else:
687 if isinstance(bysecond, int):
688 bysecond = (bysecond,)
690 self._bysecond = set(bysecond)
692 if freq == SECONDLY:
693 self._bysecond = self.__construct_byset(start=dtstart.second,
694 byxxx=bysecond,
695 base=60)
696 else:
697 self._bysecond = set(bysecond)
699 self._bysecond = tuple(sorted(self._bysecond))
700 self._original_rule['bysecond'] = self._bysecond
702 if self._freq >= HOURLY:
703 self._timeset = None
704 else:
705 self._timeset = []
706 for hour in self._byhour:
707 for minute in self._byminute:
708 for second in self._bysecond:
709 self._timeset.append(
710 datetime.time(hour, minute, second,
711 tzinfo=self._tzinfo))
712 self._timeset.sort()
713 self._timeset = tuple(self._timeset)
715 def __str__(self):
716 """
717 Output a string that would generate this RRULE if passed to rrulestr.
718 This is mostly compatible with RFC5545, except for the
719 dateutil-specific extension BYEASTER.
720 """
722 output = []
723 h, m, s = [None] * 3
724 if self._dtstart:
725 output.append(self._dtstart.strftime('DTSTART:%Y%m%dT%H%M%S'))
726 h, m, s = self._dtstart.timetuple()[3:6]
728 parts = ['FREQ=' + FREQNAMES[self._freq]]
729 if self._interval != 1:
730 parts.append('INTERVAL=' + str(self._interval))
732 if self._wkst:
733 parts.append('WKST=' + repr(weekday(self._wkst))[0:2])
735 if self._count is not None:
736 parts.append('COUNT=' + str(self._count))
738 if self._until:
739 parts.append(self._until.strftime('UNTIL=%Y%m%dT%H%M%S'))
741 if self._original_rule.get('byweekday') is not None:
742 # The str() method on weekday objects doesn't generate
743 # RFC5545-compliant strings, so we should modify that.
744 original_rule = dict(self._original_rule)
745 wday_strings = []
746 for wday in original_rule['byweekday']:
747 if wday.n:
748 wday_strings.append('{n:+d}{wday}'.format(
749 n=wday.n,
750 wday=repr(wday)[0:2]))
751 else:
752 wday_strings.append(repr(wday))
754 original_rule['byweekday'] = wday_strings
755 else:
756 original_rule = self._original_rule
758 partfmt = '{name}={vals}'
759 for name, key in [('BYSETPOS', 'bysetpos'),
760 ('BYMONTH', 'bymonth'),
761 ('BYMONTHDAY', 'bymonthday'),
762 ('BYYEARDAY', 'byyearday'),
763 ('BYWEEKNO', 'byweekno'),
764 ('BYDAY', 'byweekday'),
765 ('BYHOUR', 'byhour'),
766 ('BYMINUTE', 'byminute'),
767 ('BYSECOND', 'bysecond'),
768 ('BYEASTER', 'byeaster')]:
769 value = original_rule.get(key)
770 if value:
771 parts.append(partfmt.format(name=name, vals=(','.join(str(v)
772 for v in value))))
774 output.append('RRULE:' + ';'.join(parts))
775 return '\n'.join(output)
777 def replace(self, **kwargs):
778 """Return new rrule with same attributes except for those attributes given new
779 values by whichever keyword arguments are specified."""
780 new_kwargs = {"interval": self._interval,
781 "count": self._count,
782 "dtstart": self._dtstart,
783 "freq": self._freq,
784 "until": self._until,
785 "wkst": self._wkst,
786 "cache": False if self._cache is None else True }
787 new_kwargs.update(self._original_rule)
788 new_kwargs.update(kwargs)
789 return rrule(**new_kwargs)
791 def _iter(self):
792 year, month, day, hour, minute, second, weekday, yearday, _ = \
793 self._dtstart.timetuple()
795 # Some local variables to speed things up a bit
796 freq = self._freq
797 interval = self._interval
798 wkst = self._wkst
799 until = self._until
800 bymonth = self._bymonth
801 byweekno = self._byweekno
802 byyearday = self._byyearday
803 byweekday = self._byweekday
804 byeaster = self._byeaster
805 bymonthday = self._bymonthday
806 bynmonthday = self._bynmonthday
807 bysetpos = self._bysetpos
808 byhour = self._byhour
809 byminute = self._byminute
810 bysecond = self._bysecond
812 ii = _iterinfo(self)
813 ii.rebuild(year, month)
815 getdayset = {YEARLY: ii.ydayset,
816 MONTHLY: ii.mdayset,
817 WEEKLY: ii.wdayset,
818 DAILY: ii.ddayset,
819 HOURLY: ii.ddayset,
820 MINUTELY: ii.ddayset,
821 SECONDLY: ii.ddayset}[freq]
823 if freq < HOURLY:
824 timeset = self._timeset
825 else:
826 gettimeset = {HOURLY: ii.htimeset,
827 MINUTELY: ii.mtimeset,
828 SECONDLY: ii.stimeset}[freq]
829 if ((freq >= HOURLY and
830 self._byhour and hour not in self._byhour) or
831 (freq >= MINUTELY and
832 self._byminute and minute not in self._byminute) or
833 (freq >= SECONDLY and
834 self._bysecond and second not in self._bysecond)):
835 timeset = ()
836 else:
837 timeset = gettimeset(hour, minute, second)
839 total = 0
840 count = self._count
841 while True:
842 # Get dayset with the right frequency
843 dayset, start, end = getdayset(year, month, day)
845 # Do the "hard" work ;-)
846 filtered = False
847 for i in dayset[start:end]:
848 if ((bymonth and ii.mmask[i] not in bymonth) or
849 (byweekno and not ii.wnomask[i]) or
850 (byweekday and ii.wdaymask[i] not in byweekday) or
851 (ii.nwdaymask and not ii.nwdaymask[i]) or
852 (byeaster and not ii.eastermask[i]) or
853 ((bymonthday or bynmonthday) and
854 ii.mdaymask[i] not in bymonthday and
855 ii.nmdaymask[i] not in bynmonthday) or
856 (byyearday and
857 ((i < ii.yearlen and i+1 not in byyearday and
858 -ii.yearlen+i not in byyearday) or
859 (i >= ii.yearlen and i+1-ii.yearlen not in byyearday and
860 -ii.nextyearlen+i-ii.yearlen not in byyearday)))):
861 dayset[i] = None
862 filtered = True
864 # Output results
865 if bysetpos and timeset:
866 poslist = []
867 for pos in bysetpos:
868 if pos < 0:
869 daypos, timepos = divmod(pos, len(timeset))
870 else:
871 daypos, timepos = divmod(pos-1, len(timeset))
872 try:
873 i = [x for x in dayset[start:end]
874 if x is not None][daypos]
875 time = timeset[timepos]
876 except IndexError:
877 pass
878 else:
879 date = datetime.date.fromordinal(ii.yearordinal+i)
880 res = datetime.datetime.combine(date, time)
881 if res not in poslist:
882 poslist.append(res)
883 poslist.sort()
884 for res in poslist:
885 if until and res > until:
886 self._len = total
887 return
888 elif res >= self._dtstart:
889 if count is not None:
890 count -= 1
891 if count < 0:
892 self._len = total
893 return
894 total += 1
895 yield res
896 else:
897 for i in dayset[start:end]:
898 if i is not None:
899 date = datetime.date.fromordinal(ii.yearordinal + i)
900 for time in timeset:
901 res = datetime.datetime.combine(date, time)
902 if until and res > until:
903 self._len = total
904 return
905 elif res >= self._dtstart:
906 if count is not None:
907 count -= 1
908 if count < 0:
909 self._len = total
910 return
912 total += 1
913 yield res
915 # Handle frequency and interval
916 fixday = False
917 if freq == YEARLY:
918 year += interval
919 if year > datetime.MAXYEAR:
920 self._len = total
921 return
922 ii.rebuild(year, month)
923 elif freq == MONTHLY:
924 month += interval
925 if month > 12:
926 div, mod = divmod(month, 12)
927 month = mod
928 year += div
929 if month == 0:
930 month = 12
931 year -= 1
932 if year > datetime.MAXYEAR:
933 self._len = total
934 return
935 ii.rebuild(year, month)
936 elif freq == WEEKLY:
937 if wkst > weekday:
938 day += -(weekday+1+(6-wkst))+self._interval*7
939 else:
940 day += -(weekday-wkst)+self._interval*7
941 weekday = wkst
942 fixday = True
943 elif freq == DAILY:
944 day += interval
945 fixday = True
946 elif freq == HOURLY:
947 if filtered:
948 # Jump to one iteration before next day
949 hour += ((23-hour)//interval)*interval
951 if byhour:
952 ndays, hour = self.__mod_distance(value=hour,
953 byxxx=self._byhour,
954 base=24)
955 else:
956 ndays, hour = divmod(hour+interval, 24)
958 if ndays:
959 day += ndays
960 fixday = True
962 timeset = gettimeset(hour, minute, second)
963 elif freq == MINUTELY:
964 if filtered:
965 # Jump to one iteration before next day
966 minute += ((1439-(hour*60+minute))//interval)*interval
968 valid = False
969 rep_rate = (24*60)
970 for j in range(rep_rate // math.gcd(interval, rep_rate)):
971 if byminute:
972 nhours, minute = \
973 self.__mod_distance(value=minute,
974 byxxx=self._byminute,
975 base=60)
976 else:
977 nhours, minute = divmod(minute+interval, 60)
979 div, hour = divmod(hour+nhours, 24)
980 if div:
981 day += div
982 fixday = True
983 filtered = False
985 if not byhour or hour in byhour:
986 valid = True
987 break
989 if not valid:
990 raise ValueError('Invalid combination of interval and ' +
991 'byhour resulting in empty rule.')
993 timeset = gettimeset(hour, minute, second)
994 elif freq == SECONDLY:
995 if filtered:
996 # Jump to one iteration before next day
997 second += (((86399 - (hour * 3600 + minute * 60 + second))
998 // interval) * interval)
1000 rep_rate = (24 * 3600)
1001 valid = False
1002 for j in range(0, rep_rate // math.gcd(interval, rep_rate)):
1003 if bysecond:
1004 nminutes, second = \
1005 self.__mod_distance(value=second,
1006 byxxx=self._bysecond,
1007 base=60)
1008 else:
1009 nminutes, second = divmod(second+interval, 60)
1011 div, minute = divmod(minute+nminutes, 60)
1012 if div:
1013 hour += div
1014 div, hour = divmod(hour, 24)
1015 if div:
1016 day += div
1017 fixday = True
1019 if ((not byhour or hour in byhour) and
1020 (not byminute or minute in byminute) and
1021 (not bysecond or second in bysecond)):
1022 valid = True
1023 break
1025 if not valid:
1026 raise ValueError('Invalid combination of interval, ' +
1027 'byhour and byminute resulting in empty' +
1028 ' rule.')
1030 timeset = gettimeset(hour, minute, second)
1032 if fixday and day > 28:
1033 daysinmonth = calendar.monthrange(year, month)[1]
1034 if day > daysinmonth:
1035 while day > daysinmonth:
1036 day -= daysinmonth
1037 month += 1
1038 if month == 13:
1039 month = 1
1040 year += 1
1041 if year > datetime.MAXYEAR:
1042 self._len = total
1043 return
1044 daysinmonth = calendar.monthrange(year, month)[1]
1045 ii.rebuild(year, month)
1047 def __construct_byset(self, start, byxxx, base):
1048 """
1049 If a `BYXXX` sequence is passed to the constructor at the same level as
1050 `FREQ` (e.g. `FREQ=HOURLY,BYHOUR={2,4,7},INTERVAL=3`), there are some
1051 specifications which cannot be reached given some starting conditions.
1053 This occurs whenever the interval is not coprime with the base of a
1054 given unit and the difference between the starting position and the
1055 ending position is not coprime with the greatest common denominator
1056 between the interval and the base. For example, with a FREQ of hourly
1057 starting at 17:00 and an interval of 4, the only valid values for
1058 BYHOUR would be {21, 1, 5, 9, 13, 17}, because 4 and 24 are not
1059 coprime.
1061 :param start:
1062 Specifies the starting position.
1063 :param byxxx:
1064 An iterable containing the list of allowed values.
1065 :param base:
1066 The largest allowable value for the specified frequency (e.g.
1067 24 hours, 60 minutes).
1069 This does not preserve the type of the iterable, returning a set, since
1070 the values should be unique and the order is irrelevant, this will
1071 speed up later lookups.
1073 In the event of an empty set, raises a :exception:`ValueError`, as this
1074 results in an empty rrule.
1075 """
1077 cset = set()
1079 # Support a single byxxx value.
1080 if isinstance(byxxx, int):
1081 byxxx = (byxxx, )
1083 for num in byxxx:
1084 i_gcd = math.gcd(self._interval, base)
1085 # Use divmod rather than % because we need to wrap negative nums.
1086 if i_gcd == 1 or divmod(num - start, i_gcd)[1] == 0:
1087 cset.add(num)
1089 if len(cset) == 0:
1090 raise ValueError("Invalid rrule byxxx generates an empty set.")
1092 return cset
1094 def __mod_distance(self, value, byxxx, base):
1095 """
1096 Calculates the next value in a sequence where the `FREQ` parameter is
1097 specified along with a `BYXXX` parameter at the same "level"
1098 (e.g. `HOURLY` specified with `BYHOUR`).
1100 :param value:
1101 The old value of the component.
1102 :param byxxx:
1103 The `BYXXX` set, which should have been generated by
1104 `rrule._construct_byset`, or something else which checks that a
1105 valid rule is present.
1106 :param base:
1107 The largest allowable value for the specified frequency (e.g.
1108 24 hours, 60 minutes).
1110 If a valid value is not found after `base` iterations (the maximum
1111 number before the sequence would start to repeat), this raises a
1112 :exception:`ValueError`, as no valid values were found.
1114 This returns a tuple of `divmod(n*interval, base)`, where `n` is the
1115 smallest number of `interval` repetitions until the next specified
1116 value in `byxxx` is found.
1117 """
1118 accumulator = 0
1119 for ii in range(1, base + 1):
1120 # Using divmod() over % to account for negative intervals
1121 div, value = divmod(value + self._interval, base)
1122 accumulator += div
1123 if value in byxxx:
1124 return (accumulator, value)
1127class _iterinfo:
1128 __slots__ = ["rrule", "lastyear", "lastmonth",
1129 "yearlen", "nextyearlen", "yearordinal", "yearweekday",
1130 "mmask", "mrange", "mdaymask", "nmdaymask",
1131 "wdaymask", "wnomask", "nwdaymask", "eastermask"]
1133 def __init__(self, rrule):
1134 for attr in self.__slots__:
1135 setattr(self, attr, None)
1136 self.rrule = rrule
1138 def rebuild(self, year, month):
1139 # Every mask is 7 days longer to handle cross-year weekly periods.
1140 rr = self.rrule
1141 if year != self.lastyear:
1142 self.yearlen = 365 + calendar.isleap(year)
1143 self.nextyearlen = 365 + calendar.isleap(year + 1)
1144 firstyday = datetime.date(year, 1, 1)
1145 self.yearordinal = firstyday.toordinal()
1146 self.yearweekday = firstyday.weekday()
1148 wday = datetime.date(year, 1, 1).weekday()
1149 if self.yearlen == 365:
1150 self.mmask = M365MASK
1151 self.mdaymask = MDAY365MASK
1152 self.nmdaymask = NMDAY365MASK
1153 self.wdaymask = WDAYMASK[wday:]
1154 self.mrange = M365RANGE
1155 else:
1156 self.mmask = M366MASK
1157 self.mdaymask = MDAY366MASK
1158 self.nmdaymask = NMDAY366MASK
1159 self.wdaymask = WDAYMASK[wday:]
1160 self.mrange = M366RANGE
1162 if not rr._byweekno:
1163 self.wnomask = None
1164 else:
1165 self.wnomask = [0]*(self.yearlen+7)
1166 # no1wkst = firstwkst = self.wdaymask.index(rr._wkst)
1167 no1wkst = firstwkst = (7-self.yearweekday+rr._wkst) % 7
1168 if no1wkst >= 4:
1169 no1wkst = 0
1170 # Number of days in the year, plus the days we got
1171 # from last year.
1172 wyearlen = self.yearlen+(self.yearweekday-rr._wkst) % 7
1173 else:
1174 # Number of days in the year, minus the days we
1175 # left in last year.
1176 wyearlen = self.yearlen-no1wkst
1177 div, mod = divmod(wyearlen, 7)
1178 numweeks = div+mod//4
1179 for n in rr._byweekno:
1180 if n < 0:
1181 n += numweeks+1
1182 if not (0 < n <= numweeks):
1183 continue
1184 if n > 1:
1185 i = no1wkst+(n-1)*7
1186 if no1wkst != firstwkst:
1187 i -= 7-firstwkst
1188 else:
1189 i = no1wkst
1190 for j in range(7):
1191 self.wnomask[i] = 1
1192 i += 1
1193 if self.wdaymask[i] == rr._wkst:
1194 break
1195 if 1 in rr._byweekno:
1196 # Check week number 1 of next year as well
1197 # TODO: Check -numweeks for next year.
1198 i = no1wkst+numweeks*7
1199 if no1wkst != firstwkst:
1200 i -= 7-firstwkst
1201 if i < self.yearlen:
1202 # If week starts in next year, we
1203 # don't care about it.
1204 for j in range(7):
1205 self.wnomask[i] = 1
1206 i += 1
1207 if self.wdaymask[i] == rr._wkst:
1208 break
1209 if no1wkst:
1210 # Check last week number of last year as
1211 # well. If no1wkst is 0, either the year
1212 # started on week start, or week number 1
1213 # got days from last year, so there are no
1214 # days from last year's last week number in
1215 # this year.
1216 if -1 not in rr._byweekno:
1217 lyearweekday = datetime.date(year-1, 1, 1).weekday()
1218 lno1wkst = (7-lyearweekday+rr._wkst) % 7
1219 lyearlen = 365+calendar.isleap(year-1)
1220 if lno1wkst >= 4:
1221 lno1wkst = 0
1222 lnumweeks = 52+(lyearlen +
1223 (lyearweekday-rr._wkst) % 7) % 7//4
1224 else:
1225 lnumweeks = 52+(self.yearlen-no1wkst) % 7//4
1226 else:
1227 lnumweeks = -1
1228 if lnumweeks in rr._byweekno:
1229 for i in range(no1wkst):
1230 self.wnomask[i] = 1
1232 if (rr._bynweekday and (month != self.lastmonth or
1233 year != self.lastyear)):
1234 ranges = []
1235 if rr._freq == YEARLY:
1236 if rr._bymonth:
1237 for month in rr._bymonth:
1238 ranges.append(self.mrange[month-1:month+1])
1239 else:
1240 ranges = [(0, self.yearlen)]
1241 elif rr._freq == MONTHLY:
1242 ranges = [self.mrange[month-1:month+1]]
1243 if ranges:
1244 # Weekly frequency won't get here, so we may not
1245 # care about cross-year weekly periods.
1246 self.nwdaymask = [0]*self.yearlen
1247 for first, last in ranges:
1248 last -= 1
1249 for wday, n in rr._bynweekday:
1250 if n < 0:
1251 i = last+(n+1)*7
1252 i -= (self.wdaymask[i]-wday) % 7
1253 else:
1254 i = first+(n-1)*7
1255 i += (7-self.wdaymask[i]+wday) % 7
1256 if first <= i <= last:
1257 self.nwdaymask[i] = 1
1259 if rr._byeaster:
1260 self.eastermask = [0]*(self.yearlen+7)
1261 eyday = easter.easter(year).toordinal()-self.yearordinal
1262 for offset in rr._byeaster:
1263 self.eastermask[eyday+offset] = 1
1265 self.lastyear = year
1266 self.lastmonth = month
1268 def ydayset(self, year, month, day):
1269 return list(range(self.yearlen)), 0, self.yearlen
1271 def mdayset(self, year, month, day):
1272 dset = [None]*self.yearlen
1273 start, end = self.mrange[month-1:month+1]
1274 for i in range(start, end):
1275 dset[i] = i
1276 return dset, start, end
1278 def wdayset(self, year, month, day):
1279 # We need to handle cross-year weeks here.
1280 dset = [None]*(self.yearlen+7)
1281 i = datetime.date(year, month, day).toordinal()-self.yearordinal
1282 start = i
1283 for j in range(7):
1284 dset[i] = i
1285 i += 1
1286 # if (not (0 <= i < self.yearlen) or
1287 # self.wdaymask[i] == self.rrule._wkst):
1288 # This will cross the year boundary, if necessary.
1289 if self.wdaymask[i] == self.rrule._wkst:
1290 break
1291 return dset, start, i
1293 def ddayset(self, year, month, day):
1294 dset = [None] * self.yearlen
1295 i = datetime.date(year, month, day).toordinal() - self.yearordinal
1296 dset[i] = i
1297 return dset, i, i + 1
1299 def htimeset(self, hour, minute, second):
1300 tset = []
1301 rr = self.rrule
1302 for minute in rr._byminute:
1303 for second in rr._bysecond:
1304 tset.append(datetime.time(hour, minute, second,
1305 tzinfo=rr._tzinfo))
1306 tset.sort()
1307 return tset
1309 def mtimeset(self, hour, minute, second):
1310 tset = []
1311 rr = self.rrule
1312 for second in rr._bysecond:
1313 tset.append(datetime.time(hour, minute, second, tzinfo=rr._tzinfo))
1314 tset.sort()
1315 return tset
1317 def stimeset(self, hour, minute, second):
1318 return (datetime.time(hour, minute, second,
1319 tzinfo=self.rrule._tzinfo),)
1322class rruleset(rrulebase):
1323 """ The rruleset type allows more complex recurrence setups, mixing
1324 multiple rules, dates, exclusion rules, and exclusion dates. The type
1325 constructor takes the following keyword arguments:
1327 :param cache: If True, caching of results will be enabled, improving
1328 performance of multiple queries considerably. """
1330 class _genitem:
1331 def __init__(self, genlist, gen):
1332 try:
1333 self.dt = next(gen)
1334 genlist.append(self)
1335 except StopIteration:
1336 pass
1337 self.genlist = genlist
1338 self.gen = gen
1340 def __next__(self):
1341 try:
1342 self.dt = next(self.gen)
1343 except StopIteration:
1344 if self.genlist[0] is self:
1345 heapq.heappop(self.genlist)
1346 else:
1347 self.genlist.remove(self)
1348 heapq.heapify(self.genlist)
1351 def __lt__(self, other):
1352 return self.dt < other.dt
1354 def __gt__(self, other):
1355 return self.dt > other.dt
1357 def __eq__(self, other):
1358 return self.dt == other.dt
1360 def __ne__(self, other):
1361 return self.dt != other.dt
1363 def __init__(self, cache=False):
1364 super().__init__(cache)
1365 self._rrule = []
1366 self._rdate = []
1367 self._exrule = []
1368 self._exdate = []
1370 @_invalidates_cache
1371 def rrule(self, rrule):
1372 """ Include the given :py:class:`rrule` instance in the recurrence set
1373 generation. """
1374 self._rrule.append(rrule)
1376 @_invalidates_cache
1377 def rdate(self, rdate):
1378 """ Include the given :py:class:`datetime` instance in the recurrence
1379 set generation. """
1380 self._rdate.append(rdate)
1382 @_invalidates_cache
1383 def exrule(self, exrule):
1384 """ Include the given rrule instance in the recurrence set exclusion
1385 list. Dates which are part of the given recurrence rules will not
1386 be generated, even if some inclusive rrule or rdate matches them.
1387 """
1388 self._exrule.append(exrule)
1390 @_invalidates_cache
1391 def exdate(self, exdate):
1392 """ Include the given datetime instance in the recurrence set
1393 exclusion list. Dates included that way will not be generated,
1394 even if some inclusive rrule or rdate matches them. """
1395 self._exdate.append(exdate)
1397 def _iter(self):
1398 rlist = []
1399 self._rdate.sort()
1400 self._genitem(rlist, iter(self._rdate))
1401 for gen in [iter(x) for x in self._rrule]:
1402 self._genitem(rlist, gen)
1403 exlist = []
1404 self._exdate.sort()
1405 self._genitem(exlist, iter(self._exdate))
1406 for gen in [iter(x) for x in self._exrule]:
1407 self._genitem(exlist, gen)
1408 lastdt = None
1409 total = 0
1410 heapq.heapify(rlist)
1411 heapq.heapify(exlist)
1412 while rlist:
1413 ritem = rlist[0]
1414 if not lastdt or lastdt != ritem.dt:
1415 while exlist and exlist[0] < ritem:
1416 exitem = exlist[0]
1417 next(exitem)
1418 if exlist and exlist[0] is exitem:
1419 heapq.heapreplace(exlist, exitem)
1420 if not exlist or ritem != exlist[0]:
1421 total += 1
1422 yield ritem.dt
1423 lastdt = ritem.dt
1424 next(ritem)
1425 if rlist and rlist[0] is ritem:
1426 heapq.heapreplace(rlist, ritem)
1427 self._len = total
1430class _rrulestr:
1431 """ Parses a string representation of a recurrence rule or set of
1432 recurrence rules.
1434 :param s:
1435 Required, a string defining one or more recurrence rules.
1437 :param dtstart:
1438 If given, used as the default recurrence start if not specified in the
1439 rule string.
1441 :param cache:
1442 If set ``True`` caching of results will be enabled, improving
1443 performance of multiple queries considerably.
1445 :param unfold:
1446 If set ``True`` indicates that a rule string is split over more
1447 than one line and should be joined before processing.
1449 :param forceset:
1450 If set ``True`` forces a :class:`dateutil.rrule.rruleset` to
1451 be returned.
1453 :param compatible:
1454 If set ``True`` forces ``unfold`` and ``forceset`` to be ``True``.
1456 :param ignoretz:
1457 If set ``True``, time zones in parsed strings are ignored and a naive
1458 :class:`datetime.datetime` object is returned.
1460 :param tzids:
1461 If given, a callable or mapping used to retrieve a
1462 :class:`datetime.tzinfo` from a string representation.
1463 Defaults to :func:`dateutil.tz.gettz`.
1465 :param tzinfos:
1466 Additional time zone names / aliases which may be present in a string
1467 representation. See :func:`dateutil.parser.parse` for more
1468 information.
1470 :return:
1471 Returns a :class:`dateutil.rrule.rruleset` or
1472 :class:`dateutil.rrule.rrule`
1473 """
1475 _freq_map = {"YEARLY": YEARLY,
1476 "MONTHLY": MONTHLY,
1477 "WEEKLY": WEEKLY,
1478 "DAILY": DAILY,
1479 "HOURLY": HOURLY,
1480 "MINUTELY": MINUTELY,
1481 "SECONDLY": SECONDLY}
1483 _weekday_map = {"MO": 0, "TU": 1, "WE": 2, "TH": 3,
1484 "FR": 4, "SA": 5, "SU": 6}
1486 def _handle_int(self, rrkwargs, name, value, **kwargs):
1487 rrkwargs[name.lower()] = int(value)
1489 def _handle_int_list(self, rrkwargs, name, value, **kwargs):
1490 rrkwargs[name.lower()] = [int(x) for x in value.split(',')]
1492 _handle_INTERVAL = _handle_int
1493 _handle_COUNT = _handle_int
1494 _handle_BYSETPOS = _handle_int_list
1495 _handle_BYMONTH = _handle_int_list
1496 _handle_BYMONTHDAY = _handle_int_list
1497 _handle_BYYEARDAY = _handle_int_list
1498 _handle_BYEASTER = _handle_int_list
1499 _handle_BYWEEKNO = _handle_int_list
1500 _handle_BYHOUR = _handle_int_list
1501 _handle_BYMINUTE = _handle_int_list
1502 _handle_BYSECOND = _handle_int_list
1504 def _handle_FREQ(self, rrkwargs, name, value, **kwargs):
1505 rrkwargs["freq"] = self._freq_map[value]
1507 def _handle_UNTIL(self, rrkwargs, name, value, **kwargs):
1508 global parser
1509 if not parser:
1510 from dateutil import parser
1511 try:
1512 rrkwargs["until"] = parser.parse(value,
1513 ignoretz=kwargs.get("ignoretz"),
1514 tzinfos=kwargs.get("tzinfos"))
1515 except ValueError:
1516 raise ValueError("invalid until date")
1518 def _handle_WKST(self, rrkwargs, name, value, **kwargs):
1519 rrkwargs["wkst"] = self._weekday_map[value]
1521 def _handle_BYWEEKDAY(self, rrkwargs, name, value, **kwargs):
1522 """
1523 Two ways to specify this: +1MO or MO(+1)
1524 """
1525 l = []
1526 for wday in value.split(','):
1527 if '(' in wday:
1528 # If it's of the form TH(+1), etc.
1529 splt = wday.split('(')
1530 w = splt[0]
1531 n = int(splt[1][:-1])
1532 elif len(wday):
1533 # If it's of the form +1MO
1534 for i in range(len(wday)):
1535 if wday[i] not in '+-0123456789':
1536 break
1537 n = wday[:i] or None
1538 w = wday[i:]
1539 if n:
1540 n = int(n)
1541 else:
1542 raise ValueError("Invalid (empty) BYDAY specification.")
1544 l.append(weekdays[self._weekday_map[w]](n))
1545 rrkwargs["byweekday"] = l
1547 _handle_BYDAY = _handle_BYWEEKDAY
1549 def _parse_rfc_rrule(self, line,
1550 dtstart=None,
1551 cache=False,
1552 ignoretz=False,
1553 tzinfos=None):
1554 if line.find(':') != -1:
1555 name, value = line.split(':')
1556 if name != "RRULE":
1557 raise ValueError("unknown parameter name")
1558 else:
1559 value = line
1560 rrkwargs = {}
1561 for pair in value.split(';'):
1562 name, value = pair.split('=')
1563 name = name.upper()
1564 value = value.upper()
1565 try:
1566 getattr(self, "_handle_"+name)(rrkwargs, name, value,
1567 ignoretz=ignoretz,
1568 tzinfos=tzinfos)
1569 except AttributeError:
1570 raise ValueError("unknown parameter '%s'" % name)
1571 except (KeyError, ValueError):
1572 raise ValueError("invalid '%s': %s" % (name, value))
1573 return rrule(dtstart=dtstart, cache=cache, **rrkwargs)
1575 def _parse_date_value(self, date_value, parms, rule_tzids,
1576 ignoretz, tzids, tzinfos):
1577 global parser
1578 if not parser:
1579 from dateutil import parser
1581 datevals = []
1582 value_found = False
1583 TZID = None
1585 for parm in parms:
1586 if parm.startswith("TZID="):
1587 try:
1588 tzkey = rule_tzids[parm.split('TZID=')[-1]]
1589 except KeyError:
1590 continue
1591 if tzids is None:
1592 from . import tz
1593 tzlookup = tz.gettz
1594 elif callable(tzids):
1595 tzlookup = tzids
1596 else:
1597 tzlookup = getattr(tzids, 'get', None)
1598 if tzlookup is None:
1599 msg = ('tzids must be a callable, mapping, or None, '
1600 'not %s' % tzids)
1601 raise ValueError(msg)
1603 TZID = tzlookup(tzkey)
1604 continue
1606 # RFC 5445 3.8.2.4: The VALUE parameter is optional, but may be found
1607 # only once.
1608 if parm not in {"VALUE=DATE-TIME", "VALUE=DATE"}:
1609 raise ValueError("unsupported parm: " + parm)
1610 else:
1611 if value_found:
1612 msg = ("Duplicate value parameter found in: " + parm)
1613 raise ValueError(msg)
1614 value_found = True
1616 for datestr in date_value.split(','):
1617 date = parser.parse(datestr, ignoretz=ignoretz, tzinfos=tzinfos)
1618 if TZID is not None:
1619 if date.tzinfo is None:
1620 date = date.replace(tzinfo=TZID)
1621 else:
1622 raise ValueError('DTSTART/EXDATE specifies multiple timezone')
1623 datevals.append(date)
1625 return datevals
1627 def _parse_rfc(self, s,
1628 dtstart=None,
1629 cache=False,
1630 unfold=False,
1631 forceset=False,
1632 compatible=False,
1633 ignoretz=False,
1634 tzids=None,
1635 tzinfos=None):
1636 global parser
1637 if compatible:
1638 forceset = True
1639 unfold = True
1641 TZID_NAMES = dict(map(
1642 lambda x: (x.upper(), x),
1643 re.findall('TZID=(?P<name>[^:]+):', s)
1644 ))
1645 s = s.upper()
1646 if not s.strip():
1647 raise ValueError("empty string")
1648 if unfold:
1649 lines = s.splitlines()
1650 i = 0
1651 while i < len(lines):
1652 line = lines[i].rstrip()
1653 if not line:
1654 del lines[i]
1655 elif i > 0 and line[0] == " ":
1656 lines[i-1] += line[1:]
1657 del lines[i]
1658 else:
1659 i += 1
1660 else:
1661 lines = s.split()
1662 if (not forceset and len(lines) == 1 and (s.find(':') == -1 or
1663 s.startswith('RRULE:'))):
1664 return self._parse_rfc_rrule(lines[0], cache=cache,
1665 dtstart=dtstart, ignoretz=ignoretz,
1666 tzinfos=tzinfos)
1667 else:
1668 rrulevals = []
1669 rdatevals = []
1670 exrulevals = []
1671 exdatevals = []
1672 for line in lines:
1673 if not line:
1674 continue
1675 if line.find(':') == -1:
1676 name = "RRULE"
1677 value = line
1678 else:
1679 name, value = line.split(':', 1)
1680 parms = name.split(';')
1681 if not parms:
1682 raise ValueError("empty property name")
1683 name = parms[0]
1684 parms = parms[1:]
1685 if name == "RRULE":
1686 for parm in parms:
1687 if parm.startswith("X-"):
1688 # Experimental random vendor features; safe to ignore
1689 continue
1690 raise ValueError("unsupported RRULE parm: " + parm)
1691 rrulevals.append(value)
1692 elif name == "RDATE":
1693 for parm in parms:
1694 if parm != "VALUE=DATE-TIME":
1695 raise ValueError("unsupported RDATE parm: "+parm)
1696 rdatevals.append(value)
1697 elif name == "EXRULE":
1698 for parm in parms:
1699 raise ValueError("unsupported EXRULE parm: "+parm)
1700 exrulevals.append(value)
1701 elif name == "EXDATE":
1702 exdatevals.extend(
1703 self._parse_date_value(value, parms,
1704 TZID_NAMES, ignoretz,
1705 tzids, tzinfos)
1706 )
1707 elif name == "DTSTART":
1708 dtvals = self._parse_date_value(value, parms, TZID_NAMES,
1709 ignoretz, tzids, tzinfos)
1710 if len(dtvals) != 1:
1711 raise ValueError("Multiple DTSTART values specified:" +
1712 value)
1713 dtstart = dtvals[0]
1714 else:
1715 raise ValueError("unsupported property: "+name)
1716 if (forceset or len(rrulevals) > 1 or rdatevals
1717 or exrulevals or exdatevals):
1718 if not parser and (rdatevals or exdatevals):
1719 from dateutil import parser
1720 rset = rruleset(cache=cache)
1721 for value in rrulevals:
1722 rset.rrule(self._parse_rfc_rrule(value, dtstart=dtstart,
1723 ignoretz=ignoretz,
1724 tzinfos=tzinfos))
1725 for value in rdatevals:
1726 for datestr in value.split(','):
1727 rset.rdate(parser.parse(datestr,
1728 ignoretz=ignoretz,
1729 tzinfos=tzinfos))
1730 for value in exrulevals:
1731 rset.exrule(self._parse_rfc_rrule(value, dtstart=dtstart,
1732 ignoretz=ignoretz,
1733 tzinfos=tzinfos))
1734 for value in exdatevals:
1735 rset.exdate(value)
1736 if compatible and dtstart:
1737 rset.rdate(dtstart)
1738 return rset
1739 else:
1740 return self._parse_rfc_rrule(rrulevals[0],
1741 dtstart=dtstart,
1742 cache=cache,
1743 ignoretz=ignoretz,
1744 tzinfos=tzinfos)
1746 def __call__(self, s, **kwargs):
1747 return self._parse_rfc(s, **kwargs)
1750rrulestr = _rrulestr()
1752# vim:ts=4:sw=4:et