# HG changeset patch # User Paul Boddie # Date 1508792757 -7200 # Node ID b489a51915e6360c5486fa11c876d39220f326ac # Parent 99fb5e99498cbe95f34d2e4932b574319a00f1f8 Changed the recurrence computation to employ iterators supplying values incrementally, as opposed to looping over the selection periods and building collections of result values. Changed the way that BYSETPOS and COUNT are handled, introducing them as selectors employing iterators at the appropriate places in the selection chain. Updated the test of qualifiers and recurrence generation. diff -r 99fb5e99498c -r b489a51915e6 tests/qualifiers.py --- a/tests/qualifiers.py Sun Oct 22 17:02:40 2017 +0200 +++ b/tests/qualifiers.py Mon Oct 23 23:05:57 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" : [15, 2]}) # test ordering + ("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] diff -r 99fb5e99498c -r b489a51915e6 vRecurrence.py --- a/vRecurrence.py Sun Oct 22 17:02:40 2017 +0200 +++ b/vRecurrence.py Mon Oct 23 23:05:57 2017 +0200 @@ -141,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): @@ -163,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. @@ -179,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. @@ -232,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: @@ -240,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): @@ -650,9 +678,10 @@ "Convert 'setpos' to 0-based indexes." l = [] - for pos in setpos: - index = pos < 0 and pos or pos - 1 - l.append(index) + 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. @@ -681,12 +710,11 @@ 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 materialise(self, start, end, count=None, 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 + at 'end' or later, returning at most 'count' if specified. A list of instances is returned. If 'inclusive' is specified, the selection of instances will include the @@ -695,66 +723,36 @@ start = to_tuple(start) end = to_tuple(end) - counter = Counter(count) - results = self.materialise_items(start, start, end, counter, setpos, inclusive) - return results[:count] - - def materialise_item(self, current, earliest, next, counter, setpos=None, 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) - else: - return [current] - - def select_positions(self, results, setpos): - - "Select in 'results' the 1-based positions given by 'setpos'." - - l = [] - for index in convert_positions(setpos): - l.append(results[index]) - return l - - def filter_by_period(self, result, start, end, inclusive): - - "Return whether 'result' occurs at or after 'start' and before 'end'." - - return start <= result and (inclusive and result <= end or result < end) + results = self.materialise_items(start, start, end, inclusive) + return list(results)[:count] 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. - - multiple = get_multiple(self.qualifier) - interval = self.args.get("interval", 1) * multiple - step = scale(interval, self.level) - - # Define the scale of a single period. - - unit_step = scale(multiple, self.level) - # Employ the context as the current period if this is the first # qualifier in the selection chain. @@ -767,51 +765,18 @@ else: current = precision(context, self.level, firstvalues[self.level]) - 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 \ - not counter.at_limit(): - - # 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) - - # 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. - - counter += 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, WEEKS) - 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. @@ -830,130 +795,42 @@ 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. + return WeekDayIterator(self, current, start, end, inclusive, self.step, values) - for value, index in sort_weekdays(self.args["values"], first_day, last_day): - offset = timedelta(7 * (index - 1)) - - current = precision(to_tuple(get_first_day(first_day, value) + offset), DAYS) - 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) - - # Extract selected positions and remove out-of-period instances. - - if setpos: - results = self.select_positions(results, setpos) - - return filter(lambda result: - self.filter_by_period(result, start, end, inclusive), - results) + 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): "A generic value selector." - def materialise_items(self, context, start, end, counter, setpos=None, inclusive=False): - step = scale(1, self.level) - results = [] - - # Select each value at the current resolution. - - for value in sort_values(self.args["values"]): - current = precision(context, self.level, value) - next = update(current, step) + def __init__(self, level, args, qualifier, selecting=None, first=False): + Selector.__init__(self, level, args, qualifier, selecting, first) + self.step = scale(1, level) - # 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) - - # Extract selected positions and remove out-of-period instances. - - if setpos: - results = self.select_positions(results, setpos) - - return filter(lambda result: - self.filter_by_period(result, start, end, inclusive), - results) + 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): "A selector of month days." - def materialise_items(self, context, start, end, counter, setpos=None, inclusive=False): - step = scale(1, self.level) - results = [] - + def materialise_items(self, context, start, end, inclusive=False): last_day = end_of_month(context)[2] - - for value in sort_values(self.args["values"], last_day): - current = precision(context, self.level, value) - 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) - - # Extract selected positions and remove out-of-period instances. - - if setpos: - results = self.select_positions(results, setpos) - - return filter(lambda result: - self.filter_by_period(result, start, end, inclusive), - results) + values = sort_values(self.args["values"], last_day) + return EnumIterator(self, context, start, end, inclusive, self.step, values) class YearDayFilter(Enum): "A selector of days in years." - def materialise_items(self, context, start, end, counter, setpos=None, inclusive=False): - step = scale(1, self.level) - results = [] - + def materialise_items(self, context, start, end, inclusive=False): + first_day = start_of_year(context) year_length = get_year_length(context) - - for value in sort_values(self.args["values"], year_length): - current = day_in_year(context, value) - 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) - - # Extract selected positions and remove out-of-period instances. - - if setpos: - results = self.select_positions(results, setpos) - - return filter(lambda result: - self.filter_by_period(result, start, end, inclusive), - results) + values = sort_values(self.args["values"], year_length) + return YearDayFilterIterator(self, first_day, start, end, inclusive, self.step, values) special_enum_levels = { "BYDAY" : WeekDayFilter, @@ -961,20 +838,361 @@ "BYYEARDAY" : YearDayFilter, } -class Counter: +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 counter to track instance quantities." + "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. - def __init__(self, limit): - self.current = 0 - self.limit = limit +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'. + """ - def __iadd__(self, n): - self.current += n + 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): - return self.limit is not None and self.current >= self.limit + + "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. @@ -987,10 +1205,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):