1.1 --- a/vRecurrence.py Fri Oct 20 23:19:56 2017 +0200
1.2 +++ b/vRecurrence.py Sun Oct 22 01:24:08 2017 +0200
1.3 @@ -70,6 +70,10 @@
1.4 "SECONDLY"
1.5 )
1.6
1.7 +# Symbols corresponding to resolution levels.
1.8 +
1.9 +YEARS, MONTHS, WEEKS, DAYS, HOURS, MINUTES, SECONDS = 0, 1, 2, 5, 6, 7, 8
1.10 +
1.11 # Enumeration levels, employed by BY... qualifiers.
1.12
1.13 enum_levels = (
1.14 @@ -98,19 +102,34 @@
1.15
1.16 firstvalues = [0, 1, 1, 1, 1, 1, 0, 0, 0]
1.17
1.18 -# Map from qualifiers to interval units. Here, weeks are defined as 7 days.
1.19 +# Map from qualifiers to interval multiples. Here, weeks are defined as 7 days.
1.20
1.21 -units = {"WEEKLY" : 7}
1.22 +multiples = {"WEEKLY" : 7}
1.23
1.24 # Make dictionaries mapping qualifiers to levels.
1.25
1.26 -freq = dict([(level, i) for (i, level) in enumerate(freq_levels) if level])
1.27 -enum = dict([(level, i) for (i, level) in enumerate(enum_levels) if level])
1.28 -weekdays = dict([(weekday, i+1) for (i, weekday) in enumerate(["MO", "TU", "WE", "TH", "FR", "SA", "SU"])])
1.29 +freq = {}
1.30 +for i, level in enumerate(freq_levels):
1.31 + if level:
1.32 + freq[level] = i
1.33 +
1.34 +enum = {}
1.35 +for i, level in enumerate(enum_levels):
1.36 + if level:
1.37 + enum[level] = i
1.38 +
1.39 +# Weekdays: name -> 1-based value
1.40 +
1.41 +weekdays = {}
1.42 +for i, weekday in enumerate(["MO", "TU", "WE", "TH", "FR", "SA", "SU"]):
1.43 + weekdays[weekday] = i + 1
1.44
1.45 # Functions for structuring the recurrences.
1.46
1.47 def get_next(it):
1.48 +
1.49 + "Return the next value from iterator 'it' or None if no more values exist."
1.50 +
1.51 try:
1.52 return it.next()
1.53 except StopIteration:
1.54 @@ -186,10 +205,15 @@
1.55 suitable values.
1.56 """
1.57
1.58 + # For non-weekday selection, obtain a list of day numbers.
1.59 +
1.60 if qualifier != "BYDAY":
1.61 return map(int, value.split(","))
1.62
1.63 + # For weekday selection, obtain the weekday number and instance number.
1.64 +
1.65 values = []
1.66 +
1.67 for part in value.split(","):
1.68 weekday = weekdays.get(part[-2:])
1.69 if not weekday:
1.70 @@ -237,15 +261,15 @@
1.71 l = []
1.72 offset = 0
1.73
1.74 - for level, value in enumerate(datetime):
1.75 + for pos, value in enumerate(datetime):
1.76
1.77 # At the day number, adjust the frequency level offset to reference
1.78 # days (and then hours, minutes, seconds).
1.79
1.80 - if level == 2:
1.81 + if pos == 2:
1.82 offset = 3
1.83
1.84 - l.append(Enum(level + offset, {"values" : [value]}, "DT"))
1.85 + l.append(Enum(pos + offset, {"values" : [value]}, "DT"))
1.86
1.87 return l
1.88
1.89 @@ -318,7 +342,9 @@
1.90
1.91 # Ignore datetime values that conflict with day-level qualifiers.
1.92
1.93 - if not l or from_dt.level != freq["DAILY"] or l[-1].level not in daylevels:
1.94 + if not l or from_dt.level != freq["DAILY"] or \
1.95 + l[-1].level not in daylevels:
1.96 +
1.97 l.append(from_dt)
1.98
1.99 from_dt = get_next(iter_dt)
1.100 @@ -352,24 +378,120 @@
1.101 repeat = Pattern(level - 1, {"interval" : 1}, None)
1.102 l.append(repeat)
1.103
1.104 +def get_multiple(qualifier):
1.105 +
1.106 + "Return the time unit multiple for 'qualifier'."
1.107 +
1.108 + return multiples.get(qualifier, 1)
1.109 +
1.110 # Datetime arithmetic.
1.111
1.112 -def combine(t1, t2):
1.113 +def is_year_only(t):
1.114 +
1.115 + "Return if 't' describes a year."
1.116 +
1.117 + return len(t) == lengths[YEARS]
1.118 +
1.119 +def is_month_only(t):
1.120 +
1.121 + "Return if 't' describes a month within a year."
1.122 +
1.123 + return len(t) == lengths[MONTHS]
1.124 +
1.125 +def start_of_year(t):
1.126 +
1.127 + "Return the start of the year referenced by 't'."
1.128 +
1.129 + return (t[0], 1, 1)
1.130 +
1.131 +def end_of_year(t):
1.132 +
1.133 + "Return the end of the year referenced by 't'."
1.134 +
1.135 + return (t[0], 12, 31)
1.136 +
1.137 +def start_of_month(t):
1.138 +
1.139 + "Return the start of the month referenced by 't'."
1.140 +
1.141 + year, month = t[:2]
1.142 + return (year, month, 1)
1.143 +
1.144 +def end_of_month(t):
1.145 +
1.146 + "Return the end of the month referenced by 't'."
1.147 +
1.148 + year, month = t[:2]
1.149 + return update(update((year, month, 1), (0, 1, 0)), (0, 0, -1))
1.150 +
1.151 +def day_in_year(t, number):
1.152 +
1.153 + "Return the day in the year referenced by 't' with the given 'number'."
1.154 +
1.155 + return to_tuple(date(*start_of_year(t)) + timedelta(number - 1))
1.156 +
1.157 +def get_year_length(t):
1.158 +
1.159 + "Return the length of the year referenced by 't'."
1.160 +
1.161 + first_day = date(*start_of_year(t))
1.162 + last_day = date(*end_of_year(t))
1.163 + return (last_day - first_day).days + 1
1.164 +
1.165 +def get_weekday(t):
1.166 +
1.167 + "Return the 1-based weekday for the month referenced by 't'."
1.168 +
1.169 + year, month = t[:2]
1.170 + return monthrange(year, month)[0] + 1
1.171 +
1.172 +def get_ordered_weekdays(t):
1.173
1.174 """
1.175 - Combine tuples 't1' and 't2', returning a copy of 't1' filled with values
1.176 - from 't2' in positions where 0 appeared in 't1'.
1.177 + Return the 1-based weekday sequence describing the first week of the month
1.178 + referenced by 't'.
1.179 """
1.180
1.181 - return tuple(map(lambda x, y: x or y, t1, t2))
1.182 + first = get_weekday(t)
1.183 + return range(first, 8) + range(1, first)
1.184 +
1.185 +def get_last_weekday_instance(weekday, first_day, last_day):
1.186 +
1.187 + """
1.188 + Return the last instance number for 'weekday' within the period from
1.189 + 'first_day' to 'last_day' inclusive.
1.190
1.191 -def scale(interval, pos):
1.192 + Here, 'weekday' is 1-based (starting on Monday), the returned limit is
1.193 + 1-based.
1.194 + """
1.195 +
1.196 + weekday0 = get_first_day(first_day, weekday)
1.197 + days = (date(*last_day) - weekday0).days
1.198 + return days / 7 + 1
1.199 +
1.200 +def precision(t, level, value=None):
1.201
1.202 """
1.203 - Scale the given 'interval' value to the indicated position 'pos', returning
1.204 - a tuple with leading zero elements and 'interval' at the stated position.
1.205 + Return 't' trimmed in resolution to the indicated resolution 'level',
1.206 + setting 'value' at the given resolution if indicated.
1.207 """
1.208
1.209 + pos = positions[level]
1.210 +
1.211 + if value is None:
1.212 + return t[:pos + 1]
1.213 + else:
1.214 + return t[:pos] + (value,)
1.215 +
1.216 +def scale(interval, level):
1.217 +
1.218 + """
1.219 + Scale the given 'interval' value in resolution to the indicated resolution
1.220 + 'level', returning a tuple with leading zero elements and 'interval' at the
1.221 + stated position.
1.222 + """
1.223 +
1.224 + pos = positions[level]
1.225 return (0,) * pos + (interval,)
1.226
1.227 def get_seconds(t):
1.228 @@ -413,24 +535,26 @@
1.229 d = datetime(*updated_for_months)
1.230 s = timedelta(step[2], get_seconds(step))
1.231
1.232 - return to_tuple(d + s, len(t))
1.233 + return to_tuple(d + s)[:len(t)]
1.234
1.235 -def to_tuple(d, n=None):
1.236 +def to_tuple(d):
1.237
1.238 - "Return 'd' as a tuple, optionally trimming the result to 'n' positions."
1.239 + "Return date or datetime 'd' as a tuple."
1.240
1.241 if not isinstance(d, date):
1.242 return d
1.243 - if n is None:
1.244 - if isinstance(d, datetime):
1.245 - n = 6
1.246 - else:
1.247 - n = 3
1.248 + if isinstance(d, datetime):
1.249 + n = 6
1.250 + else:
1.251 + n = 3
1.252 return d.timetuple()[:n]
1.253
1.254 def get_first_day(first_day, weekday):
1.255
1.256 - "Return the first occurrence at or after 'first_day' of 'weekday'."
1.257 + """
1.258 + Return the first occurrence at or after 'first_day' of 'weekday' as a date
1.259 + instance.
1.260 + """
1.261
1.262 first_day = date(*first_day)
1.263 first_weekday = first_day.isoweekday()
1.264 @@ -441,7 +565,10 @@
1.265
1.266 def get_last_day(last_day, weekday):
1.267
1.268 - "Return the last occurrence at or before 'last_day' of 'weekday'."
1.269 + """
1.270 + Return the last occurrence at or before 'last_day' of 'weekday' as a date
1.271 + instance.
1.272 + """
1.273
1.274 last_day = date(*last_day)
1.275 last_weekday = last_day.isoweekday()
1.276 @@ -450,6 +577,74 @@
1.277 else:
1.278 return last_day - timedelta(last_weekday - weekday)
1.279
1.280 +# Value expansion and sorting.
1.281 +
1.282 +def sort_values(values, limit=None):
1.283 +
1.284 + """
1.285 + Sort the given 'values' using 'limit' to determine the positions of negative
1.286 + values.
1.287 + """
1.288 +
1.289 + # Convert negative values to positive values according to the value limit.
1.290 +
1.291 + if limit is not None:
1.292 + l = map(lambda x, limit=limit: x < 0 and x + 1 + limit or x, values)
1.293 + else:
1.294 + l = values[:]
1.295 +
1.296 + l.sort()
1.297 + return l
1.298 +
1.299 +def compare_weekday_selectors(x, y, weekdays):
1.300 +
1.301 + """
1.302 + Compare 'x' and 'y' values of the form (weekday number, instance number)
1.303 + using 'weekdays' to define an ordering of the weekday numbers.
1.304 + """
1.305 +
1.306 + return cmp((x[1], weekdays.index(x[0])), (y[1], weekdays.index(y[0])))
1.307 +
1.308 +def sort_weekdays(values, first_day, last_day):
1.309 +
1.310 + """
1.311 + Return a sorted copy of the given 'values', each having the form (weekday
1.312 + number, instance number) using 'weekdays' to define the ordering of the
1.313 + weekday numbers and 'limit' to determine the positions of negative instance
1.314 + numbers.
1.315 + """
1.316 +
1.317 + weekdays = get_ordered_weekdays(first_day)
1.318 +
1.319 + # Expand the values to incorporate specific weekday instances.
1.320 +
1.321 + l = []
1.322 +
1.323 + for weekday, index in values:
1.324 +
1.325 + # Obtain the last instance number of the weekday in the period.
1.326 +
1.327 + limit = get_last_weekday_instance(weekday, first_day, last_day)
1.328 +
1.329 + # For specific instances, convert negative selections.
1.330 +
1.331 + if index is not None:
1.332 + l.append((weekday, index < 0 and index + 1 + limit or index))
1.333 +
1.334 + # For None, introduce selections for all instances of the weekday.
1.335 +
1.336 + else:
1.337 + index = 1
1.338 + while index <= limit:
1.339 + l.append((weekday, index))
1.340 + index += 1
1.341 +
1.342 + # Sort the values so that the resulting dates are ordered.
1.343 +
1.344 + fn = lambda x, y, weekdays=weekdays: compare_weekday_selectors(x, y, weekdays)
1.345 + l.sort(cmp=fn)
1.346 + return l
1.347 +
1.348 # Classes for producing instances from recurrence structures.
1.349
1.350 class Selector:
1.351 @@ -472,12 +667,9 @@
1.352 self.selecting = selecting
1.353 self.first = first
1.354
1.355 - # Define the index of values from datetimes involved with this selector.
1.356 -
1.357 - self.pos = positions[level]
1.358 -
1.359 def __repr__(self):
1.360 - return "%s(%r, %r, %r, %r)" % (self.__class__.__name__, self.level, self.args, self.qualifier, self.first)
1.361 + return "%s(%r, %r, %r, %r)" % (self.__class__.__name__, self.level,
1.362 + self.args, self.qualifier, self.first)
1.363
1.364 def materialise(self, start, end, count=None, setpos=None, inclusive=False):
1.365
1.366 @@ -493,9 +685,8 @@
1.367
1.368 start = to_tuple(start)
1.369 end = to_tuple(end)
1.370 - counter = count and [0, count]
1.371 + counter = Counter(count)
1.372 results = self.materialise_items(start, start, end, counter, setpos, inclusive)
1.373 - results.sort()
1.374 return results[:count]
1.375
1.376 def materialise_item(self, current, earliest, next, counter, setpos=None, inclusive=False):
1.377 @@ -509,7 +700,8 @@
1.378 """
1.379
1.380 if self.selecting:
1.381 - return self.selecting.materialise_items(current, earliest, next, counter, setpos, inclusive)
1.382 + return self.selecting.materialise_items(current, earliest, next,
1.383 + counter, setpos, inclusive)
1.384 elif earliest <= current:
1.385 return [current]
1.386 else:
1.387 @@ -521,9 +713,8 @@
1.388
1.389 l = []
1.390 for pos in setpos:
1.391 - lower = pos < 0 and pos or pos - 1
1.392 - upper = pos > 0 and pos or pos < -1 and pos + 1 or None
1.393 - l.append((lower, upper))
1.394 + index = pos < 0 and pos or pos - 1
1.395 + l.append(index)
1.396 return l
1.397
1.398 def select_positions(self, results, setpos):
1.399 @@ -532,25 +723,15 @@
1.400
1.401 results.sort()
1.402 l = []
1.403 - for lower, upper in self.convert_positions(setpos):
1.404 - l += results[lower:upper]
1.405 + for index in self.convert_positions(setpos):
1.406 + l.append(results[index])
1.407 return l
1.408
1.409 - def filter_by_period(self, results, start, end, inclusive):
1.410 -
1.411 - """
1.412 - Filter 'results' so that only those at or after 'start' and before 'end'
1.413 - are returned.
1.414 + def filter_by_period(self, result, start, end, inclusive):
1.415
1.416 - If 'inclusive' is specified, the selection of instances will include the
1.417 - end of the search period if present in the results.
1.418 - """
1.419 + "Return whether 'result' occurs at or after 'start' and before 'end'."
1.420
1.421 - l = []
1.422 - for result in results:
1.423 - if start <= result and (inclusive and result <= end or result < end):
1.424 - l.append(result)
1.425 - return l
1.426 + return start <= result and (inclusive and result <= end or result < end)
1.427
1.428 class Pattern(Selector):
1.429
1.430 @@ -569,26 +750,25 @@
1.431
1.432 # Define the step between result periods.
1.433
1.434 - interval = self.args.get("interval", 1) * units.get(self.qualifier, 1)
1.435 - step = scale(interval, self.pos)
1.436 + multiple = get_multiple(self.qualifier)
1.437 + interval = self.args.get("interval", 1) * multiple
1.438 + step = scale(interval, self.level)
1.439
1.440 # Define the scale of a single period.
1.441
1.442 - unit_interval = units.get(self.qualifier, 1)
1.443 - unit_step = scale(unit_interval, self.pos)
1.444 + unit_step = scale(multiple, self.level)
1.445
1.446 # Employ the context as the current period if this is the first
1.447 # qualifier in the selection chain.
1.448
1.449 if self.first:
1.450 - current = context[:self.pos+1]
1.451 + current = precision(context, self.level)
1.452
1.453 # Otherwise, obtain the first value at this resolution within the
1.454 # context period.
1.455
1.456 else:
1.457 - first = scale(firstvalues[self.level], self.pos)
1.458 - current = combine(context[:self.pos], first)
1.459 + current = precision(context, self.level, firstvalues[self.level])
1.460
1.461 results = []
1.462
1.463 @@ -596,7 +776,7 @@
1.464 # provided that any limit imposed by the counter has not been exceeded.
1.465
1.466 while (inclusive and current <= end or current < end) and \
1.467 - (counter is None or counter[0] < counter[1]):
1.468 + not counter.at_limit():
1.469
1.470 # Increment the current datetime by the step for the next period.
1.471
1.472 @@ -610,12 +790,13 @@
1.473 # current period and any contraining start and end points, plus
1.474 # counter, setpos and inclusive details.
1.475
1.476 - interval_results = self.materialise_item(current, max(current, start), min(current_end, end), counter, setpos, inclusive)
1.477 + interval_results = self.materialise_item(current,
1.478 + max(current, start), min(current_end, end),
1.479 + counter, setpos, inclusive)
1.480
1.481 # Update the counter with the number of identified results.
1.482
1.483 - if counter is not None:
1.484 - counter[0] += len(interval_results)
1.485 + counter += len(interval_results)
1.486
1.487 # Accumulate the results.
1.488
1.489 @@ -632,21 +813,20 @@
1.490 "A selector of instances specified in terms of day numbers."
1.491
1.492 def materialise_items(self, context, start, end, counter, setpos=None, inclusive=False):
1.493 - step = scale(1, 2)
1.494 + step = scale(1, WEEKS)
1.495 results = []
1.496
1.497 # Get weekdays in the year.
1.498
1.499 - if len(context) == 1:
1.500 - first_day = (context[0], 1, 1)
1.501 - last_day = (context[0], 12, 31)
1.502 + if is_year_only(context):
1.503 + first_day = start_of_year(context)
1.504 + last_day = end_of_year(context)
1.505
1.506 # Get weekdays in the month.
1.507
1.508 - elif len(context) == 2:
1.509 - first_day = (context[0], context[1], 1)
1.510 - last_day = update((context[0], context[1], 1), (0, 1, 0))
1.511 - last_day = update(last_day, (0, 0, -1))
1.512 + elif is_month_only(context):
1.513 + first_day = start_of_month(context)
1.514 + last_day = end_of_month(context)
1.515
1.516 # Get weekdays in the week.
1.517
1.518 @@ -656,8 +836,11 @@
1.519
1.520 while (inclusive and current <= end or current < end):
1.521 next = update(current, step)
1.522 +
1.523 if date(*current).isoweekday() in values:
1.524 - results += self.materialise_item(current, max(current, start), min(next, end), counter, inclusive=inclusive)
1.525 + results += self.materialise_item(current,
1.526 + max(current, start), min(next, end),
1.527 + counter, inclusive=inclusive)
1.528 current = next
1.529
1.530 if setpos:
1.531 @@ -667,113 +850,113 @@
1.532
1.533 # Find each of the given days.
1.534
1.535 - for value, index in self.args["values"]:
1.536 - if index is not None:
1.537 - offset = timedelta(7 * (abs(index) - 1))
1.538 + for value, index in sort_weekdays(self.args["values"], first_day, last_day):
1.539 + offset = timedelta(7 * (abs(index) - 1))
1.540 +
1.541 + current = precision(to_tuple(get_first_day(first_day, value) + offset), DAYS)
1.542 + next = update(current, step)
1.543
1.544 - if index < 0:
1.545 - current = to_tuple(get_last_day(last_day, value) - offset, 3)
1.546 - else:
1.547 - current = to_tuple(get_first_day(first_day, value) + offset, 3)
1.548 + # To support setpos, only current and next bound the search, not
1.549 + # the period in addition.
1.550
1.551 - next = update(current, step)
1.552 + results += self.materialise_item(current, current, next, counter,
1.553 + inclusive=inclusive)
1.554
1.555 - # To support setpos, only current and next bound the search, not
1.556 - # the period in addition.
1.557 + # Extract selected positions and remove out-of-period instances.
1.558
1.559 - results += self.materialise_item(current, current, next, counter, inclusive=inclusive)
1.560 + if setpos:
1.561 + results = self.select_positions(results, setpos)
1.562
1.563 - else:
1.564 - if index < 0:
1.565 - current = to_tuple(get_last_day(last_day, value), 3)
1.566 - direction = operator.sub
1.567 - else:
1.568 - current = to_tuple(get_first_day(first_day, value), 3)
1.569 - direction = operator.add
1.570 + return filter(lambda result:
1.571 + self.filter_by_period(result, start, end, inclusive),
1.572 + results)
1.573 +
1.574 +class Enum(Selector):
1.575 +
1.576 + "A generic value selector."
1.577 +
1.578 + def materialise_items(self, context, start, end, counter, setpos=None, inclusive=False):
1.579 + step = scale(1, self.level)
1.580 + results = []
1.581
1.582 - while first_day <= current <= last_day:
1.583 - next = update(current, step)
1.584 + # Select each value at the current resolution.
1.585 +
1.586 + for value in sort_values(self.args["values"]):
1.587 + current = precision(context, self.level, value)
1.588 + next = update(current, step)
1.589
1.590 - # To support setpos, only current and next bound the search, not
1.591 - # the period in addition.
1.592 + # To support setpos, only current and next bound the search, not
1.593 + # the period in addition.
1.594
1.595 - results += self.materialise_item(current, current, next, counter, inclusive=inclusive)
1.596 - current = to_tuple(direction(date(*current), timedelta(7)), 3)
1.597 + results += self.materialise_item(current, current, next, counter,
1.598 + setpos, inclusive)
1.599
1.600 # Extract selected positions and remove out-of-period instances.
1.601
1.602 if setpos:
1.603 results = self.select_positions(results, setpos)
1.604
1.605 - return self.filter_by_period(results, start, end, inclusive)
1.606 + return filter(lambda result:
1.607 + self.filter_by_period(result, start, end, inclusive),
1.608 + results)
1.609
1.610 -class Enum(Selector):
1.611 +class MonthDayFilter(Enum):
1.612 +
1.613 + "A selector of month days."
1.614 +
1.615 def materialise_items(self, context, start, end, counter, setpos=None, inclusive=False):
1.616 - step = scale(1, self.pos)
1.617 + step = scale(1, self.level)
1.618 results = []
1.619 - for value in self.args["values"]:
1.620 - current = combine(context[:self.pos], scale(value, self.pos))
1.621 +
1.622 + last_day = end_of_month(context)[2]
1.623 +
1.624 + for value in sort_values(self.args["values"], last_day):
1.625 + current = precision(context, self.level, value)
1.626 next = update(current, step)
1.627
1.628 # To support setpos, only current and next bound the search, not
1.629 # the period in addition.
1.630
1.631 - results += self.materialise_item(current, current, next, counter, setpos, inclusive)
1.632 + results += self.materialise_item(current, current, next, counter,
1.633 + inclusive=inclusive)
1.634
1.635 # Extract selected positions and remove out-of-period instances.
1.636
1.637 if setpos:
1.638 results = self.select_positions(results, setpos)
1.639
1.640 - return self.filter_by_period(results, start, end, inclusive)
1.641 + return filter(lambda result:
1.642 + self.filter_by_period(result, start, end, inclusive),
1.643 + results)
1.644
1.645 -class MonthDayFilter(Enum):
1.646 +class YearDayFilter(Enum):
1.647 +
1.648 + "A selector of days in years."
1.649 +
1.650 def materialise_items(self, context, start, end, counter, setpos=None, inclusive=False):
1.651 - last_day = monthrange(context[0], context[1])[1]
1.652 - step = scale(1, self.pos)
1.653 + step = scale(1, self.level)
1.654 results = []
1.655 - for value in self.args["values"]:
1.656 - if value < 0:
1.657 - value = last_day + 1 + value
1.658 - current = combine(context, scale(value, self.pos))
1.659 +
1.660 + year_length = get_year_length(context)
1.661 +
1.662 + for value in sort_values(self.args["values"], year_length):
1.663 + current = day_in_year(context, value)
1.664 next = update(current, step)
1.665
1.666 # To support setpos, only current and next bound the search, not
1.667 # the period in addition.
1.668
1.669 - results += self.materialise_item(current, current, next, counter, inclusive=inclusive)
1.670 + results += self.materialise_item(current, current, next, counter,
1.671 + inclusive=inclusive)
1.672
1.673 # Extract selected positions and remove out-of-period instances.
1.674
1.675 if setpos:
1.676 results = self.select_positions(results, setpos)
1.677
1.678 - return self.filter_by_period(results, start, end, inclusive)
1.679 -
1.680 -class YearDayFilter(Enum):
1.681 - def materialise_items(self, context, start, end, counter, setpos=None, inclusive=False):
1.682 - first_day = date(context[0], 1, 1)
1.683 - next_first_day = date(context[0] + 1, 1, 1)
1.684 - year_length = (next_first_day - first_day).days
1.685 - step = scale(1, self.pos)
1.686 - results = []
1.687 - for value in self.args["values"]:
1.688 - if value < 0:
1.689 - value = year_length + 1 + value
1.690 - current = to_tuple(first_day + timedelta(value - 1), 3)
1.691 - next = update(current, step)
1.692 -
1.693 - # To support setpos, only current and next bound the search, not
1.694 - # the period in addition.
1.695 -
1.696 - results += self.materialise_item(current, current, next, counter, inclusive=inclusive)
1.697 -
1.698 - # Extract selected positions and remove out-of-period instances.
1.699 -
1.700 - if setpos:
1.701 - results = self.select_positions(results, setpos)
1.702 -
1.703 - return self.filter_by_period(results, start, end, inclusive)
1.704 + return filter(lambda result:
1.705 + self.filter_by_period(result, start, end, inclusive),
1.706 + results)
1.707
1.708 special_enum_levels = {
1.709 "BYDAY" : WeekDayFilter,
1.710 @@ -781,6 +964,21 @@
1.711 "BYYEARDAY" : YearDayFilter,
1.712 }
1.713
1.714 +class Counter:
1.715 +
1.716 + "A counter to track instance quantities."
1.717 +
1.718 + def __init__(self, limit):
1.719 + self.current = 0
1.720 + self.limit = limit
1.721 +
1.722 + def __iadd__(self, n):
1.723 + self.current += n
1.724 + return self
1.725 +
1.726 + def at_limit(self):
1.727 + return self.limit is not None and self.current >= self.limit
1.728 +
1.729 # Public functions.
1.730
1.731 def connect_selectors(selectors):