comparison env/lib/python3.9/site-packages/dateutil/rrule.py @ 0:4f3585e2f14b draft default tip

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