# HG changeset patch # User Paul Boddie # Date 1496700125 -7200 # Node ID 7023d88d726051bae98f61b9d72199f383553a7f # Parent c6bcb246a972e19b147f92fedac0d3ded074ccf5 Removed context details from selectors, converting datetime values to enumerators only when needed to provide the necessary result resolution. diff -r c6bcb246a972 -r 7023d88d7260 tests/qualifiers.py --- a/tests/qualifiers.py Mon Jun 05 18:42:20 2017 +0200 +++ b/tests/qualifiers.py Tue Jun 06 00:02:05 2017 +0200 @@ -3,7 +3,7 @@ """ Test qualifiers for recurring events. -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 @@ -772,4 +772,36 @@ print l[-1] == (1998, 3, 30, 9, 0, 0), (1998, 3, 30, 9, 0, 0), l[-1] print +qualifiers = get_qualifiers(["FREQ=MONTHLY", "BYMONTHDAY=5", "FREQ=HOURLY", "INTERVAL=12"]) + +l = order_qualifiers(qualifiers) +show(l) +dt = (2017, 6, 13) +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, (2019, 1, 1)) +print len(l) == 36 +print l[0] == (2017, 7, 5, 0), (2017, 7, 5, 0), l[0] +print l[-1] == (2018, 12, 5, 12), (2018, 12, 5, 12), l[-1] + +qualifiers = get_qualifiers(["FREQ=DAILY", "BYMONTH=1"]) + +l = order_qualifiers(qualifiers) +show(l) +dt = (2017, 6, 13) +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, (2019, 1, 1)) +print len(l) == 31 +print l[0] == (2018, 1, 1), (2018, 1, 1), l[0] +print l[-1] == (2018, 1, 31), (2018, 1, 31), l[-1] + # vim: tabstop=4 expandtab shiftwidth=4 diff -r c6bcb246a972 -r 7023d88d7260 vRecurrence.py --- a/vRecurrence.py Mon Jun 05 18:42:20 2017 +0200 +++ b/vRecurrence.py Tue Jun 06 00:02:05 2017 +0200 @@ -84,11 +84,20 @@ "BYSECOND" ) +# Levels defining days. + +daylevels = [2, 3, 4, 5] + # Map from levels to lengths of datetime tuples. lengths = [1, 2, 3, 3, 3, 3, 4, 5, 6] positions = [l-1 for l in lengths] +# Define the lowest values at each resolution (years, months, days... hours, +# minutes, seconds). + +firstvalues = [0, 1, 1, 1, 1, 1, 0, 0, 0] + # Map from qualifiers to interval units. Here, weeks are defined as 7 days. units = {"WEEKLY" : 7} @@ -245,6 +254,22 @@ """ Combine 'datetime' with 'qualifiers' to produce a structure for recurrence production. + + Initial datetime values appearing at broader resolutions than any qualifiers + are ignored, since their details will be used when materialising the + results. + + Qualifiers are accumulated in order to define a selection. Datetime values + occurring between qualifiers or at the same resolution as qualifiers are + ignored. + + Any remaining datetime values are introduced as enumerators, provided that + they do not conflict with qualifiers. For example, specific day values + conflict with day selectors and weekly qualifiers. + + The purpose of the remaining datetime values is to define a result within + a period selected by the most precise qualifier. For example, the selection + of a day and month in a year recurrence. """ iter_dt = iter(get_datetime_structure(datetime)) @@ -254,51 +279,34 @@ from_dt = get_next(iter_dt) 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]) - # Consume from both lists, merging entries. while from_dt and from_q: _level = from_dt.level level = from_q.level - # Datetime value at wider resolution. Use the datetime value to expand - # the context within which qualifiers will operate. + # Datetime value at wider resolution. if _level < level: from_dt = get_next(iter_dt) - context.append(from_dt.args["values"][0]) # 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: - add_initial_qualifier(from_q, level, context, l) + add_initial_qualifier(from_q, level, l) have_q = True - # Associate the datetime context with the qualifier and add it to - # the combined list. + # Add the qualifier to the combined list. - from_q.context = tuple(context) l.append(from_q) - # Datetime value at same resolution. Expand the context using the - # value. + # Datetime value at same resolution. if _level == level: from_dt = get_next(iter_dt) - if from_dt: - context.append(from_dt.args["values"][0]) # Get the next qualifier. @@ -307,20 +315,23 @@ # Complete the list by adding remaining datetime enumerators. while from_dt: - l.append(from_dt) + + # 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: + l.append(from_dt) + from_dt = get_next(iter_dt) # Complete the list by adding remaining qualifiers. while from_q: if not have_q: - add_initial_qualifier(from_q, level, context, l) + add_initial_qualifier(from_q, level, l) have_q = True - # Associate the datetime context with the qualifier and add it to the - # combined list. + # Add the qualifier to the combined list. - from_q.context = tuple(context) l.append(from_q) # Get the next qualifier. @@ -329,17 +340,16 @@ return l -def add_initial_qualifier(from_q, level, context, l): +def add_initial_qualifier(from_q, level, 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. + to create an initial qualifier, 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[:level]) l.append(repeat) # Datetime arithmetic. @@ -446,7 +456,7 @@ "A generic selector." - def __init__(self, level, args, qualifier, selecting=None): + def __init__(self, level, args, qualifier, selecting=None, first=False): """ Initialise at the given 'level' a selector employing the given 'args' @@ -460,17 +470,14 @@ self.args = args self.qualifier = qualifier self.selecting = selecting - - # Define an empty context to be overridden. - - self.context = () + 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.context) + 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): @@ -487,7 +494,7 @@ start = to_tuple(start) end = to_tuple(end) counter = count and [0, count] - results = self.materialise_items(self.context, start, end, counter, setpos, inclusive) + results = self.materialise_items(start, start, end, counter, setpos, inclusive) results.sort() return results[:count] @@ -560,10 +567,6 @@ 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 result periods. interval = self.args.get("interval", 1) * units.get(self.qualifier, 1) @@ -574,12 +577,19 @@ unit_interval = units.get(self.qualifier, 1) unit_step = scale(unit_interval, self.pos) - # Combine supplied context details with the pattern context. This should - # provide additional resolution information that may be missing from the - # supplied context. For example, the outer selector may indicate a month - # context, but this selector may need day information. + # 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 = combine(context, first) + # 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), @@ -702,7 +712,7 @@ step = scale(1, self.pos) results = [] for value in self.args["values"]: - current = combine(context, scale(value, self.pos)) + current = combine(context[:self.pos], scale(value, self.pos)) next = update(current, step) # To support setpos, only current and next bound the search, not @@ -782,6 +792,7 @@ """ current = selectors[0] + current.first = True for selector in selectors[1:]: current.selecting = selector current = selector