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