imip-agent

vRecurrence.py

1393:d559de7bdc6c
2017-11-24 Paul Boddie Fixed superclass method call.
     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