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