1 #!/usr/bin/env python 2 3 """ 4 Recurrence rule calculation. 5 6 Copyright (C) 2014, 2015, 2017 Paul Boddie <paul@boddie.org.uk> 7 8 This program is free software; you can redistribute it and/or modify it under 9 the terms of the GNU General Public License as published by the Free Software 10 Foundation; either version 3 of the License, or (at your option) any later 11 version. 12 13 This program is distributed in the hope that it will be useful, but WITHOUT 14 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 15 FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 16 details. 17 18 You should have received a copy of the GNU General Public License along with 19 this program. If not, see <http://www.gnu.org/licenses/>. 20 21 ---- 22 23 References: 24 25 RFC 5545: Internet Calendaring and Scheduling Core Object Specification 26 (iCalendar) 27 http://tools.ietf.org/html/rfc5545 28 29 ---- 30 31 FREQ defines the selection resolution. 32 DTSTART defines the start of the selection. 33 INTERVAL defines the step of the selection. 34 COUNT defines a number of instances 35 UNTIL defines a limit to the selection. 36 37 BY... qualifiers select instances within each outer selection instance according 38 to the recurrence of instances of the next highest resolution. For example, 39 BYDAY selects days in weeks. Thus, if no explicit week recurrence is indicated, 40 all weeks are selected within the selection of the next highest explicitly 41 specified resolution, whether this is months or years. 42 43 BYSETPOS in conjunction with BY... qualifiers permit the selection of specific 44 instances. 45 46 Within the FREQ resolution, BY... qualifiers refine selected instances. 47 48 Outside the FREQ resolution, BY... qualifiers select instances at the resolution 49 concerned. 50 51 Thus, FREQ and BY... qualifiers need to be ordered in terms of increasing 52 resolution (or decreasing scope). 53 """ 54 55 from calendar import monthrange 56 from datetime import date, datetime, timedelta 57 import operator 58 59 # Frequency levels, specified by FREQ in iCalendar. 60 61 freq_levels = ( 62 "YEARLY", 63 "MONTHLY", 64 "WEEKLY", 65 None, # yearday has no equivalent frequency 66 None, # monthday has no equivalent frequency 67 "DAILY", 68 "HOURLY", 69 "MINUTELY", 70 "SECONDLY" 71 ) 72 73 # Symbols corresponding to resolution levels. 74 75 YEARS, MONTHS, WEEKS, DAYS, HOURS, MINUTES, SECONDS = 0, 1, 2, 5, 6, 7, 8 76 77 # Enumeration levels, employed by BY... qualifiers. 78 79 enum_levels = ( 80 None, 81 "BYMONTH", 82 "BYWEEKNO", 83 "BYYEARDAY", 84 "BYMONTHDAY", 85 "BYDAY", 86 "BYHOUR", 87 "BYMINUTE", 88 "BYSECOND" 89 ) 90 91 # Levels defining days. 92 93 daylevels = [2, 3, 4, 5] 94 95 # Map from levels to lengths of datetime tuples. 96 97 lengths = [1, 2, 3, 3, 3, 3, 4, 5, 6] 98 positions = [l-1 for l in lengths] 99 100 # Define the lowest values at each resolution (years, months, days... hours, 101 # minutes, seconds). 102 103 firstvalues = [0, 1, 1, 1, 1, 1, 0, 0, 0] 104 105 # Map from qualifiers to interval multiples. Here, weeks are defined as 7 days. 106 107 multiples = {"WEEKLY" : 7} 108 109 # Make dictionaries mapping qualifiers to levels. 110 111 freq = {} 112 for i, level in enumerate(freq_levels): 113 if level: 114 freq[level] = i 115 116 enum = {} 117 for i, level in enumerate(enum_levels): 118 if level: 119 enum[level] = i 120 121 # Weekdays: name -> 1-based value 122 123 weekdays = {} 124 for i, weekday in enumerate(["MO", "TU", "WE", "TH", "FR", "SA", "SU"]): 125 weekdays[weekday] = i + 1 126 127 # Functions for structuring the recurrences. 128 129 def get_next(it): 130 131 "Return the next value from iterator 'it' or None if no more values exist." 132 133 try: 134 return it.next() 135 except StopIteration: 136 return None 137 138 def get_parameters(values): 139 140 "Return parameters from the given list of 'values'." 141 142 d = {} 143 for value in values: 144 try: 145 key, value = value.split("=", 1) 146 d[key] = value 147 except ValueError: 148 continue 149 return d 150 151 def get_qualifiers(values): 152 153 """ 154 Process the list of 'values' of the form "key=value", returning a list of 155 qualifiers of the form (qualifier name, args). 156 """ 157 158 qualifiers = [] 159 frequency = None 160 interval = 1 161 162 for value in values: 163 try: 164 key, value = value.split("=", 1) 165 except ValueError: 166 continue 167 168 # Accept frequency indicators as qualifiers. 169 170 if key == "FREQ" and freq.has_key(value): 171 qualifier = frequency = (value, {}) 172 173 # Accept interval indicators for frequency qualifier parameterisation. 174 175 elif key == "INTERVAL": 176 interval = int(value) 177 continue 178 179 # Accept result set selection, truncation and enumerators as qualifiers. 180 181 elif key in ("BYSETPOS", "COUNT") or enum.has_key(key): 182 qualifier = (key, {"values" : get_qualifier_values(key, value)}) 183 184 # Ignore other items. 185 186 else: 187 continue 188 189 qualifiers.append(qualifier) 190 191 # Parameterise any frequency qualifier with the interval. 192 193 if frequency: 194 frequency[1]["interval"] = interval 195 196 return qualifiers 197 198 def get_qualifier_values(qualifier, value): 199 200 """ 201 For the given 'qualifier', process the 'value' string, returning a list of 202 suitable values. 203 """ 204 205 # For non-weekday selection, obtain a list of day numbers. 206 207 if qualifier != "BYDAY": 208 return map(int, value.split(",")) 209 210 # For weekday selection, obtain the weekday number and instance number. 211 212 values = [] 213 214 for part in value.split(","): 215 weekday = weekdays.get(part[-2:]) 216 if not weekday: 217 continue 218 index = part[:-2] 219 if index: 220 index = int(index) 221 else: 222 index = None 223 values.append((weekday, index)) 224 225 return values 226 227 def order_qualifiers(qualifiers): 228 229 "Return the 'qualifiers' in order of increasing resolution." 230 231 l = [] 232 max_level = 0 233 234 # Special qualifiers. 235 236 setpos = None 237 count = None 238 239 for qualifier, args in qualifiers: 240 241 # Distinguish between enumerators, used to select particular periods, 242 # and frequencies, used to select repeating periods. 243 244 if enum.has_key(qualifier): 245 level = enum[qualifier] 246 247 # Certain enumerators produce their values in a special way. 248 249 if special_enum_levels.has_key(qualifier): 250 args["interval"] = 1 251 selector = special_enum_levels[qualifier] 252 else: 253 selector = Enum 254 255 elif qualifier == "BYSETPOS": 256 setpos = args 257 continue 258 259 elif qualifier == "COUNT": 260 count = args 261 continue 262 263 else: 264 level = freq[qualifier] 265 selector = Pattern 266 267 l.append(selector(level, args, qualifier)) 268 max_level = max(level, max_level) 269 270 # Add the result set selector at the maximum level of enumeration. 271 272 if setpos is not None: 273 l.append(PositionSelector(max_level, setpos, "BYSETPOS")) 274 275 # Add the result set truncator at the top level. 276 277 if count is not None: 278 l.append(LimitSelector(0, count, "COUNT")) 279 280 # Make BYSETPOS sort earlier than the enumeration it modifies. 281 # Other BY... qualifiers sort earlier than selectors at the same resolution 282 # even though such things as "FREQ=HOURLY;BYHOUR=10" do not make much sense. 283 284 l.sort(key=lambda x: (x.level, not x.qualifier.startswith("BY") and 2 or 285 x.qualifier != "BYSETPOS" and 1 or 0)) 286 return l 287 288 def get_datetime_structure(datetime): 289 290 "Return the structure of 'datetime' for recurrence production." 291 292 l = [] 293 offset = 0 294 295 for pos, value in enumerate(datetime): 296 297 # At the day number, adjust the frequency level offset to reference 298 # days (and then hours, minutes, seconds). 299 300 if pos == 2: 301 offset = 3 302 303 l.append(Enum(pos + offset, {"values" : [value]}, "DT")) 304 305 return l 306 307 def combine_datetime_with_qualifiers(datetime, qualifiers): 308 309 """ 310 Combine 'datetime' with 'qualifiers' to produce a structure for recurrence 311 production. 312 313 Initial datetime values appearing at broader resolutions than any qualifiers 314 are ignored, since their details will be used when materialising the 315 results. 316 317 Qualifiers are accumulated in order to define a selection. Datetime values 318 occurring between qualifiers or at the same resolution as qualifiers are 319 ignored. 320 321 Any remaining datetime values are introduced as enumerators, provided that 322 they do not conflict with qualifiers. For example, specific day values 323 conflict with day selectors and weekly qualifiers. 324 325 The purpose of the remaining datetime values is to define a result within 326 a period selected by the most precise qualifier. For example, the selection 327 of a day and month in a year recurrence. 328 """ 329 330 iter_dt = iter(get_datetime_structure(datetime)) 331 iter_q = iter(order_qualifiers(qualifiers)) 332 333 l = [] 334 335 from_dt = get_next(iter_dt) 336 from_q = get_next(iter_q) 337 have_q = False 338 339 # Consume from both lists, merging entries. 340 341 while from_dt and from_q: 342 _level = from_dt.level 343 level = from_q.level 344 345 # Datetime value at wider resolution. 346 347 if _level < level: 348 from_dt = get_next(iter_dt) 349 350 # Qualifier at wider or same resolution as datetime value. 351 352 else: 353 if not have_q: 354 add_initial_qualifier(from_q, level, l) 355 have_q = True 356 357 # Add the qualifier to the combined list. 358 359 l.append(from_q) 360 361 # Datetime value at same resolution. 362 363 if _level == level: 364 from_dt = get_next(iter_dt) 365 366 # Get the next qualifier. 367 368 from_q = get_next(iter_q) 369 370 # Complete the list by adding remaining datetime enumerators. 371 372 while from_dt: 373 374 # Ignore datetime values that conflict with day-level qualifiers. 375 376 if not l or from_dt.level != freq["DAILY"] or \ 377 l[-1].level not in daylevels: 378 379 l.append(from_dt) 380 381 from_dt = get_next(iter_dt) 382 383 # Complete the list by adding remaining qualifiers. 384 385 while from_q: 386 if not have_q: 387 add_initial_qualifier(from_q, level, l) 388 have_q = True 389 390 # Add the qualifier to the combined list. 391 392 l.append(from_q) 393 394 # Get the next qualifier. 395 396 from_q = get_next(iter_q) 397 398 return l 399 400 def add_initial_qualifier(from_q, level, l): 401 402 """ 403 Take the first qualifier 'from_q' at the given resolution 'level', using it 404 to create an initial qualifier, adding it to the combined list 'l' if 405 required. 406 """ 407 408 if isinstance(from_q, Enum) and level > 0: 409 repeat = Pattern(level - 1, {"interval" : 1}, None) 410 l.append(repeat) 411 412 def get_multiple(qualifier): 413 414 "Return the time unit multiple for 'qualifier'." 415 416 return multiples.get(qualifier, 1) 417 418 # Datetime arithmetic. 419 420 def is_year_only(t): 421 422 "Return if 't' describes a year." 423 424 return len(t) == lengths[YEARS] 425 426 def is_month_only(t): 427 428 "Return if 't' describes a month within a year." 429 430 return len(t) == lengths[MONTHS] 431 432 def start_of_year(t): 433 434 "Return the start of the year referenced by 't'." 435 436 return (t[0], 1, 1) 437 438 def end_of_year(t): 439 440 "Return the end of the year referenced by 't'." 441 442 return (t[0], 12, 31) 443 444 def start_of_month(t): 445 446 "Return the start of the month referenced by 't'." 447 448 year, month = t[:2] 449 return (year, month, 1) 450 451 def end_of_month(t): 452 453 "Return the end of the month referenced by 't'." 454 455 year, month = t[:2] 456 return update(update((year, month, 1), (0, 1, 0)), (0, 0, -1)) 457 458 def day_in_year(t, number): 459 460 "Return the day in the year referenced by 't' with the given 'number'." 461 462 return to_tuple(date(*start_of_year(t)) + timedelta(number - 1)) 463 464 def get_year_length(t): 465 466 "Return the length of the year referenced by 't'." 467 468 first_day = date(*start_of_year(t)) 469 last_day = date(*end_of_year(t)) 470 return (last_day - first_day).days + 1 471 472 def get_weekday(t): 473 474 "Return the 1-based weekday for the month referenced by 't'." 475 476 year, month = t[:2] 477 return monthrange(year, month)[0] + 1 478 479 def get_ordered_weekdays(t): 480 481 """ 482 Return the 1-based weekday sequence describing the first week of the month 483 referenced by 't'. 484 """ 485 486 first = get_weekday(t) 487 return range(first, 8) + range(1, first) 488 489 def get_last_weekday_instance(weekday, first_day, last_day): 490 491 """ 492 Return the last instance number for 'weekday' within the period from 493 'first_day' to 'last_day' inclusive. 494 495 Here, 'weekday' is 1-based (starting on Monday), the returned limit is 496 1-based. 497 """ 498 499 weekday0 = get_first_day(first_day, weekday) 500 days = (date(*last_day) - weekday0).days 501 return days / 7 + 1 502 503 def precision(t, level, value=None): 504 505 """ 506 Return 't' trimmed in resolution to the indicated resolution 'level', 507 setting 'value' at the given resolution if indicated. 508 """ 509 510 pos = positions[level] 511 512 if value is None: 513 return t[:pos + 1] 514 else: 515 return t[:pos] + (value,) 516 517 def scale(interval, level): 518 519 """ 520 Scale the given 'interval' value in resolution to the indicated resolution 521 'level', returning a tuple with leading zero elements and 'interval' at the 522 stated position. 523 """ 524 525 pos = positions[level] 526 return (0,) * pos + (interval,) 527 528 def get_seconds(t): 529 530 "Convert the sub-day part of 't' into seconds." 531 532 t = t + (0,) * (6 - len(t)) 533 return (t[3] * 60 + t[4]) * 60 + t[5] 534 535 def update(t, step): 536 537 "Update 't' by 'step' at the resolution of 'step'." 538 539 i = len(step) 540 541 # Years only. 542 543 if i == 1: 544 return (t[0] + step[0],) + tuple(t[1:]) 545 546 # Years and months. 547 548 elif i == 2: 549 month = t[1] + step[1] 550 return (t[0] + step[0] + (month - 1) / 12, (month - 1) % 12 + 1) + tuple(t[2:]) 551 552 # Dates and datetimes. 553 554 else: 555 updated_for_months = update(t, step[:2]) 556 557 # Dates only. 558 559 if i == 3: 560 d = datetime(*updated_for_months) 561 s = timedelta(step[2]) 562 563 # Datetimes. 564 565 else: 566 d = datetime(*updated_for_months) 567 s = timedelta(step[2], get_seconds(step)) 568 569 return to_tuple(d + s)[:len(t)] 570 571 def to_tuple(d): 572 573 "Return date or datetime 'd' as a tuple." 574 575 if not isinstance(d, date): 576 return d 577 if isinstance(d, datetime): 578 n = 6 579 else: 580 n = 3 581 return d.timetuple()[:n] 582 583 def get_first_day(first_day, weekday): 584 585 """ 586 Return the first occurrence at or after 'first_day' of 'weekday' as a date 587 instance. 588 """ 589 590 first_day = date(*first_day) 591 first_weekday = first_day.isoweekday() 592 if first_weekday > weekday: 593 return first_day + timedelta(7 - first_weekday + weekday) 594 else: 595 return first_day + timedelta(weekday - first_weekday) 596 597 def get_last_day(last_day, weekday): 598 599 """ 600 Return the last occurrence at or before 'last_day' of 'weekday' as a date 601 instance. 602 """ 603 604 last_day = date(*last_day) 605 last_weekday = last_day.isoweekday() 606 if last_weekday < weekday: 607 return last_day - timedelta(last_weekday + 7 - weekday) 608 else: 609 return last_day - timedelta(last_weekday - weekday) 610 611 # Value expansion and sorting. 612 613 def sort_values(values, limit=None): 614 615 """ 616 Sort the given 'values' using 'limit' to determine the positions of negative 617 values. 618 """ 619 620 # Convert negative values to positive values according to the value limit. 621 622 if limit is not None: 623 l = map(lambda x, limit=limit: x < 0 and x + 1 + limit or x, values) 624 else: 625 l = values[:] 626 627 l.sort() 628 return l 629 630 def compare_weekday_selectors(x, y, weekdays): 631 632 """ 633 Compare 'x' and 'y' values of the form (weekday number, instance number) 634 using 'weekdays' to define an ordering of the weekday numbers. 635 """ 636 637 return cmp((x[1], weekdays.index(x[0])), (y[1], weekdays.index(y[0]))) 638 639 def sort_weekdays(values, first_day, last_day): 640 641 """ 642 Return a sorted copy of the given 'values', each having the form (weekday 643 number, instance number) using 'weekdays' to define the ordering of the 644 weekday numbers and 'limit' to determine the positions of negative instance 645 numbers. 646 """ 647 648 weekdays = get_ordered_weekdays(first_day) 649 650 # Expand the values to incorporate specific weekday instances. 651 652 l = [] 653 654 for weekday, index in values: 655 656 # Obtain the last instance number of the weekday in the period. 657 658 limit = get_last_weekday_instance(weekday, first_day, last_day) 659 660 # For specific instances, convert negative selections. 661 662 if index is not None: 663 l.append((weekday, index < 0 and index + 1 + limit or index)) 664 665 # For None, introduce selections for all instances of the weekday. 666 667 else: 668 index = 1 669 while index <= limit: 670 l.append((weekday, index)) 671 index += 1 672 673 # Sort the values so that the resulting dates are ordered. 674 675 fn = lambda x, y, weekdays=weekdays: compare_weekday_selectors(x, y, weekdays) 676 l.sort(cmp=fn) 677 return l 678 679 def convert_positions(setpos): 680 681 "Convert 'setpos' to 0-based indexes." 682 683 l = [] 684 if setpos: 685 for pos in setpos: 686 index = pos < 0 and pos or pos - 1 687 l.append(index) 688 return l 689 690 # Classes for producing instances from recurrence structures. 691 692 class Selector: 693 694 "A generic selector." 695 696 def __init__(self, level, args, qualifier, selecting=None, first=False): 697 698 """ 699 Initialise at the given 'level' a selector employing the given 'args' 700 defined in the interpretation of recurrence rule qualifiers, with the 701 'qualifier' being the name of the rule qualifier, and 'selecting' being 702 an optional selector used to find more specific instances from those 703 found by this selector. 704 """ 705 706 self.level = level 707 self.args = args 708 self.qualifier = qualifier 709 self.selecting = selecting 710 self.first = first 711 712 def __repr__(self): 713 return "%s(%r, %r, %r, %r)" % (self.__class__.__name__, self.level, 714 self.args, self.qualifier, self.first) 715 716 def select(self, start, end, inclusive=False): 717 718 """ 719 Return an iterator over instances starting at 'start' and continuing up 720 to but not including any at 'end' or later. 721 722 If 'inclusive' is specified, the selection of instances will include the 723 end of the search period if present in the results. 724 """ 725 726 start = to_tuple(start) 727 end = to_tuple(end) 728 return self.materialise_items(start, start, end, inclusive) 729 730 def materialise(self, start, end, inclusive=False): 731 732 """ 733 Starting at 'start', materialise instances up to but not including any 734 at 'end' or later. A list of instances is returned. 735 736 If 'inclusive' is specified, the selection of instances will include the 737 end of the search period if present in the results. 738 """ 739 740 return list(self.select(start, end, inclusive)) 741 742 class Pattern(Selector): 743 744 "A selector of time periods according to a repeating pattern." 745 746 def __init__(self, level, args, qualifier, selecting=None, first=False): 747 Selector.__init__(self, level, args, qualifier, selecting, first) 748 749 multiple = get_multiple(self.qualifier) 750 interval = self.args.get("interval", 1) 751 752 # Define the step between result periods. 753 754 self.step = scale(interval * multiple, level) 755 756 # Define the scale of a single period. 757 758 self.unit_step = scale(multiple, level) 759 760 def materialise_items(self, context, start, end, inclusive=False): 761 762 """ 763 Bounded by the given 'context', return periods within 'start' to 'end'. 764 765 If 'inclusive' is specified, the selection of periods will include those 766 starting at the end of the search period, if present in the results. 767 """ 768 769 # Employ the context as the current period if this is the first 770 # qualifier in the selection chain. 771 772 if self.first: 773 current = precision(context, self.level) 774 775 # Otherwise, obtain the first value at this resolution within the 776 # context period. 777 778 else: 779 current = precision(context, self.level, firstvalues[self.level]) 780 781 return PatternIterator(self, current, start, end, inclusive, 782 self.step, self.unit_step) 783 784 class WeekDayFilter(Selector): 785 786 "A selector of instances specified in terms of day numbers." 787 788 def __init__(self, level, args, qualifier, selecting=None, first=False): 789 Selector.__init__(self, level, args, qualifier, selecting, first) 790 self.step = scale(1, WEEKS) 791 792 def materialise_items(self, context, start, end, inclusive=False): 793 794 # Get weekdays in the year. 795 796 if is_year_only(context): 797 first_day = start_of_year(context) 798 last_day = end_of_year(context) 799 800 # Get weekdays in the month. 801 802 elif is_month_only(context): 803 first_day = start_of_month(context) 804 last_day = end_of_month(context) 805 806 # Get weekdays in the week. 807 808 else: 809 current = context 810 values = [value for (value, index) in self.args["values"]] 811 return WeekDayIterator(self, current, start, end, inclusive, self.step, values) 812 813 current = first_day 814 values = sort_weekdays(self.args["values"], first_day, last_day) 815 return WeekDayGeneralIterator(self, current, start, end, inclusive, self.step, values) 816 817 class Enum(Selector): 818 819 "A generic value selector." 820 821 def __init__(self, level, args, qualifier, selecting=None, first=False): 822 Selector.__init__(self, level, args, qualifier, selecting, first) 823 self.step = scale(1, level) 824 825 def materialise_items(self, context, start, end, inclusive=False): 826 values = sort_values(self.args["values"]) 827 return EnumIterator(self, context, start, end, inclusive, self.step, values) 828 829 class MonthDayFilter(Enum): 830 831 "A selector of month days." 832 833 def materialise_items(self, context, start, end, inclusive=False): 834 last_day = end_of_month(context)[2] 835 values = sort_values(self.args["values"], last_day) 836 return EnumIterator(self, context, start, end, inclusive, self.step, values) 837 838 class YearDayFilter(Enum): 839 840 "A selector of days in years." 841 842 def materialise_items(self, context, start, end, inclusive=False): 843 first_day = start_of_year(context) 844 year_length = get_year_length(context) 845 values = sort_values(self.args["values"], year_length) 846 return YearDayFilterIterator(self, first_day, start, end, inclusive, self.step, values) 847 848 special_enum_levels = { 849 "BYDAY" : WeekDayFilter, 850 "BYMONTHDAY" : MonthDayFilter, 851 "BYYEARDAY" : YearDayFilter, 852 } 853 854 class LimitSelector(Selector): 855 856 "A result set limit selector." 857 858 def materialise_items(self, context, start, end, inclusive=False): 859 limit = self.args["values"][0] 860 return LimitIterator(self, context, start, end, inclusive, limit) 861 862 class PositionSelector(Selector): 863 864 "A result set position selector." 865 866 def __init__(self, level, args, qualifier, selecting=None, first=False): 867 Selector.__init__(self, level, args, qualifier, selecting, first) 868 self.step = scale(1, level) 869 870 def materialise_items(self, context, start, end, inclusive=False): 871 values = convert_positions(sort_values(self.args["values"])) 872 return PositionIterator(self, context, start, end, inclusive, self.step, values) 873 874 # Iterator classes. 875 876 class SelectorIterator: 877 878 "An iterator over selected data." 879 880 def __init__(self, selector, current, start, end, inclusive): 881 882 """ 883 Define an iterator having the 'current' point in time, 'start' and 'end' 884 limits, and whether the selection is 'inclusive'. 885 """ 886 887 self.selector = selector 888 self.current = current 889 self.start = start 890 self.end = end 891 self.inclusive = inclusive 892 893 # Iterator over selections within this selection. 894 895 self.iterator = None 896 897 def __iter__(self): 898 return self 899 900 def next_item(self, earliest, next): 901 902 """ 903 Given the 'earliest' acceptable instance and the 'next' instance, return 904 a list of result items. 905 906 Where no selection within the current instance occurs, the current 907 instance will be returned as a result if the same or later than the 908 earliest acceptable instance. 909 """ 910 911 # Obtain an item from a selector operating within this selection. 912 913 selecting = self.selector.selecting 914 915 if selecting: 916 917 # Obtain an iterator for any selector within the current period. 918 919 if not self.iterator: 920 self.iterator = selecting.materialise_items(self.current, 921 earliest, next, self.inclusive) 922 923 # Attempt to obtain and return a value. 924 925 return self.iterator.next() 926 927 # Return items within this selection. 928 929 else: 930 return self.current 931 932 def filter_by_period(self, result): 933 934 "Return whether 'result' occurs within the selection period." 935 936 return (self.inclusive and result <= self.end or result < self.end) and \ 937 self.start <= result 938 939 def at_limit(self): 940 941 "Obtain periods before the end (and also at the end if inclusive)." 942 943 return not self.inclusive and self.current == self.end or \ 944 self.current > self.end 945 946 class PatternIterator(SelectorIterator): 947 948 "An iterator for a general selection pattern." 949 950 def __init__(self, selector, current, start, end, inclusive, step, unit_step): 951 SelectorIterator.__init__(self, selector, current, start, end, inclusive) 952 self.step = step 953 self.unit_step = unit_step 954 955 def next(self): 956 957 "Return the next value." 958 959 while not self.at_limit(): 960 961 # Increment the current datetime by the step for the next period. 962 963 next = update(self.current, self.step) 964 965 # Determine the end point of the current period. 966 967 current_end = update(self.current, self.unit_step) 968 969 # Obtain any period or periods within the bounds defined by the 970 # current period and any contraining start and end points. 971 972 try: 973 result = self.next_item(max(self.current, self.start), 974 min(current_end, self.end)) 975 976 # Obtain the next period if not selecting within this period. 977 978 if not self.selector.selecting: 979 self.current = next 980 981 # Filter out periods. 982 983 if self.filter_by_period(result): 984 return result 985 986 # Move on to the next instance. 987 988 except StopIteration: 989 self.current = next 990 self.iterator = None 991 992 raise StopIteration 993 994 class WeekDayIterator(SelectorIterator): 995 996 "An iterator over weekday selections in week periods." 997 998 def __init__(self, selector, current, start, end, inclusive, step, values): 999 SelectorIterator.__init__(self, selector, current, start, end, inclusive) 1000 self.step = step 1001 self.values = values 1002 1003 def next(self): 1004 1005 "Return the next value." 1006 1007 while not self.at_limit(): 1008 1009 # Increment the current datetime by the step for the next period. 1010 1011 next = update(self.current, self.step) 1012 1013 # Determine whether the day is one chosen. 1014 1015 if date(*self.current).isoweekday() in self.values: 1016 try: 1017 result = self.next_item(max(self.current, self.start), 1018 min(next, self.end)) 1019 1020 # Obtain the next period if not selecting within this period. 1021 1022 if not self.selector.selecting: 1023 self.current = next 1024 1025 return result 1026 1027 # Move on to the next instance. 1028 1029 except StopIteration: 1030 self.current = next 1031 self.iterator = None 1032 1033 else: 1034 self.current = next 1035 self.iterator = None 1036 1037 raise StopIteration 1038 1039 class EnumIterator(SelectorIterator): 1040 1041 "An iterator over specific value selections." 1042 1043 def __init__(self, selector, current, start, end, inclusive, step, values): 1044 SelectorIterator.__init__(self, selector, current, start, end, inclusive) 1045 self.step = step 1046 1047 # Derive selected periods from a base and the indicated values. 1048 1049 self.base = current 1050 1051 # Iterate over the indicated period values. 1052 1053 self.values = iter(values) 1054 self.value = None 1055 1056 def get_selected_period(self): 1057 1058 "Return the period indicated by the current value." 1059 1060 return precision(self.base, self.selector.level, self.value) 1061 1062 def next(self): 1063 1064 "Return the next value." 1065 1066 while True: 1067 1068 # Find each of the given selected values. 1069 1070 if not self.selector.selecting or self.value is None: 1071 self.value = self.values.next() 1072 1073 # Select a period for each value at the current resolution. 1074 1075 self.current = self.get_selected_period() 1076 next = update(self.current, self.step) 1077 1078 # To support setpos, only current and next bound the search, not 1079 # the period in addition. 1080 1081 try: 1082 return self.next_item(self.current, next) 1083 1084 # Move on to the next instance. 1085 1086 except StopIteration: 1087 self.value = None 1088 self.iterator = None 1089 1090 raise StopIteration 1091 1092 class WeekDayGeneralIterator(EnumIterator): 1093 1094 "An iterator over weekday selections in month and year periods." 1095 1096 def get_selected_period(self): 1097 1098 "Return the day indicated by the current day value." 1099 1100 value, index = self.value 1101 offset = timedelta(7 * (index - 1)) 1102 weekday0 = get_first_day(self.base, value) 1103 return precision(to_tuple(weekday0 + offset), DAYS) 1104 1105 class YearDayFilterIterator(EnumIterator): 1106 1107 "An iterator over day-in-year selections." 1108 1109 def get_selected_period(self): 1110 1111 "Return the day indicated by the current day value." 1112 1113 offset = timedelta(self.value - 1) 1114 return precision(to_tuple(date(*self.base) + offset), DAYS) 1115 1116 class LimitIterator(SelectorIterator): 1117 1118 "A result set limiting iterator." 1119 1120 def __init__(self, selector, context, start, end, inclusive, limit): 1121 SelectorIterator.__init__(self, selector, context, start, end, inclusive) 1122 self.limit = limit 1123 self.count = 0 1124 1125 def next(self): 1126 1127 "Return the next value." 1128 1129 if self.count < self.limit: 1130 self.count += 1 1131 result = self.next_item(self.start, self.end) 1132 return result 1133 1134 raise StopIteration 1135 1136 class PositionIterator(SelectorIterator): 1137 1138 "An iterator over results, selecting positions." 1139 1140 def __init__(self, selector, context, start, end, inclusive, step, positions): 1141 SelectorIterator.__init__(self, selector, context, start, end, inclusive) 1142 self.step = step 1143 1144 # Positions to select. 1145 1146 self.positions = positions 1147 1148 # Queue to hold values matching the negative position values. 1149 1150 self.queue_length = positions and positions[0] < 0 and abs(positions[0]) or 0 1151 self.queue = [] 1152 1153 # Result position. 1154 1155 self.resultpos = 0 1156 1157 def next(self): 1158 1159 "Return the next value." 1160 1161 while True: 1162 try: 1163 result = self.next_item(self.start, self.end) 1164 1165 # Positive positions can have their values released immediately. 1166 1167 selected = self.resultpos in self.positions 1168 self.resultpos += 1 1169 1170 if selected: 1171 return result 1172 1173 # Negative positions must be held until this iterator completes and 1174 # then be released. 1175 1176 if self.queue_length: 1177 self.queue.append(result) 1178 if len(self.queue) > self.queue_length: 1179 del self.queue[0] 1180 1181 except StopIteration: 1182 1183 # With a queue and positions, attempt to find the referenced 1184 # positions. 1185 1186 if self.queue and self.positions: 1187 index = self.positions[0] 1188 del self.positions[0] 1189 1190 # Only negative positions are used at this point. 1191 1192 if index < 0: 1193 try: 1194 return self.queue[index] 1195 except IndexError: 1196 pass 1197 1198 # With only positive positions remaining, signal the end of 1199 # the collection. 1200 1201 else: 1202 raise 1203 1204 # With no queue or positions remaining, signal the end of the 1205 # collection. 1206 1207 else: 1208 raise 1209 1210 # Public functions. 1211 1212 def connect_selectors(selectors): 1213 1214 """ 1215 Make the 'selectors' reference each other in a hierarchy so that 1216 materialising the principal selector causes the more specific ones to be 1217 employed in the operation. 1218 """ 1219 1220 current = selectors[0] 1221 current.first = first = True 1222 1223 for selector in selectors[1:]: 1224 current.selecting = selector 1225 1226 # Allow selectors within the limit selector to act as if they are first 1227 # in the chain and will operate using the supplied datetime context. 1228 1229 first = isinstance(current, LimitSelector) 1230 1231 current = selector 1232 current.first = first 1233 1234 return selectors[0] 1235 1236 def get_selector(dt, qualifiers): 1237 1238 """ 1239 Combine the initial datetime 'dt' with the given 'qualifiers', returning an 1240 object that can be used to select recurrences described by the 'qualifiers'. 1241 """ 1242 1243 dt = to_tuple(dt) 1244 return connect_selectors(combine_datetime_with_qualifiers(dt, qualifiers)) 1245 1246 def get_rule(dt, rule): 1247 1248 """ 1249 Using the given initial datetime 'dt', interpret the 'rule' (a semicolon- 1250 separated collection of "key=value" strings), and return the resulting 1251 selector object. 1252 """ 1253 1254 if not isinstance(rule, tuple): 1255 rule = rule.split(";") 1256 qualifiers = get_qualifiers(rule) 1257 return get_selector(dt, qualifiers) 1258 1259 # vim: tabstop=4 expandtab shiftwidth=4