comparison CSP2/CSP2_env/env-d9b9114564458d9d-741b3de822f2aaca6c6caa4325c4afce/lib/python3.8/site-packages/dateutil/rrule.py @ 69:33d812a61356

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