# HG changeset patch # User Paul Boddie # Date 1508869982 -7200 # Node ID 177816f89852e652b56dc970134e9e4d78715f74 # Parent 365abd0d41b484e383180860dd101f0e1ecf75a2# Parent b3ba19f3e74dee1118e73f73618549365baf7522 Merged changes from the default branch. diff -r 365abd0d41b4 -r 177816f89852 imiptools/data.py --- a/imiptools/data.py Fri Oct 20 23:37:08 2017 +0200 +++ b/imiptools/data.py Tue Oct 24 20:33:02 2017 +0200 @@ -567,20 +567,7 @@ "Return whether this object may recur indefinitely." rrule = self.get_value("RRULE") - parameters = rrule and get_parameters(rrule) - until = parameters and parameters.get("UNTIL") - count = parameters and parameters.get("COUNT") - - # Non-recurring periods or constrained recurrences. - - if not rrule or until or count: - return False - - # Unconstrained recurring periods will always lie beyond any specified - # datetime. - - else: - return True + return rrule and not rule_has_end(rrule) # Modification methods. @@ -1214,6 +1201,13 @@ return delegators +def rule_has_end(rrule): + + "Return whether 'rrule' defines an end." + + parameters = rrule and get_parameters(rrule) + return parameters and parameters.has_key("UNTIL") or parameters.has_key("COUNT") + def make_rule_period(start, duration, attr, tzid): """ @@ -1232,47 +1226,73 @@ return RecurringPeriod(start, end, tzid, "RRULE", attr) -def get_rule_periods(rrule, main_period, tzid, end, inclusive=False): +class RulePeriodCollection: + + "A collection of rule periods." + + def __init__(self, rrule, main_period, tzid, end, inclusive=False): - """ - Return periods for the given 'rrule', employing the 'main_period' and - 'tzid'. + """ + Initialise a period collection for the given 'rrule', employing the + 'main_period' and 'tzid'. - The specified 'end' datetime indicates the end of the window for which - periods shall be computed. + The specified 'end' datetime indicates the end of the window for which + periods shall be computed. - If 'inclusive' is set to a true value, any period occurring at the 'end' - will be included. - """ + If 'inclusive' is set to a true value, any period occurring at the 'end' + will be included. + """ - start = main_period.get_start() - attr = main_period.get_start_attr() - duration = main_period.get_duration() + self.rrule = rrule + self.main_period = main_period + self.tzid = tzid - parameters = rrule and get_parameters(rrule) - selector = get_rule(start, rrule) + parameters = rrule and get_parameters(rrule) + until = parameters.get("UNTIL") + + # Any UNTIL qualifier changes the nature of the end of the collection. - until = parameters.get("UNTIL") + if until: + attr = main_period.get_start_attr() + until_dt = to_timezone(get_datetime(until, attr), tzid) + self.end = end and min(until_dt, end) or until_dt + self.inclusive = True + else: + self.end = end + self.inclusive = inclusive - if until: - until_dt = to_timezone(get_datetime(until, attr), tzid) - end = end and min(until_dt, end) or until_dt - inclusive = True + def __iter__(self): - # Obtain period instances, starting from the main period. Since counting - # must start from the first period, filtering from a start date must be - # done after the instances have been obtained. + """ + Obtain period instances, starting from the main period. Since counting + must start from the first period, filtering from a start date must be + done after the instances have been obtained. + """ + + start = self.main_period.get_start() + selector = get_rule(start, self.rrule) - periods = [] + return RulePeriodIterator(self.main_period, self.tzid, + selector.select(start, self.end, self.inclusive)) + +class RulePeriodIterator: + + "An iterator over rule periods." - for recurrence_start in selector.materialise(start, end, - parameters.get("COUNT"), - parameters.get("BYSETPOS"), - inclusive): + def __init__(self, main_period, tzid, iterator): + self.main_period = main_period + self.attr = main_period.get_start_attr() + self.duration = main_period.get_duration() + self.tzid = tzid + self.iterator = iterator - periods.append(make_rule_period(recurrence_start, duration, attr, tzid)) + def next(self): + recurrence_start = self.iterator.next() + period = make_rule_period(recurrence_start, self.duration, self.attr, self.tzid) - return periods + # Use the main period where it occurs. + + return period == self.main_period and self.main_period or period def get_periods(obj, start=None, end=None, inclusive=False): @@ -1291,7 +1311,6 @@ tzid = obj.get_tzid() rrule = obj.get_value("RRULE") - parameters = rrule and get_parameters(rrule) # Use localised datetimes. @@ -1305,27 +1324,14 @@ # for the agent, with instances outside that period being considered # unchecked. - elif end or parameters and parameters.has_key("UNTIL") or parameters.has_key("COUNT"): - - # Define a selection period with a start point. The end will be handled - # in the materialisation process. - - selection_period = Period(start, None) - periods = [] - - for period in get_rule_periods(rrule, main_period, tzid, end, - inclusive): + elif end or rule_has_end(rrule): - # Use the main period where it occurs. - - if period == main_period: - period = main_period + # Filter periods using a start point. The end will be handled in the + # materialisation process. - # Filter out periods before the start. - - if period.within(selection_period): - periods.append(period) - + periods = filter(Period(start, None).wraps, + RulePeriodCollection(rrule, main_period, tzid, end, + inclusive)) else: periods = [] diff -r 365abd0d41b4 -r 177816f89852 imiptools/period.py --- a/imiptools/period.py Fri Oct 20 23:37:08 2017 +0200 +++ b/imiptools/period.py Tue Oct 24 20:33:02 2017 +0200 @@ -178,6 +178,10 @@ return start_point(self) >= start_point(other) and \ end_point(self) <= end_point(other) + def wraps(self, other): + return start_point(self) <= start_point(other) and \ + end_point(self) >= end_point(other) + def common(self, other): start = max(start_point(self), start_point(other)) end = min(end_point(self), end_point(other)) diff -r 365abd0d41b4 -r 177816f89852 tests/internal/qualifiers.py --- a/tests/internal/qualifiers.py Fri Oct 20 23:37:08 2017 +0200 +++ b/tests/internal/qualifiers.py Tue Oct 24 20:33:02 2017 +0200 @@ -69,7 +69,8 @@ print qualifiers = [ - ("DAILY", {"interval" : 1}) + ("DAILY", {"interval" : 1}), + ("COUNT", {"values" : [10]}) ] l = order_qualifiers(qualifiers) @@ -81,7 +82,7 @@ show(l) s = get_selector(dt, qualifiers) -l = s.materialise(dt, (1997, 12, 24), 10) +l = s.materialise(dt, (1997, 12, 24)) print len(l) == 10, 10, len(l) print l[0] == (1997, 9, 2, 9, 0, 0), (1997, 9, 2, 9, 0, 0), l[0] print l[-1] == (1997, 9, 11, 9, 0, 0), (1997, 9, 11, 9, 0, 0), l[-1] @@ -145,7 +146,8 @@ print qualifiers = [ - ("DAILY", {"interval" : 10}) + ("DAILY", {"interval" : 10}), + ("COUNT", {"values" : [5]}) ] l = order_qualifiers(qualifiers) @@ -157,7 +159,7 @@ show(l) s = get_selector(dt, qualifiers) -l = s.materialise(dt, (1997, 12, 24, 0, 0, 0), 5) +l = s.materialise(dt, (1997, 12, 24, 0, 0, 0)) print len(l) == 5, 5, len(l) print l[0] == (1997, 9, 2, 9, 0, 0), (1997, 9, 2, 9, 0, 0), l[0] print l[-1] == (1997, 10, 12, 9, 0, 0), (1997, 10, 12, 9, 0, 0), l[-1] @@ -205,7 +207,8 @@ print qualifiers = [ - ("WEEKLY", {"interval" : 1}) + ("WEEKLY", {"interval" : 1}), + ("COUNT", {"values" : [10]}) ] l = order_qualifiers(qualifiers) @@ -217,7 +220,7 @@ show(l) s = get_selector(dt, qualifiers) -l = s.materialise(dt, (1997, 12, 24, 0, 0, 0), 10) +l = s.materialise(dt, (1997, 12, 24, 0, 0, 0)) print len(l) == 10, 10, len(l) print l[0] == (1997, 9, 2, 9, 0, 0), (1997, 9, 2, 9, 0, 0), l[0] print l[-1] == (1997, 11, 4, 9, 0, 0), (1997, 11, 4, 9, 0, 0), l[-1] @@ -243,6 +246,25 @@ print qualifiers = [ + ("WEEKLY", {"interval" : 1}) + ] + +l = order_qualifiers(qualifiers) +show(l) +dt = (1997, 9, 2) +l = get_datetime_structure(dt) +show(l) +l = combine_datetime_with_qualifiers(dt, qualifiers) +show(l) + +s = get_selector(dt, qualifiers) +l = s.materialise(dt, (1997, 12, 24, 0, 0, 0)) +print len(l) == 17, 17, len(l) +print l[0] == (1997, 9, 2), (1997, 9, 2), l[0] +print l[-1] == (1997, 12, 23), (1997, 12, 23), l[-1] +print + +qualifiers = [ ("WEEKLY", {"interval" : 2}) ] @@ -283,7 +305,8 @@ qualifiers = [ ("WEEKLY", {"interval" : 1}), - ("BYDAY", {"values" : [(2, None), (4, None)]}) + ("BYDAY", {"values" : [(2, None), (4, None)]}), + ("COUNT", {"values" : [10]}) ] l = order_qualifiers(qualifiers) @@ -295,7 +318,7 @@ show(l) s = get_selector(dt, qualifiers) -l = s.materialise(dt, (1997, 12, 24, 0, 0, 0), 10) +l = s.materialise(dt, (1997, 12, 24, 0, 0, 0)) print len(l) == 10, 10, len(l) print l[0] == (1997, 9, 2, 9, 0, 0), (1997, 9, 2, 9, 0, 0), l[0] print l[-1] == (1997, 10, 2, 9, 0, 0), (1997, 10, 2, 9, 0, 0), l[-1] @@ -323,7 +346,8 @@ qualifiers = [ ("WEEKLY", {"interval" : 2}), - ("BYDAY", {"values" : [(2, None), (4, None)]}) + ("BYDAY", {"values" : [(2, None), (4, None)]}), + ("COUNT", {"values" : [8]}) ] l = order_qualifiers(qualifiers) @@ -335,7 +359,7 @@ show(l) s = get_selector(dt, qualifiers) -l = s.materialise(dt, (1997, 12, 24, 0, 0, 0), 8) +l = s.materialise(dt, (1997, 12, 24, 0, 0, 0)) print len(l) == 8, 8, len(l) print l[0] == (1997, 9, 2, 9, 0, 0), (1997, 9, 2, 9, 0, 0), l[0] print l[-1] == (1997, 10, 16, 9, 0, 0), (1997, 10, 16, 9, 0, 0), l[-1] @@ -343,7 +367,8 @@ qualifiers = [ ("MONTHLY", {"interval" : 1}), - ("BYDAY", {"values" : [(5, 1)]}) + ("BYDAY", {"values" : [(5, 1)]}), + ("COUNT", {"values" : [10]}) ] l = order_qualifiers(qualifiers) @@ -355,7 +380,7 @@ show(l) s = get_selector(dt, qualifiers) -l = s.materialise(dt, (1998, 12, 24, 0, 0, 0), 10) +l = s.materialise(dt, (1998, 12, 24, 0, 0, 0)) print len(l) == 10, 10, len(l) print l[0] == (1997, 9, 5, 9, 0, 0), (1997, 9, 5, 9, 0, 0), l[0] print l[-1] == (1998, 6, 5, 9, 0, 0), (1998, 6, 5, 9, 0, 0), l[-1] @@ -383,7 +408,8 @@ qualifiers = [ ("MONTHLY", {"interval" : 2}), - ("BYDAY", {"values" : [(7, 1), (7, -1)]}) + ("BYDAY", {"values" : [(7, 1), (7, -1)]}), + ("COUNT", {"values" : [10]}) ] l = order_qualifiers(qualifiers) @@ -395,7 +421,7 @@ show(l) s = get_selector(dt, qualifiers) -l = s.materialise(dt, (1998, 12, 24, 0, 0, 0), 10) +l = s.materialise(dt, (1998, 12, 24, 0, 0, 0)) print len(l) == 10, 10, len(l) print l[0] == (1997, 9, 7, 9, 0, 0), (1997, 9, 7, 9, 0, 0), l[0] print l[-1] == (1998, 5, 31, 9, 0, 0), (1998, 5, 31, 9, 0, 0), l[-1] @@ -403,7 +429,8 @@ qualifiers = [ ("MONTHLY", {"interval" : 1}), - ("BYDAY", {"values" : [(1, -2)]}) + ("BYDAY", {"values" : [(1, -2)]}), + ("COUNT", {"values" : [6]}) ] l = order_qualifiers(qualifiers) @@ -415,7 +442,7 @@ show(l) s = get_selector(dt, qualifiers) -l = s.materialise(dt, (1998, 12, 24, 0, 0, 0), 6) +l = s.materialise(dt, (1998, 12, 24, 0, 0, 0)) print len(l) == 6, 6, len(l) print l[0] == (1997, 9, 22, 9, 0, 0), (1997, 9, 22, 9, 0, 0), l[0] print l[-1] == (1998, 2, 16, 9, 0, 0), (1998, 2, 16, 9, 0, 0), l[-1] @@ -423,7 +450,8 @@ qualifiers = [ ("MONTHLY", {"interval" : 1}), - ("BYMONTHDAY", {"values" : [-3]}) + ("BYMONTHDAY", {"values" : [-3]}), + ("COUNT", {"values" : [6]}) ] l = order_qualifiers(qualifiers) @@ -435,7 +463,7 @@ show(l) s = get_selector(dt, qualifiers) -l = s.materialise(dt, (1998, 12, 24, 0, 0, 0), 6) +l = s.materialise(dt, (1998, 12, 24, 0, 0, 0)) print len(l) == 6, 6, len(l) print l[0] == (1997, 9, 28, 9, 0, 0), (1997, 9, 28, 9, 0, 0), l[0] print l[-1] == (1998, 2, 26, 9, 0, 0), (1998, 2, 26, 9, 0, 0), l[-1] @@ -443,7 +471,8 @@ qualifiers = [ ("MONTHLY", {"interval" : 1}), - ("BYMONTHDAY", {"values" : [2, 15]}) + ("BYMONTHDAY", {"values" : [15, 2]}), # test ordering + ("COUNT", {"values" : [10]}) ] l = order_qualifiers(qualifiers) @@ -455,7 +484,7 @@ show(l) s = get_selector(dt, qualifiers) -l = s.materialise(dt, (1998, 12, 24, 0, 0, 0), 10) +l = s.materialise(dt, (1998, 12, 24, 0, 0, 0)) print len(l) == 10, 10, len(l) print l[0] == (1997, 9, 2, 9, 0, 0), (1997, 9, 2, 9, 0, 0), l[0] print l[-1] == (1998, 1, 15, 9, 0, 0), (1998, 1, 15, 9, 0, 0), l[-1] @@ -463,7 +492,8 @@ qualifiers = [ ("MONTHLY", {"interval" : 1}), - ("BYMONTHDAY", {"values" : [1, -1]}) + ("BYMONTHDAY", {"values" : [1, -1]}), + ("COUNT", {"values" : [10]}) ] l = order_qualifiers(qualifiers) @@ -475,7 +505,7 @@ show(l) s = get_selector(dt, qualifiers) -l = s.materialise(dt, (1998, 12, 24, 0, 0, 0), 10) +l = s.materialise(dt, (1998, 12, 24, 0, 0, 0)) print len(l) == 10, 10, len(l) print l[0] == (1997, 9, 30, 9, 0, 0), (1997, 9, 30, 9, 0, 0), l[0] print l[-1] == (1998, 2, 1, 9, 0, 0), (1998, 2, 1, 9, 0, 0), l[-1] @@ -483,7 +513,8 @@ qualifiers = [ ("MONTHLY", {"interval" : 18}), - ("BYMONTHDAY", {"values" : [10, 11, 12, 13, 14, 15]}) + ("BYMONTHDAY", {"values" : [10, 11, 12, 13, 14, 15]}), + ("COUNT", {"values" : [10]}) ] l = order_qualifiers(qualifiers) @@ -495,7 +526,7 @@ show(l) s = get_selector(dt, qualifiers) -l = s.materialise(dt, (1999, 12, 24, 0, 0, 0), 10) +l = s.materialise(dt, (1999, 12, 24, 0, 0, 0)) print len(l) == 10, 10, len(l) print l[0] == (1997, 9, 10, 9, 0, 0), (1997, 9, 10, 9, 0, 0), l[0] print l[-1] == (1999, 3, 13, 9, 0, 0), (1999, 3, 13, 9, 0, 0), l[-1] @@ -523,7 +554,8 @@ qualifiers = [ ("YEARLY", {"interval" : 1}), - ("BYMONTH", {"values" : [6, 7]}) + ("BYMONTH", {"values" : [6, 7]}), + ("COUNT", {"values" : [10]}) ] l = order_qualifiers(qualifiers) @@ -535,7 +567,7 @@ show(l) s = get_selector(dt, qualifiers) -l = s.materialise(dt, (2001, 12, 24, 0, 0, 0), 10) +l = s.materialise(dt, (2001, 12, 24, 0, 0, 0)) print len(l) == 10, 10, len(l) print l[0] == (1997, 6, 10, 9, 0, 0), (1997, 6, 10, 9, 0, 0), l[0] print l[-1] == (2001, 7, 10, 9, 0, 0), (2001, 7, 10, 9, 0, 0), l[-1] @@ -543,7 +575,8 @@ qualifiers = [ ("YEARLY", {"interval" : 2}), - ("BYMONTH", {"values" : [1, 2, 3]}) + ("BYMONTH", {"values" : [1, 2, 3]}), + ("COUNT", {"values" : [10]}) ] l = order_qualifiers(qualifiers) @@ -555,7 +588,7 @@ show(l) s = get_selector(dt, qualifiers) -l = s.materialise(dt, (2003, 12, 24, 0, 0, 0), 10) +l = s.materialise(dt, (2003, 12, 24, 0, 0, 0)) print len(l) == 10, 10, len(l) print l[0] == (1997, 3, 10, 9, 0, 0), (1997, 3, 10, 9, 0, 0), l[0] print l[-1] == (2003, 3, 10, 9, 0, 0), (2003, 3, 10, 9, 0, 0), l[-1] @@ -563,7 +596,8 @@ qualifiers = [ ("YEARLY", {"interval" : 3}), - ("BYYEARDAY", {"values" : [1, 100, 200]}) + ("BYYEARDAY", {"values" : [1, 100, 200]}), + ("COUNT", {"values" : [10]}) ] l = order_qualifiers(qualifiers) @@ -575,7 +609,7 @@ show(l) s = get_selector(dt, qualifiers) -l = s.materialise(dt, (2006, 2, 1, 0, 0, 0), 10) +l = s.materialise(dt, (2006, 2, 1, 0, 0, 0)) print len(l) == 10, 10, len(l) print l[0] == (1997, 1, 1, 9, 0, 0), (1997, 1, 1, 9, 0, 0), l[0] print l[-1] == (2006, 1, 1, 9, 0, 0), (2006, 1, 1, 9, 0, 0), l[-1] @@ -732,7 +766,9 @@ qualifiers = [ ("MONTHLY", {"interval" : 1}), - ("BYDAY", {"values" : [(2, None), (3, None), (4, None)]}) + ("BYDAY", {"values" : [(2, None), (3, None), (4, None)]}), + ("BYSETPOS", {"values" : [3]}), + ("COUNT", {"values" : [3]}) ] l = order_qualifiers(qualifiers) @@ -744,7 +780,7 @@ show(l) s = get_selector(dt, qualifiers) -l = s.materialise(dt, (1997, 12, 24, 0, 0, 0), 3, [3]) +l = s.materialise(dt, (1997, 12, 24, 0, 0, 0)) print len(l) == 3, 3, len(l) print l[0] == (1997, 9, 4, 9, 0, 0), (1997, 9, 4, 9, 0, 0), l[0] print l[-1] == (1997, 11, 6, 9, 0, 0), (1997, 11, 6, 9, 0, 0), l[-1] @@ -754,7 +790,8 @@ qualifiers = [ ("MONTHLY", {"interval" : 1}), - ("BYDAY", {"values" : [(1, None), (2, None), (3, None), (4, None), (5, None)]}) + ("BYDAY", {"values" : [(1, None), (2, None), (3, None), (4, None), (5, None)]}), + ("BYSETPOS", {"values" : [-2]}) ] l = order_qualifiers(qualifiers) @@ -766,7 +803,7 @@ show(l) s = get_selector(dt, qualifiers) -l = s.materialise(dt, (1998, 4, 1, 0, 0, 0), None, [-2]) +l = s.materialise(dt, (1998, 4, 1, 0, 0, 0)) print len(l) == 7, 7, len(l) print l[0] == (1997, 9, 29, 9, 0, 0), (1997, 9, 29, 9, 0, 0), l[0] print l[-1] == (1998, 3, 30, 9, 0, 0), (1998, 3, 30, 9, 0, 0), l[-1] @@ -804,4 +841,20 @@ print l[0] == (2018, 1, 1), (2018, 1, 1), l[0] print l[-1] == (2018, 1, 31), (2018, 1, 31), l[-1] +qualifiers = get_qualifiers(["FREQ=MONTHLY", "BYDAY=WE,1FR,2MO,2FR"]) + +l = order_qualifiers(qualifiers) +show(l) +dt = (2017, 10, 15) +l = get_datetime_structure(dt) +show(l) +l = combine_datetime_with_qualifiers(dt, qualifiers) +show(l) + +s = get_selector(dt, qualifiers) +l = s.materialise(dt, (2018, 1, 1)) +print len(l) == 17 +print l[0] == (2017, 10, 18), (2017, 10, 18), l[0] +print l[-1] == (2017, 12, 27), (2017, 12, 27), l[-1] + # vim: tabstop=4 expandtab shiftwidth=4 diff -r 365abd0d41b4 -r 177816f89852 vRecurrence.py --- a/vRecurrence.py Fri Oct 20 23:37:08 2017 +0200 +++ b/vRecurrence.py Tue Oct 24 20:33:02 2017 +0200 @@ -70,6 +70,10 @@ "SECONDLY" ) +# Symbols corresponding to resolution levels. + +YEARS, MONTHS, WEEKS, DAYS, HOURS, MINUTES, SECONDS = 0, 1, 2, 5, 6, 7, 8 + # Enumeration levels, employed by BY... qualifiers. enum_levels = ( @@ -98,19 +102,34 @@ firstvalues = [0, 1, 1, 1, 1, 1, 0, 0, 0] -# Map from qualifiers to interval units. Here, weeks are defined as 7 days. +# Map from qualifiers to interval multiples. Here, weeks are defined as 7 days. -units = {"WEEKLY" : 7} +multiples = {"WEEKLY" : 7} # Make dictionaries mapping qualifiers to levels. -freq = dict([(level, i) for (i, level) in enumerate(freq_levels) if level]) -enum = dict([(level, i) for (i, level) in enumerate(enum_levels) if level]) -weekdays = dict([(weekday, i+1) for (i, weekday) in enumerate(["MO", "TU", "WE", "TH", "FR", "SA", "SU"])]) +freq = {} +for i, level in enumerate(freq_levels): + if level: + freq[level] = i + +enum = {} +for i, level in enumerate(enum_levels): + if level: + enum[level] = i + +# Weekdays: name -> 1-based value + +weekdays = {} +for i, weekday in enumerate(["MO", "TU", "WE", "TH", "FR", "SA", "SU"]): + weekdays[weekday] = i + 1 # Functions for structuring the recurrences. def get_next(it): + + "Return the next value from iterator 'it' or None if no more values exist." + try: return it.next() except StopIteration: @@ -122,14 +141,11 @@ d = {} for value in values: - parts = value.split("=", 1) - if len(parts) < 2: + try: + key, value = value.split("=", 1) + d[key] = value + except ValueError: continue - key, value = parts - if key in ("COUNT", "BYSETPOS"): - d[key] = int(value) - else: - d[key] = value return d def get_qualifiers(values): @@ -144,10 +160,10 @@ interval = 1 for value in values: - parts = value.split("=", 1) - if len(parts) < 2: + try: + key, value = value.split("=", 1) + except ValueError: continue - key, value = parts # Accept frequency indicators as qualifiers. @@ -160,9 +176,9 @@ interval = int(value) continue - # Accept enumerators as qualifiers. + # Accept result set selection, truncation and enumerators as qualifiers. - elif enum.has_key(key): + elif key in ("BYSETPOS", "COUNT") or enum.has_key(key): qualifier = (key, {"values" : get_qualifier_values(key, value)}) # Ignore other items. @@ -186,10 +202,15 @@ suitable values. """ + # For non-weekday selection, obtain a list of day numbers. + if qualifier != "BYDAY": return map(int, value.split(",")) + # For weekday selection, obtain the weekday number and instance number. + values = [] + for part in value.split(","): weekday = weekdays.get(part[-2:]) if not weekday: @@ -208,6 +229,12 @@ "Return the 'qualifiers' in order of increasing resolution." l = [] + max_level = 0 + + # Special qualifiers. + + setpos = None + count = None for qualifier, args in qualifiers: @@ -216,18 +243,43 @@ if enum.has_key(qualifier): level = enum[qualifier] + + # Certain enumerators produce their values in a special way. + if special_enum_levels.has_key(qualifier): args["interval"] = 1 selector = special_enum_levels[qualifier] else: selector = Enum + + elif qualifier == "BYSETPOS": + setpos = args + continue + + elif qualifier == "COUNT": + count = args + continue + else: level = freq[qualifier] selector = Pattern l.append(selector(level, args, qualifier)) + max_level = max(level, max_level) - l.sort(key=lambda x: x.level) + # Add the result set selector at the maximum level of enumeration. + + if setpos is not None: + l.append(PositionSelector(max_level, setpos, "BYSETPOS")) + + # Add the result set truncator at the top level. + + if count is not None: + l.append(LimitSelector(0, count, "COUNT")) + + # Make BYSETPOS sort earlier than the enumeration it modifies. + + l.sort(key=lambda x: (x.level, x.qualifier != "BYSETPOS" and 1 or 0)) return l def get_datetime_structure(datetime): @@ -237,15 +289,15 @@ l = [] offset = 0 - for level, value in enumerate(datetime): + for pos, value in enumerate(datetime): # At the day number, adjust the frequency level offset to reference # days (and then hours, minutes, seconds). - if level == 2: + if pos == 2: offset = 3 - l.append(Enum(level + offset, {"values" : [value]}, "DT")) + l.append(Enum(pos + offset, {"values" : [value]}, "DT")) return l @@ -318,7 +370,9 @@ # Ignore datetime values that conflict with day-level qualifiers. - if not l or from_dt.level != freq["DAILY"] or l[-1].level not in daylevels: + if not l or from_dt.level != freq["DAILY"] or \ + l[-1].level not in daylevels: + l.append(from_dt) from_dt = get_next(iter_dt) @@ -352,24 +406,120 @@ repeat = Pattern(level - 1, {"interval" : 1}, None) l.append(repeat) +def get_multiple(qualifier): + + "Return the time unit multiple for 'qualifier'." + + return multiples.get(qualifier, 1) + # Datetime arithmetic. -def combine(t1, t2): +def is_year_only(t): + + "Return if 't' describes a year." + + return len(t) == lengths[YEARS] + +def is_month_only(t): + + "Return if 't' describes a month within a year." + + return len(t) == lengths[MONTHS] + +def start_of_year(t): + + "Return the start of the year referenced by 't'." + + return (t[0], 1, 1) + +def end_of_year(t): + + "Return the end of the year referenced by 't'." + + return (t[0], 12, 31) + +def start_of_month(t): + + "Return the start of the month referenced by 't'." + + year, month = t[:2] + return (year, month, 1) + +def end_of_month(t): + + "Return the end of the month referenced by 't'." + + year, month = t[:2] + return update(update((year, month, 1), (0, 1, 0)), (0, 0, -1)) + +def day_in_year(t, number): + + "Return the day in the year referenced by 't' with the given 'number'." + + return to_tuple(date(*start_of_year(t)) + timedelta(number - 1)) + +def get_year_length(t): + + "Return the length of the year referenced by 't'." + + first_day = date(*start_of_year(t)) + last_day = date(*end_of_year(t)) + return (last_day - first_day).days + 1 + +def get_weekday(t): + + "Return the 1-based weekday for the month referenced by 't'." + + year, month = t[:2] + return monthrange(year, month)[0] + 1 + +def get_ordered_weekdays(t): """ - Combine tuples 't1' and 't2', returning a copy of 't1' filled with values - from 't2' in positions where 0 appeared in 't1'. + Return the 1-based weekday sequence describing the first week of the month + referenced by 't'. """ - return tuple(map(lambda x, y: x or y, t1, t2)) + first = get_weekday(t) + return range(first, 8) + range(1, first) + +def get_last_weekday_instance(weekday, first_day, last_day): + + """ + Return the last instance number for 'weekday' within the period from + 'first_day' to 'last_day' inclusive. -def scale(interval, pos): + Here, 'weekday' is 1-based (starting on Monday), the returned limit is + 1-based. + """ + + weekday0 = get_first_day(first_day, weekday) + days = (date(*last_day) - weekday0).days + return days / 7 + 1 + +def precision(t, level, value=None): """ - Scale the given 'interval' value to the indicated position 'pos', returning - a tuple with leading zero elements and 'interval' at the stated position. + Return 't' trimmed in resolution to the indicated resolution 'level', + setting 'value' at the given resolution if indicated. """ + pos = positions[level] + + if value is None: + return t[:pos + 1] + else: + return t[:pos] + (value,) + +def scale(interval, level): + + """ + Scale the given 'interval' value in resolution to the indicated resolution + 'level', returning a tuple with leading zero elements and 'interval' at the + stated position. + """ + + pos = positions[level] return (0,) * pos + (interval,) def get_seconds(t): @@ -413,24 +563,26 @@ d = datetime(*updated_for_months) s = timedelta(step[2], get_seconds(step)) - return to_tuple(d + s, len(t)) + return to_tuple(d + s)[:len(t)] -def to_tuple(d, n=None): +def to_tuple(d): - "Return 'd' as a tuple, optionally trimming the result to 'n' positions." + "Return date or datetime 'd' as a tuple." if not isinstance(d, date): return d - if n is None: - if isinstance(d, datetime): - n = 6 - else: - n = 3 + if isinstance(d, datetime): + n = 6 + else: + n = 3 return d.timetuple()[:n] def get_first_day(first_day, weekday): - "Return the first occurrence at or after 'first_day' of 'weekday'." + """ + Return the first occurrence at or after 'first_day' of 'weekday' as a date + instance. + """ first_day = date(*first_day) first_weekday = first_day.isoweekday() @@ -441,7 +593,10 @@ def get_last_day(last_day, weekday): - "Return the last occurrence at or before 'last_day' of 'weekday'." + """ + Return the last occurrence at or before 'last_day' of 'weekday' as a date + instance. + """ last_day = date(*last_day) last_weekday = last_day.isoweekday() @@ -450,6 +605,85 @@ else: return last_day - timedelta(last_weekday - weekday) +# Value expansion and sorting. + +def sort_values(values, limit=None): + + """ + Sort the given 'values' using 'limit' to determine the positions of negative + values. + """ + + # Convert negative values to positive values according to the value limit. + + if limit is not None: + l = map(lambda x, limit=limit: x < 0 and x + 1 + limit or x, values) + else: + l = values[:] + + l.sort() + return l + +def compare_weekday_selectors(x, y, weekdays): + + """ + Compare 'x' and 'y' values of the form (weekday number, instance number) + using 'weekdays' to define an ordering of the weekday numbers. + """ + + return cmp((x[1], weekdays.index(x[0])), (y[1], weekdays.index(y[0]))) + +def sort_weekdays(values, first_day, last_day): + + """ + Return a sorted copy of the given 'values', each having the form (weekday + number, instance number) using 'weekdays' to define the ordering of the + weekday numbers and 'limit' to determine the positions of negative instance + numbers. + """ + + weekdays = get_ordered_weekdays(first_day) + + # Expand the values to incorporate specific weekday instances. + + l = [] + + for weekday, index in values: + + # Obtain the last instance number of the weekday in the period. + + limit = get_last_weekday_instance(weekday, first_day, last_day) + + # For specific instances, convert negative selections. + + if index is not None: + l.append((weekday, index < 0 and index + 1 + limit or index)) + + # For None, introduce selections for all instances of the weekday. + + else: + index = 1 + while index <= limit: + l.append((weekday, index)) + index += 1 + + # Sort the values so that the resulting dates are ordered. + + fn = lambda x, y, weekdays=weekdays: compare_weekday_selectors(x, y, weekdays) + l.sort(cmp=fn) + return l + +def convert_positions(setpos): + + "Convert 'setpos' to 0-based indexes." + + l = [] + if setpos: + for pos in setpos: + index = pos < 0 and pos or pos - 1 + l.append(index) + return l + # Classes for producing instances from recurrence structures. class Selector: @@ -472,20 +706,15 @@ self.selecting = selecting self.first = first - # Define the index of values from datetimes involved with this selector. - - self.pos = positions[level] + def __repr__(self): + return "%s(%r, %r, %r, %r)" % (self.__class__.__name__, self.level, + self.args, self.qualifier, self.first) - def __repr__(self): - return "%s(%r, %r, %r, %r)" % (self.__class__.__name__, self.level, self.args, self.qualifier, self.first) - - def materialise(self, start, end, count=None, setpos=None, inclusive=False): + def select(self, start, end, inclusive=False): """ - Starting at 'start', materialise instances up to but not including any - at 'end' or later, returning at most 'count' if specified, and returning - only the occurrences indicated by 'setpos' if specified. A list of - instances is returned. + Return an iterator over instances starting at 'start' and continuing up + to but not including any at 'end' or later. If 'inclusive' is specified, the selection of instances will include the end of the search period if present in the results. @@ -493,287 +722,125 @@ start = to_tuple(start) end = to_tuple(end) - counter = count and [0, count] - results = self.materialise_items(start, start, end, counter, setpos, inclusive) - results.sort() - return results[:count] + return self.materialise_items(start, start, end, inclusive) - def materialise_item(self, current, earliest, next, counter, setpos=None, inclusive=False): + def materialise(self, start, end, inclusive=False): """ - Given the 'current' instance, the 'earliest' acceptable instance, the - 'next' instance, an instance 'counter', and the optional 'setpos' - criteria, return a list of result items. Where no selection within the - current instance occurs, the current instance will be returned as a - result if the same or later than the earliest acceptable instance. - """ - - if self.selecting: - return self.selecting.materialise_items(current, earliest, next, counter, setpos, inclusive) - elif earliest <= current: - return [current] - else: - return [] - - def convert_positions(self, setpos): - - "Convert 'setpos' to 0-based indexes." - - l = [] - for pos in setpos: - lower = pos < 0 and pos or pos - 1 - upper = pos > 0 and pos or pos < -1 and pos + 1 or None - l.append((lower, upper)) - return l - - def select_positions(self, results, setpos): - - "Select in 'results' the 1-based positions given by 'setpos'." - - results.sort() - l = [] - for lower, upper in self.convert_positions(setpos): - l += results[lower:upper] - return l - - def filter_by_period(self, results, start, end, inclusive): - - """ - Filter 'results' so that only those at or after 'start' and before 'end' - are returned. + Starting at 'start', materialise instances up to but not including any + at 'end' or later. A list of instances is returned. If 'inclusive' is specified, the selection of instances will include the end of the search period if present in the results. """ - l = [] - for result in results: - if start <= result and (inclusive and result <= end or result < end): - l.append(result) - return l + return list(self.select(start, end, inclusive)) class Pattern(Selector): "A selector of time periods according to a repeating pattern." - def materialise_items(self, context, start, end, counter, setpos=None, inclusive=False): + def __init__(self, level, args, qualifier, selecting=None, first=False): + Selector.__init__(self, level, args, qualifier, selecting, first) + + multiple = get_multiple(self.qualifier) + interval = self.args.get("interval", 1) + + # Define the step between result periods. + + self.step = scale(interval * multiple, level) + + # Define the scale of a single period. + + self.unit_step = scale(multiple, level) + + def materialise_items(self, context, start, end, inclusive=False): """ - Bounded by the given 'context', return periods within 'start' to 'end', - updating the 'counter', selecting only the indexes specified by 'setpos' - (if given). + Bounded by the given 'context', return periods within 'start' to 'end'. If 'inclusive' is specified, the selection of periods will include those starting at the end of the search period, if present in the results. """ - # Define the step between result periods. - - interval = self.args.get("interval", 1) * units.get(self.qualifier, 1) - step = scale(interval, self.pos) - - # Define the scale of a single period. - - unit_interval = units.get(self.qualifier, 1) - unit_step = scale(unit_interval, self.pos) - # Employ the context as the current period if this is the first # qualifier in the selection chain. if self.first: - current = context[:self.pos+1] + current = precision(context, self.level) # Otherwise, obtain the first value at this resolution within the # context period. else: - first = scale(firstvalues[self.level], self.pos) - current = combine(context[:self.pos], first) - - results = [] - - # Obtain periods before the end (and also at the end if inclusive), - # provided that any limit imposed by the counter has not been exceeded. - - while (inclusive and current <= end or current < end) and \ - (counter is None or counter[0] < counter[1]): - - # Increment the current datetime by the step for the next period. - - next = update(current, step) - - # Determine the end point of the current period. - - current_end = update(current, unit_step) + current = precision(context, self.level, firstvalues[self.level]) - # Obtain any period or periods within the bounds defined by the - # current period and any contraining start and end points, plus - # counter, setpos and inclusive details. - - interval_results = self.materialise_item(current, max(current, start), min(current_end, end), counter, setpos, inclusive) - - # Update the counter with the number of identified results. - - if counter is not None: - counter[0] += len(interval_results) - - # Accumulate the results. - - results += interval_results - - # Visit the next instance. - - current = next - - return results + return PatternIterator(self, current, start, end, inclusive, + self.step, self.unit_step) class WeekDayFilter(Selector): "A selector of instances specified in terms of day numbers." - def materialise_items(self, context, start, end, counter, setpos=None, inclusive=False): - step = scale(1, 2) - results = [] + def __init__(self, level, args, qualifier, selecting=None, first=False): + Selector.__init__(self, level, args, qualifier, selecting, first) + self.step = scale(1, WEEKS) + + def materialise_items(self, context, start, end, inclusive=False): # Get weekdays in the year. - if len(context) == 1: - first_day = (context[0], 1, 1) - last_day = (context[0], 12, 31) + if is_year_only(context): + first_day = start_of_year(context) + last_day = end_of_year(context) # Get weekdays in the month. - elif len(context) == 2: - first_day = (context[0], context[1], 1) - last_day = update((context[0], context[1], 1), (0, 1, 0)) - last_day = update(last_day, (0, 0, -1)) + elif is_month_only(context): + first_day = start_of_month(context) + last_day = end_of_month(context) # Get weekdays in the week. else: current = context values = [value for (value, index) in self.args["values"]] - - while (inclusive and current <= end or current < end): - next = update(current, step) - if date(*current).isoweekday() in values: - results += self.materialise_item(current, max(current, start), min(next, end), counter, inclusive=inclusive) - current = next - - if setpos: - return self.select_positions(results, setpos) - else: - return results - - # Find each of the given days. - - for value, index in self.args["values"]: - if index is not None: - offset = timedelta(7 * (abs(index) - 1)) - - if index < 0: - current = to_tuple(get_last_day(last_day, value) - offset, 3) - else: - current = to_tuple(get_first_day(first_day, value) + offset, 3) - - next = update(current, step) + return WeekDayIterator(self, current, start, end, inclusive, self.step, values) - # To support setpos, only current and next bound the search, not - # the period in addition. - - results += self.materialise_item(current, current, next, counter, inclusive=inclusive) - - else: - if index < 0: - current = to_tuple(get_last_day(last_day, value), 3) - direction = operator.sub - else: - current = to_tuple(get_first_day(first_day, value), 3) - direction = operator.add - - while first_day <= current <= last_day: - next = update(current, step) - - # To support setpos, only current and next bound the search, not - # the period in addition. - - results += self.materialise_item(current, current, next, counter, inclusive=inclusive) - current = to_tuple(direction(date(*current), timedelta(7)), 3) - - # Extract selected positions and remove out-of-period instances. - - if setpos: - results = self.select_positions(results, setpos) - - return self.filter_by_period(results, start, end, inclusive) + current = first_day + values = sort_weekdays(self.args["values"], first_day, last_day) + return WeekDayGeneralIterator(self, current, start, end, inclusive, self.step, values) class Enum(Selector): - def materialise_items(self, context, start, end, counter, setpos=None, inclusive=False): - step = scale(1, self.pos) - results = [] - for value in self.args["values"]: - current = combine(context[:self.pos], scale(value, self.pos)) - next = update(current, step) + + "A generic value selector." - # To support setpos, only current and next bound the search, not - # the period in addition. - - results += self.materialise_item(current, current, next, counter, setpos, inclusive) + def __init__(self, level, args, qualifier, selecting=None, first=False): + Selector.__init__(self, level, args, qualifier, selecting, first) + self.step = scale(1, level) - # Extract selected positions and remove out-of-period instances. - - if setpos: - results = self.select_positions(results, setpos) - - return self.filter_by_period(results, start, end, inclusive) + def materialise_items(self, context, start, end, inclusive=False): + values = sort_values(self.args["values"]) + return EnumIterator(self, context, start, end, inclusive, self.step, values) class MonthDayFilter(Enum): - def materialise_items(self, context, start, end, counter, setpos=None, inclusive=False): - last_day = monthrange(context[0], context[1])[1] - step = scale(1, self.pos) - results = [] - for value in self.args["values"]: - if value < 0: - value = last_day + 1 + value - current = combine(context, scale(value, self.pos)) - next = update(current, step) + + "A selector of month days." - # To support setpos, only current and next bound the search, not - # the period in addition. - - results += self.materialise_item(current, current, next, counter, inclusive=inclusive) - - # Extract selected positions and remove out-of-period instances. - - if setpos: - results = self.select_positions(results, setpos) - - return self.filter_by_period(results, start, end, inclusive) + def materialise_items(self, context, start, end, inclusive=False): + last_day = end_of_month(context)[2] + values = sort_values(self.args["values"], last_day) + return EnumIterator(self, context, start, end, inclusive, self.step, values) class YearDayFilter(Enum): - def materialise_items(self, context, start, end, counter, setpos=None, inclusive=False): - first_day = date(context[0], 1, 1) - next_first_day = date(context[0] + 1, 1, 1) - year_length = (next_first_day - first_day).days - step = scale(1, self.pos) - results = [] - for value in self.args["values"]: - if value < 0: - value = year_length + 1 + value - current = to_tuple(first_day + timedelta(value - 1), 3) - next = update(current, step) + + "A selector of days in years." - # To support setpos, only current and next bound the search, not - # the period in addition. - - results += self.materialise_item(current, current, next, counter, inclusive=inclusive) - - # Extract selected positions and remove out-of-period instances. - - if setpos: - results = self.select_positions(results, setpos) - - return self.filter_by_period(results, start, end, inclusive) + def materialise_items(self, context, start, end, inclusive=False): + first_day = start_of_year(context) + year_length = get_year_length(context) + values = sort_values(self.args["values"], year_length) + return YearDayFilterIterator(self, first_day, start, end, inclusive, self.step, values) special_enum_levels = { "BYDAY" : WeekDayFilter, @@ -781,6 +848,362 @@ "BYYEARDAY" : YearDayFilter, } +class LimitSelector(Selector): + + "A result set limit selector." + + def materialise_items(self, context, start, end, inclusive=False): + limit = self.args["values"][0] + return LimitIterator(self, context, start, end, inclusive, limit) + +class PositionSelector(Selector): + + "A result set position selector." + + def __init__(self, level, args, qualifier, selecting=None, first=False): + Selector.__init__(self, level, args, qualifier, selecting, first) + self.step = scale(1, level) + + def materialise_items(self, context, start, end, inclusive=False): + values = convert_positions(sort_values(self.args["values"])) + return PositionIterator(self, context, start, end, inclusive, self.step, values) + +# Iterator classes. + +class SelectorIterator: + + "An iterator over selected data." + + def __init__(self, selector, current, start, end, inclusive): + + """ + Define an iterator having the 'current' point in time, 'start' and 'end' + limits, and whether the selection is 'inclusive'. + """ + + self.selector = selector + self.current = current + self.start = start + self.end = end + self.inclusive = inclusive + + # Iterator over selections within this selection. + + self.iterator = None + + def __iter__(self): + return self + + def next_item(self, earliest, next): + + """ + Given the 'earliest' acceptable instance and the 'next' instance, return + a list of result items. + + Where no selection within the current instance occurs, the current + instance will be returned as a result if the same or later than the + earliest acceptable instance. + """ + + # Obtain an item from a selector operating within this selection. + + selecting = self.selector.selecting + + if selecting: + + # Obtain an iterator for any selector within the current period. + + if not self.iterator: + self.iterator = selecting.materialise_items(self.current, + earliest, next, self.inclusive) + + # Attempt to obtain and return a value. + + return self.iterator.next() + + # Return items within this selection. + + else: + return self.current + + def filter_by_period(self, result): + + "Return whether 'result' occurs within the selection period." + + return (self.inclusive and result <= self.end or result < self.end) and \ + self.start <= result + + def at_limit(self): + + "Obtain periods before the end (and also at the end if inclusive)." + + return not self.inclusive and self.current == self.end or \ + self.current > self.end + +class PatternIterator(SelectorIterator): + + "An iterator for a general selection pattern." + + def __init__(self, selector, current, start, end, inclusive, step, unit_step): + SelectorIterator.__init__(self, selector, current, start, end, inclusive) + self.step = step + self.unit_step = unit_step + + def next(self): + + "Return the next value." + + while not self.at_limit(): + + # Increment the current datetime by the step for the next period. + + next = update(self.current, self.step) + + # Determine the end point of the current period. + + current_end = update(self.current, self.unit_step) + + # Obtain any period or periods within the bounds defined by the + # current period and any contraining start and end points. + + try: + result = self.next_item(max(self.current, self.start), + min(current_end, self.end)) + + # Obtain the next period if not selecting within this period. + + if not self.selector.selecting: + self.current = next + + # Filter out periods. + + if self.filter_by_period(result): + return result + + # Move on to the next instance. + + except StopIteration: + self.current = next + self.iterator = None + + raise StopIteration + +class WeekDayIterator(SelectorIterator): + + "An iterator over weekday selections in week periods." + + def __init__(self, selector, current, start, end, inclusive, step, values): + SelectorIterator.__init__(self, selector, current, start, end, inclusive) + self.step = step + self.values = values + + def next(self): + + "Return the next value." + + while not self.at_limit(): + + # Increment the current datetime by the step for the next period. + + next = update(self.current, self.step) + + # Determine whether the day is one chosen. + + if date(*self.current).isoweekday() in self.values: + try: + result = self.next_item(max(self.current, self.start), + min(next, self.end)) + + # Obtain the next period if not selecting within this period. + + if not self.selector.selecting: + self.current = next + + return result + + # Move on to the next instance. + + except StopIteration: + self.current = next + self.iterator = None + + else: + self.current = next + self.iterator = None + + raise StopIteration + +class EnumIterator(SelectorIterator): + + "An iterator over specific value selections." + + def __init__(self, selector, current, start, end, inclusive, step, values): + SelectorIterator.__init__(self, selector, current, start, end, inclusive) + self.step = step + + # Derive selected periods from a base and the indicated values. + + self.base = current + + # Iterate over the indicated period values. + + self.values = iter(values) + self.value = None + + def get_selected_period(self): + + "Return the period indicated by the current value." + + return precision(self.base, self.selector.level, self.value) + + def next(self): + + "Return the next value." + + while True: + + # Find each of the given selected values. + + if not self.selector.selecting or self.value is None: + self.value = self.values.next() + + # Select a period for each value at the current resolution. + + self.current = self.get_selected_period() + next = update(self.current, self.step) + + # To support setpos, only current and next bound the search, not + # the period in addition. + + try: + return self.next_item(self.current, next) + + # Move on to the next instance. + + except StopIteration: + self.value = None + self.iterator = None + + raise StopIteration + +class WeekDayGeneralIterator(EnumIterator): + + "An iterator over weekday selections in month and year periods." + + def get_selected_period(self): + + "Return the day indicated by the current day value." + + value, index = self.value + offset = timedelta(7 * (index - 1)) + weekday0 = get_first_day(self.base, value) + return precision(to_tuple(weekday0 + offset), DAYS) + +class YearDayFilterIterator(EnumIterator): + + "An iterator over day-in-year selections." + + def get_selected_period(self): + + "Return the day indicated by the current day value." + + offset = timedelta(self.value - 1) + return precision(to_tuple(date(*self.base) + offset), DAYS) + +class LimitIterator(SelectorIterator): + + "A result set limiting iterator." + + def __init__(self, selector, context, start, end, inclusive, limit): + SelectorIterator.__init__(self, selector, context, start, end, inclusive) + self.limit = limit + self.count = 0 + + def next(self): + + "Return the next value." + + if self.count < self.limit: + self.count += 1 + result = self.next_item(self.start, self.end) + return result + + raise StopIteration + +class PositionIterator(SelectorIterator): + + "An iterator over results, selecting positions." + + def __init__(self, selector, context, start, end, inclusive, step, positions): + SelectorIterator.__init__(self, selector, context, start, end, inclusive) + self.step = step + + # Positions to select. + + self.positions = positions + + # Queue to hold values matching the negative position values. + + self.queue_length = positions and positions[0] < 0 and abs(positions[0]) or 0 + self.queue = [] + + # Result position. + + self.resultpos = 0 + + def next(self): + + "Return the next value." + + while True: + try: + result = self.next_item(self.start, self.end) + + # Positive positions can have their values released immediately. + + selected = self.resultpos in self.positions + self.resultpos += 1 + + if selected: + return result + + # Negative positions must be held until this iterator completes and + # then be released. + + if self.queue_length: + self.queue.append(result) + if len(self.queue) > self.queue_length: + del self.queue[0] + + except StopIteration: + + # With a queue and positions, attempt to find the referenced + # positions. + + if self.queue and self.positions: + index = self.positions[0] + del self.positions[0] + + # Only negative positions are used at this point. + + if index < 0: + try: + return self.queue[index] + except IndexError: + pass + + # With only positive positions remaining, signal the end of + # the collection. + + else: + raise + + # With no queue or positions remaining, signal the end of the + # collection. + + else: + raise + # Public functions. def connect_selectors(selectors): @@ -792,10 +1215,19 @@ """ current = selectors[0] - current.first = True + current.first = first = True + for selector in selectors[1:]: current.selecting = selector + + # Allow selectors within the limit selector to act as if they are first + # in the chain and will operate using the supplied datetime context. + + first = isinstance(current, LimitSelector) + current = selector + current.first = first + return selectors[0] def get_selector(dt, qualifiers):