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