1.1 --- a/vRecurrence.py Mon Jun 05 18:42:20 2017 +0200
1.2 +++ b/vRecurrence.py Tue Jun 06 00:02:05 2017 +0200
1.3 @@ -84,11 +84,20 @@
1.4 "BYSECOND"
1.5 )
1.6
1.7 +# Levels defining days.
1.8 +
1.9 +daylevels = [2, 3, 4, 5]
1.10 +
1.11 # Map from levels to lengths of datetime tuples.
1.12
1.13 lengths = [1, 2, 3, 3, 3, 3, 4, 5, 6]
1.14 positions = [l-1 for l in lengths]
1.15
1.16 +# Define the lowest values at each resolution (years, months, days... hours,
1.17 +# minutes, seconds).
1.18 +
1.19 +firstvalues = [0, 1, 1, 1, 1, 1, 0, 0, 0]
1.20 +
1.21 # Map from qualifiers to interval units. Here, weeks are defined as 7 days.
1.22
1.23 units = {"WEEKLY" : 7}
1.24 @@ -245,6 +254,22 @@
1.25 """
1.26 Combine 'datetime' with 'qualifiers' to produce a structure for recurrence
1.27 production.
1.28 +
1.29 + Initial datetime values appearing at broader resolutions than any qualifiers
1.30 + are ignored, since their details will be used when materialising the
1.31 + results.
1.32 +
1.33 + Qualifiers are accumulated in order to define a selection. Datetime values
1.34 + occurring between qualifiers or at the same resolution as qualifiers are
1.35 + ignored.
1.36 +
1.37 + Any remaining datetime values are introduced as enumerators, provided that
1.38 + they do not conflict with qualifiers. For example, specific day values
1.39 + conflict with day selectors and weekly qualifiers.
1.40 +
1.41 + The purpose of the remaining datetime values is to define a result within
1.42 + a period selected by the most precise qualifier. For example, the selection
1.43 + of a day and month in a year recurrence.
1.44 """
1.45
1.46 iter_dt = iter(get_datetime_structure(datetime))
1.47 @@ -254,51 +279,34 @@
1.48
1.49 from_dt = get_next(iter_dt)
1.50 from_q = get_next(iter_q)
1.51 -
1.52 have_q = False
1.53
1.54 - # The initial context for any qualifiers is taken from the first datetime
1.55 - # value, which should be the year.
1.56 -
1.57 - context = []
1.58 - context.append(from_dt.args["values"][0])
1.59 -
1.60 # Consume from both lists, merging entries.
1.61
1.62 while from_dt and from_q:
1.63 _level = from_dt.level
1.64 level = from_q.level
1.65
1.66 - # Datetime value at wider resolution. Use the datetime value to expand
1.67 - # the context within which qualifiers will operate.
1.68 + # Datetime value at wider resolution.
1.69
1.70 if _level < level:
1.71 from_dt = get_next(iter_dt)
1.72 - context.append(from_dt.args["values"][0])
1.73
1.74 # Qualifier at wider or same resolution as datetime value.
1.75
1.76 else:
1.77 - # Without any previous qualifier, introduce a special qualifier to
1.78 - # provide context for this qualifier.
1.79 -
1.80 if not have_q:
1.81 - add_initial_qualifier(from_q, level, context, l)
1.82 + add_initial_qualifier(from_q, level, l)
1.83 have_q = True
1.84
1.85 - # Associate the datetime context with the qualifier and add it to
1.86 - # the combined list.
1.87 + # Add the qualifier to the combined list.
1.88
1.89 - from_q.context = tuple(context)
1.90 l.append(from_q)
1.91
1.92 - # Datetime value at same resolution. Expand the context using the
1.93 - # value.
1.94 + # Datetime value at same resolution.
1.95
1.96 if _level == level:
1.97 from_dt = get_next(iter_dt)
1.98 - if from_dt:
1.99 - context.append(from_dt.args["values"][0])
1.100
1.101 # Get the next qualifier.
1.102
1.103 @@ -307,20 +315,23 @@
1.104 # Complete the list by adding remaining datetime enumerators.
1.105
1.106 while from_dt:
1.107 - l.append(from_dt)
1.108 +
1.109 + # Ignore datetime values that conflict with day-level qualifiers.
1.110 +
1.111 + if not l or from_dt.level != freq["DAILY"] or l[-1].level not in daylevels:
1.112 + l.append(from_dt)
1.113 +
1.114 from_dt = get_next(iter_dt)
1.115
1.116 # Complete the list by adding remaining qualifiers.
1.117
1.118 while from_q:
1.119 if not have_q:
1.120 - add_initial_qualifier(from_q, level, context, l)
1.121 + add_initial_qualifier(from_q, level, l)
1.122 have_q = True
1.123
1.124 - # Associate the datetime context with the qualifier and add it to the
1.125 - # combined list.
1.126 + # Add the qualifier to the combined list.
1.127
1.128 - from_q.context = tuple(context)
1.129 l.append(from_q)
1.130
1.131 # Get the next qualifier.
1.132 @@ -329,17 +340,16 @@
1.133
1.134 return l
1.135
1.136 -def add_initial_qualifier(from_q, level, context, l):
1.137 +def add_initial_qualifier(from_q, level, l):
1.138
1.139 """
1.140 Take the first qualifier 'from_q' at the given resolution 'level', using it
1.141 - to create an initial qualifier providing appropriate context, using the
1.142 - given 'context', adding it to the combined list 'l' if required.
1.143 + to create an initial qualifier, adding it to the combined list 'l' if
1.144 + required.
1.145 """
1.146
1.147 if isinstance(from_q, Enum) and level > 0:
1.148 repeat = Pattern(level - 1, {"interval" : 1}, None)
1.149 - repeat.context = tuple(context[:level])
1.150 l.append(repeat)
1.151
1.152 # Datetime arithmetic.
1.153 @@ -446,7 +456,7 @@
1.154
1.155 "A generic selector."
1.156
1.157 - def __init__(self, level, args, qualifier, selecting=None):
1.158 + def __init__(self, level, args, qualifier, selecting=None, first=False):
1.159
1.160 """
1.161 Initialise at the given 'level' a selector employing the given 'args'
1.162 @@ -460,17 +470,14 @@
1.163 self.args = args
1.164 self.qualifier = qualifier
1.165 self.selecting = selecting
1.166 -
1.167 - # Define an empty context to be overridden.
1.168 -
1.169 - self.context = ()
1.170 + self.first = first
1.171
1.172 # Define the index of values from datetimes involved with this selector.
1.173
1.174 self.pos = positions[level]
1.175
1.176 def __repr__(self):
1.177 - return "%s(%r, %r, %r, %r)" % (self.__class__.__name__, self.level, self.args, self.qualifier, self.context)
1.178 + return "%s(%r, %r, %r, %r)" % (self.__class__.__name__, self.level, self.args, self.qualifier, self.first)
1.179
1.180 def materialise(self, start, end, count=None, setpos=None, inclusive=False):
1.181
1.182 @@ -487,7 +494,7 @@
1.183 start = to_tuple(start)
1.184 end = to_tuple(end)
1.185 counter = count and [0, count]
1.186 - results = self.materialise_items(self.context, start, end, counter, setpos, inclusive)
1.187 + results = self.materialise_items(start, start, end, counter, setpos, inclusive)
1.188 results.sort()
1.189 return results[:count]
1.190
1.191 @@ -560,10 +567,6 @@
1.192 starting at the end of the search period, if present in the results.
1.193 """
1.194
1.195 - # Obtain the pattern context's value at the appropriate level.
1.196 -
1.197 - first = scale(self.context[self.pos], self.pos)
1.198 -
1.199 # Define the step between result periods.
1.200
1.201 interval = self.args.get("interval", 1) * units.get(self.qualifier, 1)
1.202 @@ -574,12 +577,19 @@
1.203 unit_interval = units.get(self.qualifier, 1)
1.204 unit_step = scale(unit_interval, self.pos)
1.205
1.206 - # Combine supplied context details with the pattern context. This should
1.207 - # provide additional resolution information that may be missing from the
1.208 - # supplied context. For example, the outer selector may indicate a month
1.209 - # context, but this selector may need day information.
1.210 + # Employ the context as the current period if this is the first
1.211 + # qualifier in the selection chain.
1.212 +
1.213 + if self.first:
1.214 + current = context[:self.pos+1]
1.215
1.216 - current = combine(context, first)
1.217 + # Otherwise, obtain the first value at this resolution within the
1.218 + # context period.
1.219 +
1.220 + else:
1.221 + first = scale(firstvalues[self.level], self.pos)
1.222 + current = combine(context[:self.pos], first)
1.223 +
1.224 results = []
1.225
1.226 # Obtain periods before the end (and also at the end if inclusive),
1.227 @@ -702,7 +712,7 @@
1.228 step = scale(1, self.pos)
1.229 results = []
1.230 for value in self.args["values"]:
1.231 - current = combine(context, scale(value, self.pos))
1.232 + current = combine(context[:self.pos], scale(value, self.pos))
1.233 next = update(current, step)
1.234
1.235 # To support setpos, only current and next bound the search, not
1.236 @@ -782,6 +792,7 @@
1.237 """
1.238
1.239 current = selectors[0]
1.240 + current.first = True
1.241 for selector in selectors[1:]:
1.242 current.selecting = selector
1.243 current = selector