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