Mercurial > repos > rliterman > csp2
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 |