imip-agent

Changeset

1398:5525d39ba8ed
2017-12-01 Paul Boddie raw files shortlog changelog graph Added various functions and methods to facilitate rule selector editing. Reorganised rule period generation to permit modified selectors to be used instead of existing rule property values. client-editing-simplification
imiptools/data.py (file) imiptools/period.py (file) vRecurrence.py (file)
     1.1 --- a/imiptools/data.py	Sat Nov 25 00:11:38 2017 +0100
     1.2 +++ b/imiptools/data.py	Fri Dec 01 23:09:21 2017 +0100
     1.3 @@ -33,7 +33,7 @@
     1.4                               MergingIterator, RulePeriodCollection
     1.5  from itertools import ifilter
     1.6  from vCalendar import iterwrite, parse, ParseError, to_dict, to_node
     1.7 -from vRecurrence import get_parameters
     1.8 +from vRecurrence import get_parameters, get_rule
     1.9  import email.utils
    1.10  
    1.11  try:
    1.12 @@ -1217,31 +1217,32 @@
    1.13      will be included.
    1.14      """
    1.15  
    1.16 -    tzid = obj.get_tzid()
    1.17      rrule = obj.get_value("RRULE")
    1.18  
    1.19 -    # Use localised datetimes.
    1.20 -
    1.21 -    main_period = obj.get_main_period()
    1.22 -
    1.23 -    if not rrule:
    1.24 -        rule_periods = iter([main_period])
    1.25 -
    1.26      # Recurrence rules create multiple instances to be checked.
    1.27      # Conflicts may only be assessed within a period defined by policy
    1.28      # for the agent, with instances outside that period being considered
    1.29      # unchecked.
    1.30  
    1.31 -    elif end or rule_has_end(rrule):
    1.32 +    if not end and not rule_has_end(rrule):
    1.33 +        return iter([])
    1.34  
    1.35 -        # Filter periods using a start point. The end will be handled in the
    1.36 -        # materialisation process.
    1.37 +    tzid = obj.get_tzid()
    1.38 +    main_period = obj.get_main_period()
    1.39 +
    1.40 +    start = main_period.get_start()
    1.41 +    selector = get_rule(start, rrule)
    1.42  
    1.43 -        rule_periods = ifilter(Period(start, None).wraps,
    1.44 -                               RulePeriodCollection(rrule, main_period, tzid,
    1.45 -                                                    end, inclusive))
    1.46 -    else:
    1.47 -        rule_periods = iter([])
    1.48 +    if not selector:
    1.49 +        return iter([main_period])
    1.50 +
    1.51 +    parameters = get_parameters(rrule)
    1.52 +    until = parameters and parameters.has_key("UNTIL") or None
    1.53 +    until = until and to_timezone(get_datetime(until), tzid)
    1.54 +
    1.55 +    rule_periods = get_periods_using_selector(selector, until,
    1.56 +                                              main_period, tzid, start, end,
    1.57 +                                              inclusive)
    1.58  
    1.59      # Add recurrence dates.
    1.60  
    1.61 @@ -1259,6 +1260,17 @@
    1.62  
    1.63      return filter(lambda p, excluded=exdates: p not in excluded, periods)
    1.64  
    1.65 +def get_periods_using_selector(selector, until, main_period, tzid, start, end,
    1.66 +                               inclusive=False):
    1.67 +
    1.68 +    # Filter periods using a start point. The period from the given start
    1.69 +    # until the end of time must wrap each period for that period to be
    1.70 +    # included. The end will be handled in the materialisation process.
    1.71 +
    1.72 +    return ifilter(Period(start, None).wraps,
    1.73 +                   RulePeriodCollection(selector, until, main_period, tzid,
    1.74 +                                        end, inclusive))
    1.75 +
    1.76  def get_main_period(periods):
    1.77  
    1.78      "Return the main period from 'periods' using origin information."
     2.1 --- a/imiptools/period.py	Sat Nov 25 00:11:38 2017 +0100
     2.2 +++ b/imiptools/period.py	Fri Dec 01 23:09:21 2017 +0100
     2.3 @@ -28,7 +28,7 @@
     2.4                              get_start_of_day, \
     2.5                              get_tzid, \
     2.6                              to_timezone, to_utc_datetime
     2.7 -from vRecurrence import get_parameters, get_rule
     2.8 +from vRecurrence import get_selector
     2.9  
    2.10  def ifnone(x, y):
    2.11      if x is None: return y
    2.12 @@ -422,11 +422,11 @@
    2.13  
    2.14      "A collection of rule periods."
    2.15  
    2.16 -    def __init__(self, rrule, main_period, tzid, end, inclusive=False):
    2.17 +    def __init__(self, selector, until, main_period, tzid, end, inclusive=False):
    2.18  
    2.19          """
    2.20 -        Initialise a period collection for the given 'rrule', employing the
    2.21 -        'main_period' and 'tzid'.
    2.22 +        Initialise a period collection for the given 'selectors', limited by any
    2.23 +        'until' datetime, employing the 'main_period' and 'tzid'.
    2.24  
    2.25          The specified 'end' datetime indicates the end of the window for which
    2.26          periods shall be computed.
    2.27 @@ -435,19 +435,14 @@
    2.28          will be included.
    2.29          """
    2.30  
    2.31 -        self.rrule = rrule
    2.32 +        self.selector = selector
    2.33          self.main_period = main_period
    2.34          self.tzid = tzid
    2.35  
    2.36 -        parameters = rrule and get_parameters(rrule)
    2.37 -        until = parameters.get("UNTIL")
    2.38 -
    2.39          # Any UNTIL qualifier changes the nature of the end of the collection.
    2.40  
    2.41          if until:
    2.42 -            attr = main_period.get_start_attr()
    2.43 -            until_dt = to_timezone(get_datetime(until, attr), tzid)
    2.44 -            self.end = end and min(until_dt, end) or until_dt
    2.45 +            self.end = end and min(until, end) or until
    2.46              self.inclusive = True
    2.47          else:
    2.48              self.end = end
    2.49 @@ -462,10 +457,9 @@
    2.50          """
    2.51  
    2.52          start = self.main_period.get_start()
    2.53 -        selector = get_rule(start, self.rrule)
    2.54  
    2.55          return RulePeriodIterator(self.main_period, self.tzid,
    2.56 -                               selector.select(start, self.end, self.inclusive))
    2.57 +                   self.selector.select(start, self.end, self.inclusive))
    2.58  
    2.59  class RulePeriodIterator:
    2.60  
     3.1 --- a/vRecurrence.py	Sat Nov 25 00:11:38 2017 +0100
     3.2 +++ b/vRecurrence.py	Fri Dec 01 23:09:21 2017 +0100
     3.3 @@ -53,6 +53,7 @@
     3.4  """
     3.5  
     3.6  from calendar import monthrange
     3.7 +from collections import OrderedDict
     3.8  from datetime import date, datetime, timedelta
     3.9  import operator
    3.10  
    3.11 @@ -120,8 +121,10 @@
    3.12  
    3.13  # Weekdays: name -> 1-based value
    3.14  
    3.15 -weekdays = {}
    3.16 -for i, weekday in enumerate(["MO", "TU", "WE", "TH", "FR", "SA", "SU"]):
    3.17 +weekday_values = ["MO", "TU", "WE", "TH", "FR", "SA", "SU"]
    3.18 +
    3.19 +weekdays = OrderedDict()
    3.20 +for i, weekday in enumerate(weekday_values):
    3.21      weekdays[weekday] = i + 1
    3.22  
    3.23  # Functions for structuring the recurrences.
    3.24 @@ -140,6 +143,9 @@
    3.25      "Return parameters from the given list of 'values'."
    3.26  
    3.27      d = {}
    3.28 +    if not values:
    3.29 +        return d
    3.30 +
    3.31      for value in values:
    3.32          try:
    3.33              key, value = value.split("=", 1)
    3.34 @@ -158,13 +164,24 @@
    3.35      qualifiers = []
    3.36      frequency = None
    3.37      interval = 1
    3.38 +    keys = set()
    3.39  
    3.40      for value in values:
    3.41 +
    3.42 +        # Ignore qualifiers without values.
    3.43 +
    3.44          try:
    3.45              key, value = value.split("=", 1)
    3.46          except ValueError:
    3.47              continue
    3.48  
    3.49 +        # Ignore duplicate qualifiers.
    3.50 +
    3.51 +        if key in keys:
    3.52 +            continue
    3.53 +
    3.54 +        keys.add(key)
    3.55 +
    3.56          # Accept frequency indicators as qualifiers.
    3.57  
    3.58          if key == "FREQ" and freq.has_key(value):
    3.59 @@ -202,7 +219,7 @@
    3.60      suitable values.
    3.61      """
    3.62  
    3.63 -    # For non-weekday selection, obtain a list of day numbers.
    3.64 +    # For non-weekday selection, obtain a list of numbers.
    3.65  
    3.66      if qualifier != "BYDAY":
    3.67          return map(int, value.split(","))
    3.68 @@ -212,78 +229,185 @@
    3.69      values = []
    3.70  
    3.71      for part in value.split(","):
    3.72 -        weekday = weekdays.get(part[-2:])
    3.73 -        if not weekday:
    3.74 +        index, weekday = part[:-2], part[-2:]
    3.75 +
    3.76 +        weekday_number = weekdays.get(weekday)
    3.77 +        if not weekday_number:
    3.78              continue
    3.79 -        index = part[:-2]
    3.80 +
    3.81          if index:
    3.82              index = int(index)
    3.83          else:
    3.84              index = None
    3.85 -        values.append((weekday, index))
    3.86 +
    3.87 +        values.append((weekday_number, index))
    3.88  
    3.89      return values
    3.90  
    3.91  def order_qualifiers(qualifiers):
    3.92  
    3.93 -    "Return the 'qualifiers' in order of increasing resolution."
    3.94 +    """
    3.95 +    Obtain 'qualifiers' in order of increasing resolution, producing and
    3.96 +    returning selector objects corresponding to the qualifiers.
    3.97 +    """
    3.98  
    3.99      l = []
   3.100 -    max_level = 0
   3.101  
   3.102 -    # Special qualifiers.
   3.103 -
   3.104 -    setpos = None
   3.105 -    count = None
   3.106 +    # Obtain selectors for the qualifiers.
   3.107  
   3.108      for qualifier, args in qualifiers:
   3.109 +        selector = new_selector(qualifier, args)
   3.110 +        l.append(selector)
   3.111  
   3.112 -        # Distinguish between enumerators, used to select particular periods,
   3.113 -        # and frequencies, used to select repeating periods.
   3.114 +    return sort_selectors(l)
   3.115  
   3.116 -        if enum.has_key(qualifier):
   3.117 -            level = enum[qualifier]
   3.118 +def new_selector(qualifier, args=None):
   3.119  
   3.120 -            # Certain enumerators produce their values in a special way.
   3.121 +    "Return a selector for 'qualifier' and 'args'."
   3.122 +
   3.123 +    # Distinguish between enumerators, used to select particular periods,
   3.124 +    # and frequencies, used to select repeating periods.
   3.125  
   3.126 -            if special_enum_levels.has_key(qualifier):
   3.127 -                args["interval"] = 1
   3.128 -                selector = special_enum_levels[qualifier]
   3.129 -            else:
   3.130 -                selector = Enum
   3.131 +    if enum.has_key(qualifier):
   3.132 +        selector = special_enum_levels.get(qualifier, Enum)
   3.133 +        return selector(enum[qualifier], args, qualifier)
   3.134 +
   3.135 +    # Create a selector that must be updated with the maximum resolution.
   3.136  
   3.137 -        elif qualifier == "BYSETPOS":
   3.138 -            setpos = args
   3.139 -            continue
   3.140 +    elif qualifier == "BYSETPOS":
   3.141 +        return PositionSelector(None, args, "BYSETPOS")
   3.142 +
   3.143 +    elif qualifier == "COUNT":
   3.144 +        return LimitSelector(0, args, "COUNT")
   3.145  
   3.146 -        elif qualifier == "COUNT":
   3.147 -            count = args
   3.148 -            continue
   3.149 +    else:
   3.150 +        return Pattern(freq[qualifier], args, qualifier)
   3.151 +
   3.152 +def sort_selectors(selectors):
   3.153  
   3.154 -        else:
   3.155 -            level = freq[qualifier]
   3.156 -            selector = Pattern
   3.157 +    "Sort 'selectors' in order of increasing resolution."
   3.158 +
   3.159 +    if not selectors:
   3.160 +        return selectors
   3.161 +
   3.162 +    max_level = max(map(lambda selector: selector.level or 0, selectors))
   3.163  
   3.164 -        l.append(selector(level, args, qualifier))
   3.165 -        max_level = max(level, max_level)
   3.166 +    # Update the result set selector at the maximum resolution.
   3.167  
   3.168 -    # Add the result set selector at the maximum level of enumeration.
   3.169 +    for selector in selectors:
   3.170 +        if isinstance(selector, PositionSelector):
   3.171 +            selector.level = max_level
   3.172  
   3.173 -    if setpos is not None:
   3.174 -        l.append(PositionSelector(max_level, setpos, "BYSETPOS"))
   3.175 +    selectors.sort(key=selector_sort_key)
   3.176 +    return selectors
   3.177  
   3.178 -    # Add the result set truncator at the top level.
   3.179 +def selector_sort_key(selector):
   3.180  
   3.181 -    if count is not None:
   3.182 -        l.append(LimitSelector(0, count, "COUNT"))
   3.183 +    "Produce a sort key for 'selector'."
   3.184  
   3.185      # Make BYSETPOS sort earlier than the enumeration it modifies.
   3.186      # Other BY... qualifiers sort earlier than selectors at the same resolution
   3.187      # even though such things as "FREQ=HOURLY;BYHOUR=10" do not make much sense.
   3.188  
   3.189 -    l.sort(key=lambda x: (x.level, not x.qualifier.startswith("BY") and 2 or
   3.190 -                                   x.qualifier != "BYSETPOS" and 1 or 0))
   3.191 -    return l
   3.192 +    return (selector.level, not selector.qualifier.startswith("BY") and 2 or
   3.193 +                            selector.qualifier != "BYSETPOS" and 1 or 0)
   3.194 +
   3.195 +def get_value_ranges(qualifier):
   3.196 +
   3.197 +    """
   3.198 +    Return value ranges for 'qualifier'. Each range is either given by a tuple
   3.199 +    indicating the inclusive start and end values or by a list enumerating the
   3.200 +    values.
   3.201 +    """
   3.202 +
   3.203 +    # Provide ranges for the numeric value of each qualifier.
   3.204 +
   3.205 +    if qualifier == "BYMONTH":
   3.206 +        return [(-12, -1), (1, 12)],
   3.207 +    elif qualifier == "BYWEEKNO":
   3.208 +        return [(-53, -1), (1, 53)],
   3.209 +    elif qualifier == "BYYEARDAY":
   3.210 +        return [(-366, -1), (1, 366)],
   3.211 +    elif qualifier == "BYMONTHDAY":
   3.212 +        return [(-31, -1), (1, 31)],
   3.213 +    elif qualifier == "BYHOUR":
   3.214 +        return [(0, 23)],
   3.215 +    elif qualifier == "BYMINUTE":
   3.216 +        return [(0, 59)],
   3.217 +    elif qualifier == "BYSECOND":
   3.218 +        return [(0, 60)],
   3.219 +
   3.220 +    # Provide ranges for the weekday value and index.
   3.221 +
   3.222 +    elif qualifier == "BYDAY":
   3.223 +        return [weekdays], [(-53, -1), (1, 53), None]
   3.224 +
   3.225 +    return None
   3.226 +
   3.227 +def check_values(qualifier, values):
   3.228 +
   3.229 +    """
   3.230 +    Check for 'qualifier' the given 'values', returning checked values that may
   3.231 +    be converted or updated.
   3.232 +    """
   3.233 +
   3.234 +    ranges = get_value_ranges(qualifier)
   3.235 +
   3.236 +    if not ranges:
   3.237 +        return None
   3.238 +
   3.239 +    # Match each value against each range specification.
   3.240 +
   3.241 +    checked = []
   3.242 +
   3.243 +    for v, value_ranges in zip(values, ranges):
   3.244 +
   3.245 +        # Return None if no match occurred for the value.
   3.246 +
   3.247 +        try:
   3.248 +            checked.append(check_value_in_ranges(v, value_ranges))
   3.249 +        except ValueError:
   3.250 +            return None
   3.251 +
   3.252 +    # Return the checked values.
   3.253 +
   3.254 +    return checked
   3.255 +
   3.256 +def check_value_in_ranges(value, value_ranges):
   3.257 +
   3.258 +    """
   3.259 +    Check the given 'value' against the given 'value_ranges'. Return the
   3.260 +    checked value, possibly converted or updated, or raise ValueError if the
   3.261 +    value was not suitable.
   3.262 +    """
   3.263 +
   3.264 +    for value_range in value_ranges:
   3.265 +
   3.266 +        # Test actual ranges.
   3.267 +
   3.268 +        if isinstance(value_range, tuple):
   3.269 +            start, end = value_range
   3.270 +            if start <= value <= end:
   3.271 +                return value
   3.272 +
   3.273 +        # Test enumerations.
   3.274 +
   3.275 +        elif isinstance(value_range, list):
   3.276 +            if value in value_range:
   3.277 +                return value
   3.278 +
   3.279 +        # Test mappings.
   3.280 +
   3.281 +        elif isinstance(value_range, dict):
   3.282 +            if value_range.has_key(value):
   3.283 +                return value_range[value]
   3.284 +
   3.285 +        # Test value matches.
   3.286 +
   3.287 +        elif value == value_range:
   3.288 +            return value
   3.289 +
   3.290 +    raise ValueError, value
   3.291  
   3.292  def get_datetime_structure(datetime):
   3.293  
   3.294 @@ -304,10 +428,10 @@
   3.295  
   3.296      return l
   3.297  
   3.298 -def combine_datetime_with_qualifiers(datetime, qualifiers):
   3.299 +def combine_datetime_with_selectors(datetime, selectors):
   3.300  
   3.301      """
   3.302 -    Combine 'datetime' with 'qualifiers' to produce a structure for recurrence
   3.303 +    Combine 'datetime' with 'selectors' to produce a structure for recurrence
   3.304      production.
   3.305  
   3.306      Initial datetime values appearing at broader resolutions than any qualifiers
   3.307 @@ -328,19 +452,19 @@
   3.308      """
   3.309  
   3.310      iter_dt = iter(get_datetime_structure(datetime))
   3.311 -    iter_q = iter(order_qualifiers(qualifiers))
   3.312 +    iter_sel = iter(selectors)
   3.313  
   3.314      l = []
   3.315  
   3.316      from_dt = get_next(iter_dt)
   3.317 -    from_q = get_next(iter_q)
   3.318 -    have_q = False
   3.319 +    from_sel = get_next(iter_sel)
   3.320 +    have_sel = False
   3.321  
   3.322      # Consume from both lists, merging entries.
   3.323  
   3.324 -    while from_dt and from_q:
   3.325 +    while from_dt and from_sel:
   3.326          _level = from_dt.level
   3.327 -        level = from_q.level
   3.328 +        level = from_sel.level
   3.329  
   3.330          # Datetime value at wider resolution.
   3.331  
   3.332 @@ -350,13 +474,13 @@
   3.333          # Qualifier at wider or same resolution as datetime value.
   3.334  
   3.335          else:
   3.336 -            if not have_q:
   3.337 -                add_initial_qualifier(from_q, level, l)
   3.338 -                have_q = True
   3.339 +            if not have_sel:
   3.340 +                add_initial_selector(from_sel, level, l)
   3.341 +                have_sel = True
   3.342  
   3.343              # Add the qualifier to the combined list.
   3.344  
   3.345 -            l.append(from_q)
   3.346 +            l.append(from_sel)
   3.347  
   3.348              # Datetime value at same resolution.
   3.349  
   3.350 @@ -365,7 +489,7 @@
   3.351  
   3.352              # Get the next qualifier.
   3.353  
   3.354 -            from_q = get_next(iter_q)
   3.355 +            from_sel = get_next(iter_sel)
   3.356  
   3.357      # Complete the list by adding remaining datetime enumerators.
   3.358  
   3.359 @@ -382,30 +506,30 @@
   3.360  
   3.361      # Complete the list by adding remaining qualifiers.
   3.362  
   3.363 -    while from_q:
   3.364 -        if not have_q:
   3.365 -            add_initial_qualifier(from_q, level, l)
   3.366 -            have_q = True
   3.367 +    while from_sel:
   3.368 +        if not have_sel:
   3.369 +            add_initial_selector(from_sel, level, l)
   3.370 +            have_sel = True
   3.371  
   3.372          # Add the qualifier to the combined list.
   3.373  
   3.374 -        l.append(from_q)
   3.375 +        l.append(from_sel)
   3.376  
   3.377          # Get the next qualifier.
   3.378  
   3.379 -        from_q = get_next(iter_q)
   3.380 +        from_sel = get_next(iter_sel)
   3.381  
   3.382      return l
   3.383  
   3.384 -def add_initial_qualifier(from_q, level, l):
   3.385 +def add_initial_selector(from_sel, level, l):
   3.386  
   3.387      """
   3.388 -    Take the first qualifier 'from_q' at the given resolution 'level', using it
   3.389 -    to create an initial qualifier, adding it to the combined list 'l' if
   3.390 +    Take the first selector 'from_sel' at the given resolution 'level', using it
   3.391 +    to create an initial selector, adding it to the combined list 'l' if
   3.392      required.
   3.393      """
   3.394  
   3.395 -    if isinstance(from_q, Enum) and level > 0:
   3.396 +    if isinstance(from_sel, Enum) and level > 0:
   3.397          repeat = Pattern(level - 1, {"interval" : 1}, None)
   3.398          l.append(repeat)
   3.399  
   3.400 @@ -640,9 +764,9 @@
   3.401  
   3.402      """
   3.403      Return a sorted copy of the given 'values', each having the form (weekday
   3.404 -    number, instance number) using 'weekdays' to define the ordering of the
   3.405 -    weekday numbers and 'limit' to determine the positions of negative instance
   3.406 -    numbers.
   3.407 +    number, instance number), where 'first_day' indicates the start of the
   3.408 +    period in which these values apply, and where 'last_day' indicates the end
   3.409 +    of the period.
   3.410      """
   3.411  
   3.412      weekdays = get_ordered_weekdays(first_day)
   3.413 @@ -704,7 +828,7 @@
   3.414          """
   3.415  
   3.416          self.level = level
   3.417 -        self.args = args
   3.418 +        self.args = args or {}
   3.419          self.qualifier = qualifier
   3.420          self.selecting = selecting
   3.421          self.first = first
   3.422 @@ -739,6 +863,9 @@
   3.423  
   3.424          return list(self.select(start, end, inclusive))
   3.425  
   3.426 +    def set_values(self, values):
   3.427 +        self.args["values"] = values
   3.428 +
   3.429  class Pattern(Selector):
   3.430  
   3.431      "A selector of time periods according to a repeating pattern."
   3.432 @@ -747,7 +874,7 @@
   3.433          Selector.__init__(self, level, args, qualifier, selecting, first)
   3.434  
   3.435          multiple = get_multiple(self.qualifier)
   3.436 -        interval = self.args.get("interval", 1)
   3.437 +        interval = self.get_interval()
   3.438  
   3.439          # Define the step between result periods.
   3.440  
   3.441 @@ -781,6 +908,12 @@
   3.442          return PatternIterator(self, current, start, end, inclusive,
   3.443                                 self.step, self.unit_step)
   3.444  
   3.445 +    def get_interval(self):
   3.446 +        return self.args.get("interval", 1)
   3.447 +
   3.448 +    def set_interval(self, interval):
   3.449 +        self.args["interval"] = interval
   3.450 +
   3.451  class WeekDayFilter(Selector):
   3.452  
   3.453      "A selector of instances specified in terms of day numbers."
   3.454 @@ -807,12 +940,31 @@
   3.455  
   3.456          else:
   3.457              current = context
   3.458 -            values = [value for (value, index) in self.args["values"]]
   3.459 -            return WeekDayIterator(self, current, start, end, inclusive, self.step, values)
   3.460 +            return WeekDayIterator(self, current, start, end, inclusive, self.step,
   3.461 +                                   self.get_weekdays())
   3.462  
   3.463          current = first_day
   3.464          values = sort_weekdays(self.args["values"], first_day, last_day)
   3.465 -        return WeekDayGeneralIterator(self, current, start, end, inclusive, self.step, values)
   3.466 +        return WeekDayGeneralIterator(self, current, start, end, inclusive,
   3.467 +                                      self.step, values)
   3.468 +
   3.469 +    def get_values(self):
   3.470 +
   3.471 +        """
   3.472 +        Return a sequence of (value, index) tuples selecting weekdays in the
   3.473 +        applicable period. Each value is a 1-based index representing a weekday.
   3.474 +        """
   3.475 +
   3.476 +        return self.args["values"]
   3.477 +
   3.478 +    def get_weekdays(self):
   3.479 +
   3.480 +        "Return only the 1-based weekday indexes."
   3.481 +
   3.482 +        values = []
   3.483 +        for value, index in self.args["values"]:
   3.484 +            values.append(value)
   3.485 +        return values
   3.486  
   3.487  class Enum(Selector):
   3.488  
   3.489 @@ -823,8 +975,11 @@
   3.490          self.step = scale(1, level)
   3.491  
   3.492      def materialise_items(self, context, start, end, inclusive=False):
   3.493 -        values = sort_values(self.args["values"])
   3.494 -        return EnumIterator(self, context, start, end, inclusive, self.step, values)
   3.495 +        return EnumIterator(self, context, start, end, inclusive, self.step,
   3.496 +                            self.get_values())
   3.497 +
   3.498 +    def get_values(self, limit=None):
   3.499 +        return sort_values(self.args["values"], limit)
   3.500  
   3.501  class MonthDayFilter(Enum):
   3.502  
   3.503 @@ -832,8 +987,8 @@
   3.504  
   3.505      def materialise_items(self, context, start, end, inclusive=False):
   3.506          last_day = end_of_month(context)[2]
   3.507 -        values = sort_values(self.args["values"], last_day)
   3.508 -        return EnumIterator(self, context, start, end, inclusive, self.step, values)
   3.509 +        return EnumIterator(self, context, start, end, inclusive, self.step,
   3.510 +                            self.get_values(last_day))
   3.511  
   3.512  class YearDayFilter(Enum):
   3.513  
   3.514 @@ -842,22 +997,21 @@
   3.515      def materialise_items(self, context, start, end, inclusive=False):
   3.516          first_day = start_of_year(context)
   3.517          year_length = get_year_length(context)
   3.518 -        values = sort_values(self.args["values"], year_length)
   3.519 -        return YearDayFilterIterator(self, first_day, start, end, inclusive, self.step, values)
   3.520 -
   3.521 -special_enum_levels = {
   3.522 -    "BYDAY" : WeekDayFilter,
   3.523 -    "BYMONTHDAY" : MonthDayFilter,
   3.524 -    "BYYEARDAY" : YearDayFilter,
   3.525 -    }
   3.526 +        return YearDayFilterIterator(self, first_day, start, end, inclusive, self.step,
   3.527 +                                     self.get_values(year_length))
   3.528  
   3.529  class LimitSelector(Selector):
   3.530  
   3.531      "A result set limit selector."
   3.532  
   3.533      def materialise_items(self, context, start, end, inclusive=False):
   3.534 -        limit = self.args["values"][0]
   3.535 -        return LimitIterator(self, context, start, end, inclusive, limit)
   3.536 +        return LimitIterator(self, context, start, end, inclusive, self.get_limit())
   3.537 +
   3.538 +    def get_limit(self):
   3.539 +        return self.args["values"][0]
   3.540 +
   3.541 +    def set_limit(self, limit):
   3.542 +        self.args["values"] = [limit]
   3.543  
   3.544  class PositionSelector(Selector):
   3.545  
   3.546 @@ -868,8 +1022,20 @@
   3.547          self.step = scale(1, level)
   3.548  
   3.549      def materialise_items(self, context, start, end, inclusive=False):
   3.550 -        values = convert_positions(sort_values(self.args["values"]))
   3.551 -        return PositionIterator(self, context, start, end, inclusive, self.step, values)
   3.552 +        return PositionIterator(self, context, start, end, inclusive, self.step,
   3.553 +                                self.get_positions())
   3.554 +
   3.555 +    def get_positions(self):
   3.556 +        return convert_positions(sort_values(self.args["values"]))
   3.557 +
   3.558 +    def set_positions(self, positions):
   3.559 +        self.args["values"] = positions
   3.560 +
   3.561 +special_enum_levels = {
   3.562 +    "BYDAY" : WeekDayFilter,
   3.563 +    "BYMONTHDAY" : MonthDayFilter,
   3.564 +    "BYYEARDAY" : YearDayFilter,
   3.565 +    }
   3.566  
   3.567  # Iterator classes.
   3.568  
   3.569 @@ -1207,8 +1373,6 @@
   3.570                  else:
   3.571                      raise
   3.572  
   3.573 -# Public functions.
   3.574 -
   3.575  def connect_selectors(selectors):
   3.576  
   3.577      """
   3.578 @@ -1233,15 +1397,7 @@
   3.579  
   3.580      return selectors[0]
   3.581  
   3.582 -def get_selector(dt, qualifiers):
   3.583 -
   3.584 -    """
   3.585 -    Combine the initial datetime 'dt' with the given 'qualifiers', returning an
   3.586 -    object that can be used to select recurrences described by the 'qualifiers'.
   3.587 -    """
   3.588 -
   3.589 -    dt = to_tuple(dt)
   3.590 -    return connect_selectors(combine_datetime_with_qualifiers(dt, qualifiers))
   3.591 +# Public functions.
   3.592  
   3.593  def get_rule(dt, rule):
   3.594  
   3.595 @@ -1251,9 +1407,28 @@
   3.596      selector object.
   3.597      """
   3.598  
   3.599 +    selectors = get_selectors_for_rule(rule)
   3.600 +    return get_selector(dt, selectors)
   3.601 +
   3.602 +def get_selector(dt, selectors):
   3.603 +
   3.604 +    """
   3.605 +    Combine the initial datetime 'dt' with the given 'selectors', returning an
   3.606 +    object that can be used to select recurrences described by the 'selectors'.
   3.607 +    """
   3.608 +
   3.609 +    dt = to_tuple(dt)
   3.610 +    return connect_selectors(combine_datetime_with_selectors(dt, selectors))
   3.611 +
   3.612 +def get_selectors_for_rule(rule):
   3.613 +
   3.614 +    """
   3.615 +    Return a list of selectors implementing 'rule', useful for "explaining" how
   3.616 +    a rule works.
   3.617 +    """
   3.618 +
   3.619      if not isinstance(rule, tuple):
   3.620 -        rule = rule.split(";")
   3.621 -    qualifiers = get_qualifiers(rule)
   3.622 -    return get_selector(dt, qualifiers)
   3.623 +        rule = (rule or "").split(";")
   3.624 +    return order_qualifiers(get_qualifiers(rule))
   3.625  
   3.626  # vim: tabstop=4 expandtab shiftwidth=4