# HG changeset patch # User Paul Boddie # Date 1496593698 -7200 # Node ID f8e01f701116ad76ba12f03808904e3113b1ea96 # Parent 651da7f796f5b4ece5728cad214768c59e9f75c4 Tidied and commented, also apparently fixing selector context initialisation in certain cases. diff -r 651da7f796f5 -r f8e01f701116 vRecurrence.py --- a/vRecurrence.py Fri Jun 16 23:19:02 2017 +0200 +++ b/vRecurrence.py Sun Jun 04 18:28:18 2017 +0200 @@ -3,7 +3,7 @@ """ Recurrence rule calculation. -Copyright (C) 2014, 2015 Paul Boddie +Copyright (C) 2014, 2015, 2017 Paul Boddie This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -187,6 +187,10 @@ l = [] for qualifier, args in qualifiers: + + # Distinguish between enumerators, used to select particular periods, + # and frequencies, used to select repeating periods. + if enum.has_key(qualifier): level = enum[qualifier] if special_enum_levels.has_key(qualifier): @@ -209,10 +213,17 @@ l = [] offset = 0 + for level, value in enumerate(datetime): + + # At the day number, adjust the frequency level offset to reference + # days (and then hours, minutes, seconds). + if level == 2: offset = 3 + l.append(Enum(level + offset, {"values" : [value]}, "DT")) + return l def combine_datetime_with_qualifiers(datetime, qualifiers): @@ -231,6 +242,10 @@ from_q = get_next(iter_q) have_q = False + + # The initial context for any qualifiers is taken from the first datetime + # value, which should be the year. + context = [] context.append(from_dt.args["values"][0]) @@ -240,7 +255,8 @@ _level = from_dt.level level = from_q.level - # Datetime value at wider resolution. + # Datetime value at wider resolution. Use the datetime value to expand + # the context within which qualifiers will operate. if _level < level: from_dt = get_next(iter_dt) @@ -249,41 +265,69 @@ # Qualifier at wider or same resolution as datetime value. else: + # Without any previous qualifier, introduce a special qualifier to + # provide context for this qualifier. + if not have_q: - if isinstance(from_q, Enum) and level > 0: - repeat = Pattern(level - 1, {"interval" : 1}, None) - repeat.context = tuple(context) - l.append(repeat) + add_initial_qualifier(from_q, level, context, l) have_q = True + # Associate the datetime context with the qualifier and add it to + # the combined list. + from_q.context = tuple(context) l.append(from_q) - from_q = get_next(iter_q) + + # Datetime value at same resolution. Expand the context using the + # value. if _level == level: - context.append(from_dt.args["values"][0]) from_dt = get_next(iter_dt) + if from_dt: + context.append(from_dt.args["values"][0]) - # Complete the list. + # Get the next qualifier. + + from_q = get_next(iter_q) + + # Complete the list by adding remaining datetime enumerators. while from_dt: l.append(from_dt) from_dt = get_next(iter_dt) + # Complete the list by adding remaining qualifiers. + while from_q: if not have_q: - if isinstance(from_q, Enum) and level > 0: - repeat = Pattern(level - 1, {"interval" : 1}, None) - repeat.context = tuple(context) - l.append(repeat) + add_initial_qualifier(from_q, level, context, l) have_q = True + # Associate the datetime context with the qualifier and add it to the + # combined list. + from_q.context = tuple(context) l.append(from_q) + + # Get the next qualifier. + from_q = get_next(iter_q) return l +def add_initial_qualifier(from_q, level, context, l): + + """ + Take the first qualifier 'from_q' at the given resolution 'level', using it + to create an initial qualifier providing appropriate context, using the + given 'context', adding it to the combined list 'l' if required. + """ + + if isinstance(from_q, Enum) and level > 0: + repeat = Pattern(level - 1, {"interval" : 1}, None) + repeat.context = tuple(context) + l.append(repeat) + # Datetime arithmetic. def combine(t1, t2): @@ -399,11 +443,17 @@ """ self.level = level - self.pos = positions[level] self.args = args self.qualifier = qualifier + self.selecting = selecting + + # Define an empty context to be overridden. + self.context = () - self.selecting = selecting + + # 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.context) @@ -483,31 +533,70 @@ class Pattern(Selector): - "A selector of instances according to a repeating pattern." + "A selector of time periods according to a repeating pattern." def materialise_items(self, context, start, end, counter, setpos=None, 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). + + If 'inclusive' is specified, the selection of periods will include those + starting at the end of the search period, if present in the results. + """ + + # Obtain the pattern context's value at the appropriate level. + first = scale(self.context[self.pos], self.pos) - # Define the step between items. + # 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 item. + # Define the scale of a single period. unit_interval = units.get(self.qualifier, 1) unit_step = scale(unit_interval, self.pos) + # Combine specific context details with the pattern context. This should + # make the result more specific than the pattern context. + current = combine(context, first) results = [] - while (inclusive and current <= end or current < end) and (counter is None or counter[0] < counter[1]): + # 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) + + # 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