# HG changeset patch # User Paul Boddie # Date 1512423342 -3600 # Node ID 94338c98305bf4bd35566f49505d9015dfa34889 # Parent 8e216b3055d7559113b696ac3cb16406173c4363 Prevent month-level and monthday updates from introducing bad datetime values. diff -r 8e216b3055d7 -r 94338c98305b vRecurrence.py --- a/vRecurrence.py Sun Dec 03 22:56:53 2017 +0100 +++ b/vRecurrence.py Mon Dec 04 22:35:42 2017 +0100 @@ -681,19 +681,47 @@ days = (date(*last_day) - weekday0).days return days / 7 + 1 -def precision(t, level, value=None): +def constrain_to_month(t): + + "Return 't' corrected to ensure that the day number is within the month." + + year, month, day = t[:3] + time = t[3:] + max_day = monthrange(year, month)[1] + day = min(max_day, day) + return tuple((year, month) + (day,) + time) + +def within_month(t): + + """ + Return whether 't' is within the permissible day range of the given month, + if 't' describes a day or datetime. Otherwise, return a true value for years + and months. + """ + + if len(t) < 3: + return True + + year, month, day = t[:3] + max_day = monthrange(year, month)[1] + return day <= max_day + +def limit_value(t, level): + + "Return 't' trimmed in resolution to the indicated resolution 'level'." + + pos = positions[level] + return t[:pos + 1] + +def make_value(t, level, value): """ Return 't' trimmed in resolution to the indicated resolution 'level', - setting 'value' at the given resolution if indicated. + extended by setting 'value' at the given resolution. """ pos = positions[level] - - if value is None: - return t[:pos + 1] - else: - return t[:pos] + (value,) + return t[:pos] + (value,) def scale(interval, level): @@ -733,18 +761,17 @@ # Dates and datetimes. else: - updated_for_months = update(t, step[:2]) + updated_for_months = constrain_to_month(update(t, step[:2])) + d = datetime(*updated_for_months) # Dates only. if i == 3: - d = datetime(*updated_for_months) s = timedelta(step[2]) # Datetimes. else: - d = datetime(*updated_for_months) s = timedelta(step[2], get_seconds(step)) return to_tuple(d + s)[:len(t)] @@ -958,13 +985,13 @@ # qualifier in the selection chain. if self.first: - current = precision(context, self.level) + current = limit_value(context, self.level) # Otherwise, obtain the first value at this resolution within the # context period. else: - current = precision(context, self.level, firstvalues[self.level]) + current = make_value(context, self.level, firstvalues[self.level]) return PatternIterator(self, current, start, end, inclusive, self.step, self.unit_step) @@ -1307,7 +1334,12 @@ "Return the period indicated by the current value." - return precision(self.base, self.selector.level, self.value) + t = make_value(self.base, self.selector.level, self.value) + + if not within_month(t): + raise StopIteration + + return t def next(self): @@ -1320,15 +1352,15 @@ if not self.selector.selecting or self.value is None: self.value = self.values.next() - # Select a period for each value at the current resolution. - - self.current = self.get_selected_period() - next = update(self.current, self.step) + try: + # Select a period for each value at the current resolution. - # To support setpos, only current and next bound the search, not - # the period in addition. + self.current = self.get_selected_period() + next = update(self.current, self.step) - try: + # To support setpos, only current and next bound the search, not + # the period in addition. + return self.next_item(self.current, next) # Move on to the next instance. @@ -1350,7 +1382,7 @@ value, index = self.value offset = timedelta(7 * (index - 1)) weekday0 = get_first_day(self.base, value) - return precision(to_tuple(weekday0 + offset), DAYS) + return limit_value(to_tuple(weekday0 + offset), DAYS) class YearDayFilterIterator(EnumIterator): @@ -1361,7 +1393,7 @@ "Return the day indicated by the current day value." offset = timedelta(self.value - 1) - return precision(to_tuple(date(*self.base) + offset), DAYS) + return limit_value(to_tuple(date(*self.base) + offset), DAYS) class LimitIterator(SelectorIterator):