1.1 --- a/vRecurrence.py Thu Jun 01 23:26:38 2017 +0200
1.2 +++ b/vRecurrence.py Sun Jun 04 18:28:18 2017 +0200
1.3 @@ -3,7 +3,7 @@
1.4 """
1.5 Recurrence rule calculation.
1.6
1.7 -Copyright (C) 2014, 2015 Paul Boddie <paul@boddie.org.uk>
1.8 +Copyright (C) 2014, 2015, 2017 Paul Boddie <paul@boddie.org.uk>
1.9
1.10 This program is free software; you can redistribute it and/or modify it under
1.11 the terms of the GNU General Public License as published by the Free Software
1.12 @@ -187,6 +187,10 @@
1.13 l = []
1.14
1.15 for qualifier, args in qualifiers:
1.16 +
1.17 + # Distinguish between enumerators, used to select particular periods,
1.18 + # and frequencies, used to select repeating periods.
1.19 +
1.20 if enum.has_key(qualifier):
1.21 level = enum[qualifier]
1.22 if special_enum_levels.has_key(qualifier):
1.23 @@ -209,10 +213,17 @@
1.24
1.25 l = []
1.26 offset = 0
1.27 +
1.28 for level, value in enumerate(datetime):
1.29 +
1.30 + # At the day number, adjust the frequency level offset to reference
1.31 + # days (and then hours, minutes, seconds).
1.32 +
1.33 if level == 2:
1.34 offset = 3
1.35 +
1.36 l.append(Enum(level + offset, {"values" : [value]}, "DT"))
1.37 +
1.38 return l
1.39
1.40 def combine_datetime_with_qualifiers(datetime, qualifiers):
1.41 @@ -231,6 +242,10 @@
1.42 from_q = get_next(iter_q)
1.43
1.44 have_q = False
1.45 +
1.46 + # The initial context for any qualifiers is taken from the first datetime
1.47 + # value, which should be the year.
1.48 +
1.49 context = []
1.50 context.append(from_dt.args["values"][0])
1.51
1.52 @@ -240,7 +255,8 @@
1.53 _level = from_dt.level
1.54 level = from_q.level
1.55
1.56 - # Datetime value at wider resolution.
1.57 + # Datetime value at wider resolution. Use the datetime value to expand
1.58 + # the context within which qualifiers will operate.
1.59
1.60 if _level < level:
1.61 from_dt = get_next(iter_dt)
1.62 @@ -249,41 +265,69 @@
1.63 # Qualifier at wider or same resolution as datetime value.
1.64
1.65 else:
1.66 + # Without any previous qualifier, introduce a special qualifier to
1.67 + # provide context for this qualifier.
1.68 +
1.69 if not have_q:
1.70 - if isinstance(from_q, Enum) and level > 0:
1.71 - repeat = Pattern(level - 1, {"interval" : 1}, None)
1.72 - repeat.context = tuple(context)
1.73 - l.append(repeat)
1.74 + add_initial_qualifier(from_q, level, context, l)
1.75 have_q = True
1.76
1.77 + # Associate the datetime context with the qualifier and add it to
1.78 + # the combined list.
1.79 +
1.80 from_q.context = tuple(context)
1.81 l.append(from_q)
1.82 - from_q = get_next(iter_q)
1.83 +
1.84 + # Datetime value at same resolution. Expand the context using the
1.85 + # value.
1.86
1.87 if _level == level:
1.88 - context.append(from_dt.args["values"][0])
1.89 from_dt = get_next(iter_dt)
1.90 + if from_dt:
1.91 + context.append(from_dt.args["values"][0])
1.92
1.93 - # Complete the list.
1.94 + # Get the next qualifier.
1.95 +
1.96 + from_q = get_next(iter_q)
1.97 +
1.98 + # Complete the list by adding remaining datetime enumerators.
1.99
1.100 while from_dt:
1.101 l.append(from_dt)
1.102 from_dt = get_next(iter_dt)
1.103
1.104 + # Complete the list by adding remaining qualifiers.
1.105 +
1.106 while from_q:
1.107 if not have_q:
1.108 - if isinstance(from_q, Enum) and level > 0:
1.109 - repeat = Pattern(level - 1, {"interval" : 1}, None)
1.110 - repeat.context = tuple(context)
1.111 - l.append(repeat)
1.112 + add_initial_qualifier(from_q, level, context, l)
1.113 have_q = True
1.114
1.115 + # Associate the datetime context with the qualifier and add it to the
1.116 + # combined list.
1.117 +
1.118 from_q.context = tuple(context)
1.119 l.append(from_q)
1.120 +
1.121 + # Get the next qualifier.
1.122 +
1.123 from_q = get_next(iter_q)
1.124
1.125 return l
1.126
1.127 +def add_initial_qualifier(from_q, level, context, l):
1.128 +
1.129 + """
1.130 + Take the first qualifier 'from_q' at the given resolution 'level', using it
1.131 + to create an initial qualifier providing appropriate context, using the
1.132 + given 'context', adding it to the combined list 'l' if required.
1.133 + """
1.134 +
1.135 + if isinstance(from_q, Enum) and level > 0:
1.136 + repeat = Pattern(level - 1, {"interval" : 1}, None)
1.137 + repeat.context = tuple(context)
1.138 + l.append(repeat)
1.139 +
1.140 # Datetime arithmetic.
1.141
1.142 def combine(t1, t2):
1.143 @@ -399,11 +443,17 @@
1.144 """
1.145
1.146 self.level = level
1.147 - self.pos = positions[level]
1.148 self.args = args
1.149 self.qualifier = qualifier
1.150 + self.selecting = selecting
1.151 +
1.152 + # Define an empty context to be overridden.
1.153 +
1.154 self.context = ()
1.155 - self.selecting = selecting
1.156 +
1.157 + # Define the index of values from datetimes involved with this selector.
1.158 +
1.159 + self.pos = positions[level]
1.160
1.161 def __repr__(self):
1.162 return "%s(%r, %r, %r, %r)" % (self.__class__.__name__, self.level, self.args, self.qualifier, self.context)
1.163 @@ -483,31 +533,70 @@
1.164
1.165 class Pattern(Selector):
1.166
1.167 - "A selector of instances according to a repeating pattern."
1.168 + "A selector of time periods according to a repeating pattern."
1.169
1.170 def materialise_items(self, context, start, end, counter, setpos=None, inclusive=False):
1.171 +
1.172 + """
1.173 + Bounded by the given 'context', return periods within 'start' to 'end',
1.174 + updating the 'counter', selecting only the indexes specified by 'setpos'
1.175 + (if given).
1.176 +
1.177 + If 'inclusive' is specified, the selection of periods will include those
1.178 + starting at the end of the search period, if present in the results.
1.179 + """
1.180 +
1.181 + # Obtain the pattern context's value at the appropriate level.
1.182 +
1.183 first = scale(self.context[self.pos], self.pos)
1.184
1.185 - # Define the step between items.
1.186 + # Define the step between result periods.
1.187
1.188 interval = self.args.get("interval", 1) * units.get(self.qualifier, 1)
1.189 step = scale(interval, self.pos)
1.190
1.191 - # Define the scale of a single item.
1.192 + # Define the scale of a single period.
1.193
1.194 unit_interval = units.get(self.qualifier, 1)
1.195 unit_step = scale(unit_interval, self.pos)
1.196
1.197 + # Combine specific context details with the pattern context. This should
1.198 + # make the result more specific than the pattern context.
1.199 +
1.200 current = combine(context, first)
1.201 results = []
1.202
1.203 - while (inclusive and current <= end or current < end) and (counter is None or counter[0] < counter[1]):
1.204 + # Obtain periods before the end (and also at the end if inclusive),
1.205 + # provided that any limit imposed by the counter has not been exceeded.
1.206 +
1.207 + while (inclusive and current <= end or current < end) and \
1.208 + (counter is None or counter[0] < counter[1]):
1.209 +
1.210 + # Increment the current datetime by the step for the next period.
1.211 +
1.212 next = update(current, step)
1.213 +
1.214 + # Determine the end point of the current period.
1.215 +
1.216 current_end = update(current, unit_step)
1.217 +
1.218 + # Obtain any period or periods within the bounds defined by the
1.219 + # current period and any contraining start and end points, plus
1.220 + # counter, setpos and inclusive details.
1.221 +
1.222 interval_results = self.materialise_item(current, max(current, start), min(current_end, end), counter, setpos, inclusive)
1.223 +
1.224 + # Update the counter with the number of identified results.
1.225 +
1.226 if counter is not None:
1.227 counter[0] += len(interval_results)
1.228 +
1.229 + # Accumulate the results.
1.230 +
1.231 results += interval_results
1.232 +
1.233 + # Visit the next instance.
1.234 +
1.235 current = next
1.236
1.237 return results