1.1 --- a/tests/internal/qualifiers.py Mon Jun 05 18:42:20 2017 +0200
1.2 +++ b/tests/internal/qualifiers.py Tue Jun 06 00:02:05 2017 +0200
1.3 @@ -3,7 +3,7 @@
1.4 """
1.5 Test qualifiers for recurring events.
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 @@ -772,4 +772,36 @@
1.13 print l[-1] == (1998, 3, 30, 9, 0, 0), (1998, 3, 30, 9, 0, 0), l[-1]
1.14 print
1.15
1.16 +qualifiers = get_qualifiers(["FREQ=MONTHLY", "BYMONTHDAY=5", "FREQ=HOURLY", "INTERVAL=12"])
1.17 +
1.18 +l = order_qualifiers(qualifiers)
1.19 +show(l)
1.20 +dt = (2017, 6, 13)
1.21 +l = get_datetime_structure(dt)
1.22 +show(l)
1.23 +l = combine_datetime_with_qualifiers(dt, qualifiers)
1.24 +show(l)
1.25 +
1.26 +s = get_selector(dt, qualifiers)
1.27 +l = s.materialise(dt, (2019, 1, 1))
1.28 +print len(l) == 36
1.29 +print l[0] == (2017, 7, 5, 0), (2017, 7, 5, 0), l[0]
1.30 +print l[-1] == (2018, 12, 5, 12), (2018, 12, 5, 12), l[-1]
1.31 +
1.32 +qualifiers = get_qualifiers(["FREQ=DAILY", "BYMONTH=1"])
1.33 +
1.34 +l = order_qualifiers(qualifiers)
1.35 +show(l)
1.36 +dt = (2017, 6, 13)
1.37 +l = get_datetime_structure(dt)
1.38 +show(l)
1.39 +l = combine_datetime_with_qualifiers(dt, qualifiers)
1.40 +show(l)
1.41 +
1.42 +s = get_selector(dt, qualifiers)
1.43 +l = s.materialise(dt, (2019, 1, 1))
1.44 +print len(l) == 31
1.45 +print l[0] == (2018, 1, 1), (2018, 1, 1), l[0]
1.46 +print l[-1] == (2018, 1, 31), (2018, 1, 31), l[-1]
1.47 +
1.48 # vim: tabstop=4 expandtab shiftwidth=4
2.1 --- a/vRecurrence.py Mon Jun 05 18:42:20 2017 +0200
2.2 +++ b/vRecurrence.py Tue Jun 06 00:02:05 2017 +0200
2.3 @@ -84,11 +84,20 @@
2.4 "BYSECOND"
2.5 )
2.6
2.7 +# Levels defining days.
2.8 +
2.9 +daylevels = [2, 3, 4, 5]
2.10 +
2.11 # Map from levels to lengths of datetime tuples.
2.12
2.13 lengths = [1, 2, 3, 3, 3, 3, 4, 5, 6]
2.14 positions = [l-1 for l in lengths]
2.15
2.16 +# Define the lowest values at each resolution (years, months, days... hours,
2.17 +# minutes, seconds).
2.18 +
2.19 +firstvalues = [0, 1, 1, 1, 1, 1, 0, 0, 0]
2.20 +
2.21 # Map from qualifiers to interval units. Here, weeks are defined as 7 days.
2.22
2.23 units = {"WEEKLY" : 7}
2.24 @@ -245,6 +254,22 @@
2.25 """
2.26 Combine 'datetime' with 'qualifiers' to produce a structure for recurrence
2.27 production.
2.28 +
2.29 + Initial datetime values appearing at broader resolutions than any qualifiers
2.30 + are ignored, since their details will be used when materialising the
2.31 + results.
2.32 +
2.33 + Qualifiers are accumulated in order to define a selection. Datetime values
2.34 + occurring between qualifiers or at the same resolution as qualifiers are
2.35 + ignored.
2.36 +
2.37 + Any remaining datetime values are introduced as enumerators, provided that
2.38 + they do not conflict with qualifiers. For example, specific day values
2.39 + conflict with day selectors and weekly qualifiers.
2.40 +
2.41 + The purpose of the remaining datetime values is to define a result within
2.42 + a period selected by the most precise qualifier. For example, the selection
2.43 + of a day and month in a year recurrence.
2.44 """
2.45
2.46 iter_dt = iter(get_datetime_structure(datetime))
2.47 @@ -254,51 +279,34 @@
2.48
2.49 from_dt = get_next(iter_dt)
2.50 from_q = get_next(iter_q)
2.51 -
2.52 have_q = False
2.53
2.54 - # The initial context for any qualifiers is taken from the first datetime
2.55 - # value, which should be the year.
2.56 -
2.57 - context = []
2.58 - context.append(from_dt.args["values"][0])
2.59 -
2.60 # Consume from both lists, merging entries.
2.61
2.62 while from_dt and from_q:
2.63 _level = from_dt.level
2.64 level = from_q.level
2.65
2.66 - # Datetime value at wider resolution. Use the datetime value to expand
2.67 - # the context within which qualifiers will operate.
2.68 + # Datetime value at wider resolution.
2.69
2.70 if _level < level:
2.71 from_dt = get_next(iter_dt)
2.72 - context.append(from_dt.args["values"][0])
2.73
2.74 # Qualifier at wider or same resolution as datetime value.
2.75
2.76 else:
2.77 - # Without any previous qualifier, introduce a special qualifier to
2.78 - # provide context for this qualifier.
2.79 -
2.80 if not have_q:
2.81 - add_initial_qualifier(from_q, level, context, l)
2.82 + add_initial_qualifier(from_q, level, l)
2.83 have_q = True
2.84
2.85 - # Associate the datetime context with the qualifier and add it to
2.86 - # the combined list.
2.87 + # Add the qualifier to the combined list.
2.88
2.89 - from_q.context = tuple(context)
2.90 l.append(from_q)
2.91
2.92 - # Datetime value at same resolution. Expand the context using the
2.93 - # value.
2.94 + # Datetime value at same resolution.
2.95
2.96 if _level == level:
2.97 from_dt = get_next(iter_dt)
2.98 - if from_dt:
2.99 - context.append(from_dt.args["values"][0])
2.100
2.101 # Get the next qualifier.
2.102
2.103 @@ -307,20 +315,23 @@
2.104 # Complete the list by adding remaining datetime enumerators.
2.105
2.106 while from_dt:
2.107 - l.append(from_dt)
2.108 +
2.109 + # Ignore datetime values that conflict with day-level qualifiers.
2.110 +
2.111 + if not l or from_dt.level != freq["DAILY"] or l[-1].level not in daylevels:
2.112 + l.append(from_dt)
2.113 +
2.114 from_dt = get_next(iter_dt)
2.115
2.116 # Complete the list by adding remaining qualifiers.
2.117
2.118 while from_q:
2.119 if not have_q:
2.120 - add_initial_qualifier(from_q, level, context, l)
2.121 + add_initial_qualifier(from_q, level, l)
2.122 have_q = True
2.123
2.124 - # Associate the datetime context with the qualifier and add it to the
2.125 - # combined list.
2.126 + # Add the qualifier to the combined list.
2.127
2.128 - from_q.context = tuple(context)
2.129 l.append(from_q)
2.130
2.131 # Get the next qualifier.
2.132 @@ -329,17 +340,16 @@
2.133
2.134 return l
2.135
2.136 -def add_initial_qualifier(from_q, level, context, l):
2.137 +def add_initial_qualifier(from_q, level, l):
2.138
2.139 """
2.140 Take the first qualifier 'from_q' at the given resolution 'level', using it
2.141 - to create an initial qualifier providing appropriate context, using the
2.142 - given 'context', adding it to the combined list 'l' if required.
2.143 + to create an initial qualifier, adding it to the combined list 'l' if
2.144 + required.
2.145 """
2.146
2.147 if isinstance(from_q, Enum) and level > 0:
2.148 repeat = Pattern(level - 1, {"interval" : 1}, None)
2.149 - repeat.context = tuple(context[:level])
2.150 l.append(repeat)
2.151
2.152 # Datetime arithmetic.
2.153 @@ -446,7 +456,7 @@
2.154
2.155 "A generic selector."
2.156
2.157 - def __init__(self, level, args, qualifier, selecting=None):
2.158 + def __init__(self, level, args, qualifier, selecting=None, first=False):
2.159
2.160 """
2.161 Initialise at the given 'level' a selector employing the given 'args'
2.162 @@ -460,17 +470,14 @@
2.163 self.args = args
2.164 self.qualifier = qualifier
2.165 self.selecting = selecting
2.166 -
2.167 - # Define an empty context to be overridden.
2.168 -
2.169 - self.context = ()
2.170 + self.first = first
2.171
2.172 # Define the index of values from datetimes involved with this selector.
2.173
2.174 self.pos = positions[level]
2.175
2.176 def __repr__(self):
2.177 - return "%s(%r, %r, %r, %r)" % (self.__class__.__name__, self.level, self.args, self.qualifier, self.context)
2.178 + return "%s(%r, %r, %r, %r)" % (self.__class__.__name__, self.level, self.args, self.qualifier, self.first)
2.179
2.180 def materialise(self, start, end, count=None, setpos=None, inclusive=False):
2.181
2.182 @@ -487,7 +494,7 @@
2.183 start = to_tuple(start)
2.184 end = to_tuple(end)
2.185 counter = count and [0, count]
2.186 - results = self.materialise_items(self.context, start, end, counter, setpos, inclusive)
2.187 + results = self.materialise_items(start, start, end, counter, setpos, inclusive)
2.188 results.sort()
2.189 return results[:count]
2.190
2.191 @@ -560,10 +567,6 @@
2.192 starting at the end of the search period, if present in the results.
2.193 """
2.194
2.195 - # Obtain the pattern context's value at the appropriate level.
2.196 -
2.197 - first = scale(self.context[self.pos], self.pos)
2.198 -
2.199 # Define the step between result periods.
2.200
2.201 interval = self.args.get("interval", 1) * units.get(self.qualifier, 1)
2.202 @@ -574,12 +577,19 @@
2.203 unit_interval = units.get(self.qualifier, 1)
2.204 unit_step = scale(unit_interval, self.pos)
2.205
2.206 - # Combine supplied context details with the pattern context. This should
2.207 - # provide additional resolution information that may be missing from the
2.208 - # supplied context. For example, the outer selector may indicate a month
2.209 - # context, but this selector may need day information.
2.210 + # Employ the context as the current period if this is the first
2.211 + # qualifier in the selection chain.
2.212 +
2.213 + if self.first:
2.214 + current = context[:self.pos+1]
2.215
2.216 - current = combine(context, first)
2.217 + # Otherwise, obtain the first value at this resolution within the
2.218 + # context period.
2.219 +
2.220 + else:
2.221 + first = scale(firstvalues[self.level], self.pos)
2.222 + current = combine(context[:self.pos], first)
2.223 +
2.224 results = []
2.225
2.226 # Obtain periods before the end (and also at the end if inclusive),
2.227 @@ -702,7 +712,7 @@
2.228 step = scale(1, self.pos)
2.229 results = []
2.230 for value in self.args["values"]:
2.231 - current = combine(context, scale(value, self.pos))
2.232 + current = combine(context[:self.pos], scale(value, self.pos))
2.233 next = update(current, step)
2.234
2.235 # To support setpos, only current and next bound the search, not
2.236 @@ -782,6 +792,7 @@
2.237 """
2.238
2.239 current = selectors[0]
2.240 + current.first = True
2.241 for selector in selectors[1:]:
2.242 current.selecting = selector
2.243 current = selector