1.1 --- a/imiptools/period.py Wed Oct 25 00:10:09 2017 +0200
1.2 +++ b/imiptools/period.py Wed Oct 25 00:24:08 2017 +0200
1.3 @@ -28,6 +28,7 @@
1.4 get_start_of_day, \
1.5 get_tzid, \
1.6 to_timezone, to_utc_datetime
1.7 +from vRecurrence import get_parameters, get_rule
1.8
1.9 def ifnone(x, y):
1.10 if x is None: return y
1.11 @@ -399,6 +400,152 @@
1.12 def make_corrected(self, start, end):
1.13 return self.__class__(start, end, self.tzid, self.origin, self.get_start_attr(), self.get_end_attr())
1.14
1.15 +def make_rule_period(start, duration, attr, tzid):
1.16 +
1.17 + """
1.18 + Make a period for the rule period starting at 'start' with the given
1.19 + 'duration' employing the given datetime 'attr' and 'tzid'.
1.20 + """
1.21 +
1.22 + # Determine the resolution of the period.
1.23 +
1.24 + create = len(start) == 3 and date or datetime
1.25 + start = to_timezone(create(*start), tzid)
1.26 + end = start + duration
1.27 +
1.28 + # Create the period with accompanying metadata based on the main
1.29 + # period and event details.
1.30 +
1.31 + return RecurringPeriod(start, end, tzid, "RRULE", attr)
1.32 +
1.33 +class RulePeriodCollection:
1.34 +
1.35 + "A collection of rule periods."
1.36 +
1.37 + def __init__(self, rrule, main_period, tzid, end, inclusive=False):
1.38 +
1.39 + """
1.40 + Initialise a period collection for the given 'rrule', employing the
1.41 + 'main_period' and 'tzid'.
1.42 +
1.43 + The specified 'end' datetime indicates the end of the window for which
1.44 + periods shall be computed.
1.45 +
1.46 + If 'inclusive' is set to a true value, any period occurring at the 'end'
1.47 + will be included.
1.48 + """
1.49 +
1.50 + self.rrule = rrule
1.51 + self.main_period = main_period
1.52 + self.tzid = tzid
1.53 +
1.54 + parameters = rrule and get_parameters(rrule)
1.55 + until = parameters.get("UNTIL")
1.56 +
1.57 + # Any UNTIL qualifier changes the nature of the end of the collection.
1.58 +
1.59 + if until:
1.60 + attr = main_period.get_start_attr()
1.61 + until_dt = to_timezone(get_datetime(until, attr), tzid)
1.62 + self.end = end and min(until_dt, end) or until_dt
1.63 + self.inclusive = True
1.64 + else:
1.65 + self.end = end
1.66 + self.inclusive = inclusive
1.67 +
1.68 + def __iter__(self):
1.69 +
1.70 + """
1.71 + Obtain period instances, starting from the main period. Since counting
1.72 + must start from the first period, filtering from a start date must be
1.73 + done after the instances have been obtained.
1.74 + """
1.75 +
1.76 + start = self.main_period.get_start()
1.77 + selector = get_rule(start, self.rrule)
1.78 +
1.79 + return RulePeriodIterator(self.main_period, self.tzid,
1.80 + selector.select(start, self.end, self.inclusive))
1.81 +
1.82 +class RulePeriodIterator:
1.83 +
1.84 + "An iterator over rule periods."
1.85 +
1.86 + def __init__(self, main_period, tzid, iterator):
1.87 + self.main_period = main_period
1.88 + self.attr = main_period.get_start_attr()
1.89 + self.duration = main_period.get_duration()
1.90 + self.tzid = tzid
1.91 + self.iterator = iterator
1.92 +
1.93 + def next(self):
1.94 + recurrence_start = self.iterator.next()
1.95 + period = make_rule_period(recurrence_start, self.duration, self.attr, self.tzid)
1.96 +
1.97 + # Use the main period where it occurs.
1.98 +
1.99 + return period == self.main_period and self.main_period or period
1.100 +
1.101 +class MergingIterator:
1.102 +
1.103 + "An iterator merging ordered collections."
1.104 +
1.105 + def __init__(self, iterators):
1.106 +
1.107 + "Initialise an iterator merging 'iterators'."
1.108 +
1.109 + self.current = []
1.110 +
1.111 + # Populate an ordered collection of (value, iterator) pairs by obtaining
1.112 + # the first value from each iterator.
1.113 +
1.114 + for iterator in iterators:
1.115 + t = self.get_next(iterator)
1.116 + if t:
1.117 + self.current.append(t)
1.118 +
1.119 + self.current.sort()
1.120 +
1.121 + def __iter__(self):
1.122 + return self
1.123 +
1.124 + def get_next(self, iterator):
1.125 +
1.126 + """
1.127 + Return a (value, iterator) pair for 'iterator' or None if the iterator
1.128 + has been exhausted.
1.129 + """
1.130 +
1.131 + try:
1.132 + return (iterator.next(), iterator)
1.133 + except StopIteration:
1.134 + return None
1.135 +
1.136 + def next(self):
1.137 +
1.138 + """
1.139 + Return the next value in an ordered sequence, choosing it from one of
1.140 + the available iterators.
1.141 + """
1.142 +
1.143 + if not self.current:
1.144 + raise StopIteration
1.145 +
1.146 + # Obtain the current value and remove the (value, iterator) pair,
1.147 + # pending insertion of a new pair for the iterator.
1.148 +
1.149 + current, iterator = self.current[0]
1.150 + del self.current[0]
1.151 +
1.152 + # Get the next value, if any and insert the value and iterator into the
1.153 + # ordered collection.
1.154 +
1.155 + t = self.get_next(iterator)
1.156 + if t:
1.157 + insort_left(self.current, t)
1.158 +
1.159 + return current
1.160 +
1.161 def get_overlapping(first, second):
1.162
1.163 """