2.1 --- a/vRecurrence.py Fri Nov 24 18:11:57 2017 +0100
2.2 +++ b/vRecurrence.py Sun Oct 22 01:24:08 2017 +0200
2.3 @@ -70,6 +70,10 @@
2.4 "SECONDLY"
2.5 )
2.6
2.7 +# Symbols corresponding to resolution levels.
2.8 +
2.9 +YEARS, MONTHS, WEEKS, DAYS, HOURS, MINUTES, SECONDS = 0, 1, 2, 5, 6, 7, 8
2.10 +
2.11 # Enumeration levels, employed by BY... qualifiers.
2.12
2.13 enum_levels = (
2.14 @@ -98,19 +102,34 @@
2.15
2.16 firstvalues = [0, 1, 1, 1, 1, 1, 0, 0, 0]
2.17
2.18 -# Map from qualifiers to interval units. Here, weeks are defined as 7 days.
2.19 +# Map from qualifiers to interval multiples. Here, weeks are defined as 7 days.
2.20
2.21 -units = {"WEEKLY" : 7}
2.22 +multiples = {"WEEKLY" : 7}
2.23
2.24 # Make dictionaries mapping qualifiers to levels.
2.25
2.26 -freq = dict([(level, i) for (i, level) in enumerate(freq_levels) if level])
2.27 -enum = dict([(level, i) for (i, level) in enumerate(enum_levels) if level])
2.28 -weekdays = dict([(weekday, i+1) for (i, weekday) in enumerate(["MO", "TU", "WE", "TH", "FR", "SA", "SU"])])
2.29 +freq = {}
2.30 +for i, level in enumerate(freq_levels):
2.31 + if level:
2.32 + freq[level] = i
2.33 +
2.34 +enum = {}
2.35 +for i, level in enumerate(enum_levels):
2.36 + if level:
2.37 + enum[level] = i
2.38 +
2.39 +# Weekdays: name -> 1-based value
2.40 +
2.41 +weekdays = {}
2.42 +for i, weekday in enumerate(["MO", "TU", "WE", "TH", "FR", "SA", "SU"]):
2.43 + weekdays[weekday] = i + 1
2.44
2.45 # Functions for structuring the recurrences.
2.46
2.47 def get_next(it):
2.48 +
2.49 + "Return the next value from iterator 'it' or None if no more values exist."
2.50 +
2.51 try:
2.52 return it.next()
2.53 except StopIteration:
2.54 @@ -186,10 +205,15 @@
2.55 suitable values.
2.56 """
2.57
2.58 + # For non-weekday selection, obtain a list of day numbers.
2.59 +
2.60 if qualifier != "BYDAY":
2.61 return map(int, value.split(","))
2.62
2.63 + # For weekday selection, obtain the weekday number and instance number.
2.64 +
2.65 values = []
2.66 +
2.67 for part in value.split(","):
2.68 weekday = weekdays.get(part[-2:])
2.69 if not weekday:
2.70 @@ -237,15 +261,15 @@
2.71 l = []
2.72 offset = 0
2.73
2.74 - for level, value in enumerate(datetime):
2.75 + for pos, value in enumerate(datetime):
2.76
2.77 # At the day number, adjust the frequency level offset to reference
2.78 # days (and then hours, minutes, seconds).
2.79
2.80 - if level == 2:
2.81 + if pos == 2:
2.82 offset = 3
2.83
2.84 - l.append(Enum(level + offset, {"values" : [value]}, "DT"))
2.85 + l.append(Enum(pos + offset, {"values" : [value]}, "DT"))
2.86
2.87 return l
2.88
2.89 @@ -318,7 +342,9 @@
2.90
2.91 # Ignore datetime values that conflict with day-level qualifiers.
2.92
2.93 - if not l or from_dt.level != freq["DAILY"] or l[-1].level not in daylevels:
2.94 + if not l or from_dt.level != freq["DAILY"] or \
2.95 + l[-1].level not in daylevels:
2.96 +
2.97 l.append(from_dt)
2.98
2.99 from_dt = get_next(iter_dt)
2.100 @@ -352,24 +378,120 @@
2.101 repeat = Pattern(level - 1, {"interval" : 1}, None)
2.102 l.append(repeat)
2.103
2.104 +def get_multiple(qualifier):
2.105 +
2.106 + "Return the time unit multiple for 'qualifier'."
2.107 +
2.108 + return multiples.get(qualifier, 1)
2.109 +
2.110 # Datetime arithmetic.
2.111
2.112 -def combine(t1, t2):
2.113 +def is_year_only(t):
2.114 +
2.115 + "Return if 't' describes a year."
2.116 +
2.117 + return len(t) == lengths[YEARS]
2.118 +
2.119 +def is_month_only(t):
2.120 +
2.121 + "Return if 't' describes a month within a year."
2.122 +
2.123 + return len(t) == lengths[MONTHS]
2.124 +
2.125 +def start_of_year(t):
2.126 +
2.127 + "Return the start of the year referenced by 't'."
2.128 +
2.129 + return (t[0], 1, 1)
2.130 +
2.131 +def end_of_year(t):
2.132 +
2.133 + "Return the end of the year referenced by 't'."
2.134 +
2.135 + return (t[0], 12, 31)
2.136 +
2.137 +def start_of_month(t):
2.138 +
2.139 + "Return the start of the month referenced by 't'."
2.140 +
2.141 + year, month = t[:2]
2.142 + return (year, month, 1)
2.143 +
2.144 +def end_of_month(t):
2.145 +
2.146 + "Return the end of the month referenced by 't'."
2.147 +
2.148 + year, month = t[:2]
2.149 + return update(update((year, month, 1), (0, 1, 0)), (0, 0, -1))
2.150 +
2.151 +def day_in_year(t, number):
2.152 +
2.153 + "Return the day in the year referenced by 't' with the given 'number'."
2.154 +
2.155 + return to_tuple(date(*start_of_year(t)) + timedelta(number - 1))
2.156 +
2.157 +def get_year_length(t):
2.158 +
2.159 + "Return the length of the year referenced by 't'."
2.160 +
2.161 + first_day = date(*start_of_year(t))
2.162 + last_day = date(*end_of_year(t))
2.163 + return (last_day - first_day).days + 1
2.164 +
2.165 +def get_weekday(t):
2.166 +
2.167 + "Return the 1-based weekday for the month referenced by 't'."
2.168 +
2.169 + year, month = t[:2]
2.170 + return monthrange(year, month)[0] + 1
2.171 +
2.172 +def get_ordered_weekdays(t):
2.173
2.174 """
2.175 - Combine tuples 't1' and 't2', returning a copy of 't1' filled with values
2.176 - from 't2' in positions where 0 appeared in 't1'.
2.177 + Return the 1-based weekday sequence describing the first week of the month
2.178 + referenced by 't'.
2.179 """
2.180
2.181 - return tuple(map(lambda x, y: x or y, t1, t2))
2.182 + first = get_weekday(t)
2.183 + return range(first, 8) + range(1, first)
2.184 +
2.185 +def get_last_weekday_instance(weekday, first_day, last_day):
2.186 +
2.187 + """
2.188 + Return the last instance number for 'weekday' within the period from
2.189 + 'first_day' to 'last_day' inclusive.
2.190
2.191 -def scale(interval, pos):
2.192 + Here, 'weekday' is 1-based (starting on Monday), the returned limit is
2.193 + 1-based.
2.194 + """
2.195 +
2.196 + weekday0 = get_first_day(first_day, weekday)
2.197 + days = (date(*last_day) - weekday0).days
2.198 + return days / 7 + 1
2.199 +
2.200 +def precision(t, level, value=None):
2.201
2.202 """
2.203 - Scale the given 'interval' value to the indicated position 'pos', returning
2.204 - a tuple with leading zero elements and 'interval' at the stated position.
2.205 + Return 't' trimmed in resolution to the indicated resolution 'level',
2.206 + setting 'value' at the given resolution if indicated.
2.207 """
2.208
2.209 + pos = positions[level]
2.210 +
2.211 + if value is None:
2.212 + return t[:pos + 1]
2.213 + else:
2.214 + return t[:pos] + (value,)
2.215 +
2.216 +def scale(interval, level):
2.217 +
2.218 + """
2.219 + Scale the given 'interval' value in resolution to the indicated resolution
2.220 + 'level', returning a tuple with leading zero elements and 'interval' at the
2.221 + stated position.
2.222 + """
2.223 +
2.224 + pos = positions[level]
2.225 return (0,) * pos + (interval,)
2.226
2.227 def get_seconds(t):
2.228 @@ -413,24 +535,26 @@
2.229 d = datetime(*updated_for_months)
2.230 s = timedelta(step[2], get_seconds(step))
2.231
2.232 - return to_tuple(d + s, len(t))
2.233 + return to_tuple(d + s)[:len(t)]
2.234
2.235 -def to_tuple(d, n=None):
2.236 +def to_tuple(d):
2.237
2.238 - "Return 'd' as a tuple, optionally trimming the result to 'n' positions."
2.239 + "Return date or datetime 'd' as a tuple."
2.240
2.241 if not isinstance(d, date):
2.242 return d
2.243 - if n is None:
2.244 - if isinstance(d, datetime):
2.245 - n = 6
2.246 - else:
2.247 - n = 3
2.248 + if isinstance(d, datetime):
2.249 + n = 6
2.250 + else:
2.251 + n = 3
2.252 return d.timetuple()[:n]
2.253
2.254 def get_first_day(first_day, weekday):
2.255
2.256 - "Return the first occurrence at or after 'first_day' of 'weekday'."
2.257 + """
2.258 + Return the first occurrence at or after 'first_day' of 'weekday' as a date
2.259 + instance.
2.260 + """
2.261
2.262 first_day = date(*first_day)
2.263 first_weekday = first_day.isoweekday()
2.264 @@ -441,7 +565,10 @@
2.265
2.266 def get_last_day(last_day, weekday):
2.267
2.268 - "Return the last occurrence at or before 'last_day' of 'weekday'."
2.269 + """
2.270 + Return the last occurrence at or before 'last_day' of 'weekday' as a date
2.271 + instance.
2.272 + """
2.273
2.274 last_day = date(*last_day)
2.275 last_weekday = last_day.isoweekday()
2.276 @@ -450,6 +577,74 @@
2.277 else:
2.278 return last_day - timedelta(last_weekday - weekday)
2.279
2.280 +# Value expansion and sorting.
2.281 +
2.282 +def sort_values(values, limit=None):
2.283 +
2.284 + """
2.285 + Sort the given 'values' using 'limit' to determine the positions of negative
2.286 + values.
2.287 + """
2.288 +
2.289 + # Convert negative values to positive values according to the value limit.
2.290 +
2.291 + if limit is not None:
2.292 + l = map(lambda x, limit=limit: x < 0 and x + 1 + limit or x, values)
2.293 + else:
2.294 + l = values[:]
2.295 +
2.296 + l.sort()
2.297 + return l
2.298 +
2.299 +def compare_weekday_selectors(x, y, weekdays):
2.300 +
2.301 + """
2.302 + Compare 'x' and 'y' values of the form (weekday number, instance number)
2.303 + using 'weekdays' to define an ordering of the weekday numbers.
2.304 + """
2.305 +
2.306 + return cmp((x[1], weekdays.index(x[0])), (y[1], weekdays.index(y[0])))
2.307 +
2.308 +def sort_weekdays(values, first_day, last_day):
2.309 +
2.310 + """
2.311 + Return a sorted copy of the given 'values', each having the form (weekday
2.312 + number, instance number) using 'weekdays' to define the ordering of the
2.313 + weekday numbers and 'limit' to determine the positions of negative instance
2.314 + numbers.
2.315 + """
2.316 +
2.317 + weekdays = get_ordered_weekdays(first_day)
2.318 +
2.319 + # Expand the values to incorporate specific weekday instances.
2.320 +
2.321 + l = []
2.322 +
2.323 + for weekday, index in values:
2.324 +
2.325 + # Obtain the last instance number of the weekday in the period.
2.326 +
2.327 + limit = get_last_weekday_instance(weekday, first_day, last_day)
2.328 +
2.329 + # For specific instances, convert negative selections.
2.330 +
2.331 + if index is not None:
2.332 + l.append((weekday, index < 0 and index + 1 + limit or index))
2.333 +
2.334 + # For None, introduce selections for all instances of the weekday.
2.335 +
2.336 + else:
2.337 + index = 1
2.338 + while index <= limit:
2.339 + l.append((weekday, index))
2.340 + index += 1
2.341 +
2.342 + # Sort the values so that the resulting dates are ordered.
2.343 +
2.344 + fn = lambda x, y, weekdays=weekdays: compare_weekday_selectors(x, y, weekdays)
2.345 + l.sort(cmp=fn)
2.346 + return l
2.347 +
2.348 # Classes for producing instances from recurrence structures.
2.349
2.350 class Selector:
2.351 @@ -472,12 +667,9 @@
2.352 self.selecting = selecting
2.353 self.first = first
2.354
2.355 - # Define the index of values from datetimes involved with this selector.
2.356 -
2.357 - self.pos = positions[level]
2.358 -
2.359 def __repr__(self):
2.360 - return "%s(%r, %r, %r, %r)" % (self.__class__.__name__, self.level, self.args, self.qualifier, self.first)
2.361 + return "%s(%r, %r, %r, %r)" % (self.__class__.__name__, self.level,
2.362 + self.args, self.qualifier, self.first)
2.363
2.364 def materialise(self, start, end, count=None, setpos=None, inclusive=False):
2.365
2.366 @@ -493,9 +685,8 @@
2.367
2.368 start = to_tuple(start)
2.369 end = to_tuple(end)
2.370 - counter = count and [0, count]
2.371 + counter = Counter(count)
2.372 results = self.materialise_items(start, start, end, counter, setpos, inclusive)
2.373 - results.sort()
2.374 return results[:count]
2.375
2.376 def materialise_item(self, current, earliest, next, counter, setpos=None, inclusive=False):
2.377 @@ -509,7 +700,8 @@
2.378 """
2.379
2.380 if self.selecting:
2.381 - return self.selecting.materialise_items(current, earliest, next, counter, setpos, inclusive)
2.382 + return self.selecting.materialise_items(current, earliest, next,
2.383 + counter, setpos, inclusive)
2.384 elif earliest <= current:
2.385 return [current]
2.386 else:
2.387 @@ -521,9 +713,8 @@
2.388
2.389 l = []
2.390 for pos in setpos:
2.391 - lower = pos < 0 and pos or pos - 1
2.392 - upper = pos > 0 and pos or pos < -1 and pos + 1 or None
2.393 - l.append((lower, upper))
2.394 + index = pos < 0 and pos or pos - 1
2.395 + l.append(index)
2.396 return l
2.397
2.398 def select_positions(self, results, setpos):
2.399 @@ -532,25 +723,15 @@
2.400
2.401 results.sort()
2.402 l = []
2.403 - for lower, upper in self.convert_positions(setpos):
2.404 - l += results[lower:upper]
2.405 + for index in self.convert_positions(setpos):
2.406 + l.append(results[index])
2.407 return l
2.408
2.409 - def filter_by_period(self, results, start, end, inclusive):
2.410 -
2.411 - """
2.412 - Filter 'results' so that only those at or after 'start' and before 'end'
2.413 - are returned.
2.414 + def filter_by_period(self, result, start, end, inclusive):
2.415
2.416 - If 'inclusive' is specified, the selection of instances will include the
2.417 - end of the search period if present in the results.
2.418 - """
2.419 + "Return whether 'result' occurs at or after 'start' and before 'end'."
2.420
2.421 - l = []
2.422 - for result in results:
2.423 - if start <= result and (inclusive and result <= end or result < end):
2.424 - l.append(result)
2.425 - return l
2.426 + return start <= result and (inclusive and result <= end or result < end)
2.427
2.428 class Pattern(Selector):
2.429
2.430 @@ -569,26 +750,25 @@
2.431
2.432 # Define the step between result periods.
2.433
2.434 - interval = self.args.get("interval", 1) * units.get(self.qualifier, 1)
2.435 - step = scale(interval, self.pos)
2.436 + multiple = get_multiple(self.qualifier)
2.437 + interval = self.args.get("interval", 1) * multiple
2.438 + step = scale(interval, self.level)
2.439
2.440 # Define the scale of a single period.
2.441
2.442 - unit_interval = units.get(self.qualifier, 1)
2.443 - unit_step = scale(unit_interval, self.pos)
2.444 + unit_step = scale(multiple, self.level)
2.445
2.446 # Employ the context as the current period if this is the first
2.447 # qualifier in the selection chain.
2.448
2.449 if self.first:
2.450 - current = context[:self.pos+1]
2.451 + current = precision(context, self.level)
2.452
2.453 # Otherwise, obtain the first value at this resolution within the
2.454 # context period.
2.455
2.456 else:
2.457 - first = scale(firstvalues[self.level], self.pos)
2.458 - current = combine(context[:self.pos], first)
2.459 + current = precision(context, self.level, firstvalues[self.level])
2.460
2.461 results = []
2.462
2.463 @@ -596,7 +776,7 @@
2.464 # provided that any limit imposed by the counter has not been exceeded.
2.465
2.466 while (inclusive and current <= end or current < end) and \
2.467 - (counter is None or counter[0] < counter[1]):
2.468 + not counter.at_limit():
2.469
2.470 # Increment the current datetime by the step for the next period.
2.471
2.472 @@ -610,12 +790,13 @@
2.473 # current period and any contraining start and end points, plus
2.474 # counter, setpos and inclusive details.
2.475
2.476 - interval_results = self.materialise_item(current, max(current, start), min(current_end, end), counter, setpos, inclusive)
2.477 + interval_results = self.materialise_item(current,
2.478 + max(current, start), min(current_end, end),
2.479 + counter, setpos, inclusive)
2.480
2.481 # Update the counter with the number of identified results.
2.482
2.483 - if counter is not None:
2.484 - counter[0] += len(interval_results)
2.485 + counter += len(interval_results)
2.486
2.487 # Accumulate the results.
2.488
2.489 @@ -632,21 +813,20 @@
2.490 "A selector of instances specified in terms of day numbers."
2.491
2.492 def materialise_items(self, context, start, end, counter, setpos=None, inclusive=False):
2.493 - step = scale(1, 2)
2.494 + step = scale(1, WEEKS)
2.495 results = []
2.496
2.497 # Get weekdays in the year.
2.498
2.499 - if len(context) == 1:
2.500 - first_day = (context[0], 1, 1)
2.501 - last_day = (context[0], 12, 31)
2.502 + if is_year_only(context):
2.503 + first_day = start_of_year(context)
2.504 + last_day = end_of_year(context)
2.505
2.506 # Get weekdays in the month.
2.507
2.508 - elif len(context) == 2:
2.509 - first_day = (context[0], context[1], 1)
2.510 - last_day = update((context[0], context[1], 1), (0, 1, 0))
2.511 - last_day = update(last_day, (0, 0, -1))
2.512 + elif is_month_only(context):
2.513 + first_day = start_of_month(context)
2.514 + last_day = end_of_month(context)
2.515
2.516 # Get weekdays in the week.
2.517
2.518 @@ -656,8 +836,11 @@
2.519
2.520 while (inclusive and current <= end or current < end):
2.521 next = update(current, step)
2.522 +
2.523 if date(*current).isoweekday() in values:
2.524 - results += self.materialise_item(current, max(current, start), min(next, end), counter, inclusive=inclusive)
2.525 + results += self.materialise_item(current,
2.526 + max(current, start), min(next, end),
2.527 + counter, inclusive=inclusive)
2.528 current = next
2.529
2.530 if setpos:
2.531 @@ -667,113 +850,113 @@
2.532
2.533 # Find each of the given days.
2.534
2.535 - for value, index in self.args["values"]:
2.536 - if index is not None:
2.537 - offset = timedelta(7 * (abs(index) - 1))
2.538 + for value, index in sort_weekdays(self.args["values"], first_day, last_day):
2.539 + offset = timedelta(7 * (abs(index) - 1))
2.540 +
2.541 + current = precision(to_tuple(get_first_day(first_day, value) + offset), DAYS)
2.542 + next = update(current, step)
2.543
2.544 - if index < 0:
2.545 - current = to_tuple(get_last_day(last_day, value) - offset, 3)
2.546 - else:
2.547 - current = to_tuple(get_first_day(first_day, value) + offset, 3)
2.548 + # To support setpos, only current and next bound the search, not
2.549 + # the period in addition.
2.550
2.551 - next = update(current, step)
2.552 + results += self.materialise_item(current, current, next, counter,
2.553 + inclusive=inclusive)
2.554
2.555 - # To support setpos, only current and next bound the search, not
2.556 - # the period in addition.
2.557 + # Extract selected positions and remove out-of-period instances.
2.558
2.559 - results += self.materialise_item(current, current, next, counter, inclusive=inclusive)
2.560 + if setpos:
2.561 + results = self.select_positions(results, setpos)
2.562
2.563 - else:
2.564 - if index < 0:
2.565 - current = to_tuple(get_last_day(last_day, value), 3)
2.566 - direction = operator.sub
2.567 - else:
2.568 - current = to_tuple(get_first_day(first_day, value), 3)
2.569 - direction = operator.add
2.570 + return filter(lambda result:
2.571 + self.filter_by_period(result, start, end, inclusive),
2.572 + results)
2.573 +
2.574 +class Enum(Selector):
2.575 +
2.576 + "A generic value selector."
2.577 +
2.578 + def materialise_items(self, context, start, end, counter, setpos=None, inclusive=False):
2.579 + step = scale(1, self.level)
2.580 + results = []
2.581
2.582 - while first_day <= current <= last_day:
2.583 - next = update(current, step)
2.584 + # Select each value at the current resolution.
2.585 +
2.586 + for value in sort_values(self.args["values"]):
2.587 + current = precision(context, self.level, value)
2.588 + next = update(current, step)
2.589
2.590 - # To support setpos, only current and next bound the search, not
2.591 - # the period in addition.
2.592 + # To support setpos, only current and next bound the search, not
2.593 + # the period in addition.
2.594
2.595 - results += self.materialise_item(current, current, next, counter, inclusive=inclusive)
2.596 - current = to_tuple(direction(date(*current), timedelta(7)), 3)
2.597 + results += self.materialise_item(current, current, next, counter,
2.598 + setpos, inclusive)
2.599
2.600 # Extract selected positions and remove out-of-period instances.
2.601
2.602 if setpos:
2.603 results = self.select_positions(results, setpos)
2.604
2.605 - return self.filter_by_period(results, start, end, inclusive)
2.606 + return filter(lambda result:
2.607 + self.filter_by_period(result, start, end, inclusive),
2.608 + results)
2.609
2.610 -class Enum(Selector):
2.611 +class MonthDayFilter(Enum):
2.612 +
2.613 + "A selector of month days."
2.614 +
2.615 def materialise_items(self, context, start, end, counter, setpos=None, inclusive=False):
2.616 - step = scale(1, self.pos)
2.617 + step = scale(1, self.level)
2.618 results = []
2.619 - for value in self.args["values"]:
2.620 - current = combine(context[:self.pos], scale(value, self.pos))
2.621 +
2.622 + last_day = end_of_month(context)[2]
2.623 +
2.624 + for value in sort_values(self.args["values"], last_day):
2.625 + current = precision(context, self.level, value)
2.626 next = update(current, step)
2.627
2.628 # To support setpos, only current and next bound the search, not
2.629 # the period in addition.
2.630
2.631 - results += self.materialise_item(current, current, next, counter, setpos, inclusive)
2.632 + results += self.materialise_item(current, current, next, counter,
2.633 + inclusive=inclusive)
2.634
2.635 # Extract selected positions and remove out-of-period instances.
2.636
2.637 if setpos:
2.638 results = self.select_positions(results, setpos)
2.639
2.640 - return self.filter_by_period(results, start, end, inclusive)
2.641 + return filter(lambda result:
2.642 + self.filter_by_period(result, start, end, inclusive),
2.643 + results)
2.644
2.645 -class MonthDayFilter(Enum):
2.646 +class YearDayFilter(Enum):
2.647 +
2.648 + "A selector of days in years."
2.649 +
2.650 def materialise_items(self, context, start, end, counter, setpos=None, inclusive=False):
2.651 - last_day = monthrange(context[0], context[1])[1]
2.652 - step = scale(1, self.pos)
2.653 + step = scale(1, self.level)
2.654 results = []
2.655 - for value in self.args["values"]:
2.656 - if value < 0:
2.657 - value = last_day + 1 + value
2.658 - current = combine(context, scale(value, self.pos))
2.659 +
2.660 + year_length = get_year_length(context)
2.661 +
2.662 + for value in sort_values(self.args["values"], year_length):
2.663 + current = day_in_year(context, value)
2.664 next = update(current, step)
2.665
2.666 # To support setpos, only current and next bound the search, not
2.667 # the period in addition.
2.668
2.669 - results += self.materialise_item(current, current, next, counter, inclusive=inclusive)
2.670 + results += self.materialise_item(current, current, next, counter,
2.671 + inclusive=inclusive)
2.672
2.673 # Extract selected positions and remove out-of-period instances.
2.674
2.675 if setpos:
2.676 results = self.select_positions(results, setpos)
2.677
2.678 - return self.filter_by_period(results, start, end, inclusive)
2.679 -
2.680 -class YearDayFilter(Enum):
2.681 - def materialise_items(self, context, start, end, counter, setpos=None, inclusive=False):
2.682 - first_day = date(context[0], 1, 1)
2.683 - next_first_day = date(context[0] + 1, 1, 1)
2.684 - year_length = (next_first_day - first_day).days
2.685 - step = scale(1, self.pos)
2.686 - results = []
2.687 - for value in self.args["values"]:
2.688 - if value < 0:
2.689 - value = year_length + 1 + value
2.690 - current = to_tuple(first_day + timedelta(value - 1), 3)
2.691 - next = update(current, step)
2.692 -
2.693 - # To support setpos, only current and next bound the search, not
2.694 - # the period in addition.
2.695 -
2.696 - results += self.materialise_item(current, current, next, counter, inclusive=inclusive)
2.697 -
2.698 - # Extract selected positions and remove out-of-period instances.
2.699 -
2.700 - if setpos:
2.701 - results = self.select_positions(results, setpos)
2.702 -
2.703 - return self.filter_by_period(results, start, end, inclusive)
2.704 + return filter(lambda result:
2.705 + self.filter_by_period(result, start, end, inclusive),
2.706 + results)
2.707
2.708 special_enum_levels = {
2.709 "BYDAY" : WeekDayFilter,
2.710 @@ -781,6 +964,21 @@
2.711 "BYYEARDAY" : YearDayFilter,
2.712 }
2.713
2.714 +class Counter:
2.715 +
2.716 + "A counter to track instance quantities."
2.717 +
2.718 + def __init__(self, limit):
2.719 + self.current = 0
2.720 + self.limit = limit
2.721 +
2.722 + def __iadd__(self, n):
2.723 + self.current += n
2.724 + return self
2.725 +
2.726 + def at_limit(self):
2.727 + return self.limit is not None and self.current >= self.limit
2.728 +
2.729 # Public functions.
2.730
2.731 def connect_selectors(selectors):