1.1 --- a/imiptools/data.py Wed Oct 25 00:09:19 2017 +0200
1.2 +++ b/imiptools/data.py Wed Oct 25 00:18:24 2017 +0200
1.3 @@ -19,7 +19,6 @@
1.4 this program. If not, see <http://www.gnu.org/licenses/>.
1.5 """
1.6
1.7 -from bisect import bisect_left, insort_left
1.8 from datetime import date, datetime, timedelta
1.9 from email.mime.text import MIMEText
1.10 from imiptools.dates import format_datetime, get_datetime, \
1.11 @@ -30,10 +29,11 @@
1.12 get_time, get_timestamp, get_tzid, to_datetime, \
1.13 to_timezone, to_utc_datetime
1.14 from imiptools.freebusy import FreeBusyPeriod
1.15 -from imiptools.period import Period, RecurringPeriod
1.16 +from imiptools.period import Period, RecurringPeriod, \
1.17 + MergingIterator, RulePeriodCollection
1.18 from itertools import ifilter
1.19 from vCalendar import iterwrite, parse, ParseError, to_dict, to_node
1.20 -from vRecurrence import get_parameters, get_rule
1.21 +from vRecurrence import get_parameters
1.22 import email.utils
1.23
1.24 try:
1.25 @@ -1077,154 +1077,6 @@
1.26
1.27 return delegators
1.28
1.29 -def rule_has_end(rrule):
1.30 -
1.31 - "Return whether 'rrule' defines an end."
1.32 -
1.33 - parameters = rrule and get_parameters(rrule)
1.34 - return parameters and parameters.has_key("UNTIL") or parameters.has_key("COUNT")
1.35 -
1.36 -def make_rule_period(start, duration, attr, tzid):
1.37 -
1.38 - """
1.39 - Make a period for the rule period starting at 'start' with the given
1.40 - 'duration' employing the given datetime 'attr' and 'tzid'.
1.41 - """
1.42 -
1.43 - # Determine the resolution of the period.
1.44 -
1.45 - create = len(start) == 3 and date or datetime
1.46 - start = to_timezone(create(*start), tzid)
1.47 - end = start + duration
1.48 -
1.49 - # Create the period with accompanying metadata based on the main
1.50 - # period and event details.
1.51 -
1.52 - return RecurringPeriod(start, end, tzid, "RRULE", attr)
1.53 -
1.54 -class RulePeriodCollection:
1.55 -
1.56 - "A collection of rule periods."
1.57 -
1.58 - def __init__(self, rrule, main_period, tzid, end, inclusive=False):
1.59 -
1.60 - """
1.61 - Initialise a period collection for the given 'rrule', employing the
1.62 - 'main_period' and 'tzid'.
1.63 -
1.64 - The specified 'end' datetime indicates the end of the window for which
1.65 - periods shall be computed.
1.66 -
1.67 - If 'inclusive' is set to a true value, any period occurring at the 'end'
1.68 - will be included.
1.69 - """
1.70 -
1.71 - self.rrule = rrule
1.72 - self.main_period = main_period
1.73 - self.tzid = tzid
1.74 -
1.75 - parameters = rrule and get_parameters(rrule)
1.76 - until = parameters.get("UNTIL")
1.77 -
1.78 - # Any UNTIL qualifier changes the nature of the end of the collection.
1.79 -
1.80 - if until:
1.81 - attr = main_period.get_start_attr()
1.82 - until_dt = to_timezone(get_datetime(until, attr), tzid)
1.83 - self.end = end and min(until_dt, end) or until_dt
1.84 - self.inclusive = True
1.85 - else:
1.86 - self.end = end
1.87 - self.inclusive = inclusive
1.88 -
1.89 - def __iter__(self):
1.90 -
1.91 - """
1.92 - Obtain period instances, starting from the main period. Since counting
1.93 - must start from the first period, filtering from a start date must be
1.94 - done after the instances have been obtained.
1.95 - """
1.96 -
1.97 - start = self.main_period.get_start()
1.98 - selector = get_rule(start, self.rrule)
1.99 -
1.100 - return RulePeriodIterator(self.main_period, self.tzid,
1.101 - selector.select(start, self.end, self.inclusive))
1.102 -
1.103 -class RulePeriodIterator:
1.104 -
1.105 - "An iterator over rule periods."
1.106 -
1.107 - def __init__(self, main_period, tzid, iterator):
1.108 - self.attr = main_period.get_start_attr()
1.109 - self.duration = main_period.get_duration()
1.110 - self.tzid = tzid
1.111 - self.iterator = iterator
1.112 -
1.113 - def next(self):
1.114 - recurrence_start = self.iterator.next()
1.115 - return make_rule_period(recurrence_start, self.duration, self.attr, self.tzid)
1.116 -
1.117 -class MergingIterator:
1.118 -
1.119 - "An iterator merging ordered collections."
1.120 -
1.121 - def __init__(self, iterators):
1.122 -
1.123 - "Initialise an iterator merging 'iterators'."
1.124 -
1.125 - self.current = []
1.126 -
1.127 - # Populate an ordered collection of (value, iterator) pairs by obtaining
1.128 - # the first value from each iterator.
1.129 -
1.130 - for iterator in iterators:
1.131 - t = self.get_next(iterator)
1.132 - if t:
1.133 - self.current.append(t)
1.134 -
1.135 - self.current.sort()
1.136 -
1.137 - def __iter__(self):
1.138 - return self
1.139 -
1.140 - def get_next(self, iterator):
1.141 -
1.142 - """
1.143 - Return a (value, iterator) pair for 'iterator' or None if the iterator
1.144 - has been exhausted.
1.145 - """
1.146 -
1.147 - try:
1.148 - return (iterator.next(), iterator)
1.149 - except StopIteration:
1.150 - return None
1.151 -
1.152 - def next(self):
1.153 -
1.154 - """
1.155 - Return the next value in an ordered sequence, choosing it from one of
1.156 - the available iterators.
1.157 - """
1.158 -
1.159 - if not self.current:
1.160 - raise StopIteration
1.161 -
1.162 - # Obtain the current value and remove the (value, iterator) pair,
1.163 - # pending insertion of a new pair for the iterator.
1.164 -
1.165 - current, iterator = self.current[0]
1.166 - del self.current[0]
1.167 -
1.168 - # Get the next value, if any and insert the value and iterator into the
1.169 - # ordered collection.
1.170 -
1.171 - t = self.get_next(iterator)
1.172 - if t:
1.173 - insort_left(self.current, t)
1.174 -
1.175 - return current
1.176 -
1.177 def get_periods(obj, tzid, start=None, end=None, inclusive=False):
1.178
1.179 """
1.180 @@ -1321,6 +1173,13 @@
1.181
1.182 return to_timezone(start or datetime.now(), tzid) + timedelta(days)
1.183
1.184 +def rule_has_end(rrule):
1.185 +
1.186 + "Return whether 'rrule' defines an end."
1.187 +
1.188 + parameters = rrule and get_parameters(rrule)
1.189 + return parameters and parameters.has_key("UNTIL") or parameters.has_key("COUNT")
1.190 +
1.191 def update_attendees_with_delegates(stored_attendees, attendees):
1.192
1.193 """
2.1 --- a/imiptools/period.py Wed Oct 25 00:09:19 2017 +0200
2.2 +++ b/imiptools/period.py Wed Oct 25 00:18:24 2017 +0200
2.3 @@ -28,6 +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
2.9 def ifnone(x, y):
2.10 if x is None: return y
2.11 @@ -381,6 +382,147 @@
2.12 def make_corrected(self, start, end):
2.13 return self.__class__(start, end, self.tzid, self.origin, self.get_start_attr(), self.get_end_attr())
2.14
2.15 +def make_rule_period(start, duration, attr, tzid):
2.16 +
2.17 + """
2.18 + Make a period for the rule period starting at 'start' with the given
2.19 + 'duration' employing the given datetime 'attr' and 'tzid'.
2.20 + """
2.21 +
2.22 + # Determine the resolution of the period.
2.23 +
2.24 + create = len(start) == 3 and date or datetime
2.25 + start = to_timezone(create(*start), tzid)
2.26 + end = start + duration
2.27 +
2.28 + # Create the period with accompanying metadata based on the main
2.29 + # period and event details.
2.30 +
2.31 + return RecurringPeriod(start, end, tzid, "RRULE", attr)
2.32 +
2.33 +class RulePeriodCollection:
2.34 +
2.35 + "A collection of rule periods."
2.36 +
2.37 + def __init__(self, rrule, main_period, tzid, end, inclusive=False):
2.38 +
2.39 + """
2.40 + Initialise a period collection for the given 'rrule', employing the
2.41 + 'main_period' and 'tzid'.
2.42 +
2.43 + The specified 'end' datetime indicates the end of the window for which
2.44 + periods shall be computed.
2.45 +
2.46 + If 'inclusive' is set to a true value, any period occurring at the 'end'
2.47 + will be included.
2.48 + """
2.49 +
2.50 + self.rrule = rrule
2.51 + self.main_period = main_period
2.52 + self.tzid = tzid
2.53 +
2.54 + parameters = rrule and get_parameters(rrule)
2.55 + until = parameters.get("UNTIL")
2.56 +
2.57 + # Any UNTIL qualifier changes the nature of the end of the collection.
2.58 +
2.59 + if until:
2.60 + attr = main_period.get_start_attr()
2.61 + until_dt = to_timezone(get_datetime(until, attr), tzid)
2.62 + self.end = end and min(until_dt, end) or until_dt
2.63 + self.inclusive = True
2.64 + else:
2.65 + self.end = end
2.66 + self.inclusive = inclusive
2.67 +
2.68 + def __iter__(self):
2.69 +
2.70 + """
2.71 + Obtain period instances, starting from the main period. Since counting
2.72 + must start from the first period, filtering from a start date must be
2.73 + done after the instances have been obtained.
2.74 + """
2.75 +
2.76 + start = self.main_period.get_start()
2.77 + selector = get_rule(start, self.rrule)
2.78 +
2.79 + return RulePeriodIterator(self.main_period, self.tzid,
2.80 + selector.select(start, self.end, self.inclusive))
2.81 +
2.82 +class RulePeriodIterator:
2.83 +
2.84 + "An iterator over rule periods."
2.85 +
2.86 + def __init__(self, main_period, tzid, iterator):
2.87 + self.attr = main_period.get_start_attr()
2.88 + self.duration = main_period.get_duration()
2.89 + self.tzid = tzid
2.90 + self.iterator = iterator
2.91 +
2.92 + def next(self):
2.93 + recurrence_start = self.iterator.next()
2.94 + return make_rule_period(recurrence_start, self.duration, self.attr, self.tzid)
2.95 +
2.96 +class MergingIterator:
2.97 +
2.98 + "An iterator merging ordered collections."
2.99 +
2.100 + def __init__(self, iterators):
2.101 +
2.102 + "Initialise an iterator merging 'iterators'."
2.103 +
2.104 + self.current = []
2.105 +
2.106 + # Populate an ordered collection of (value, iterator) pairs by obtaining
2.107 + # the first value from each iterator.
2.108 +
2.109 + for iterator in iterators:
2.110 + t = self.get_next(iterator)
2.111 + if t:
2.112 + self.current.append(t)
2.113 +
2.114 + self.current.sort()
2.115 +
2.116 + def __iter__(self):
2.117 + return self
2.118 +
2.119 + def get_next(self, iterator):
2.120 +
2.121 + """
2.122 + Return a (value, iterator) pair for 'iterator' or None if the iterator
2.123 + has been exhausted.
2.124 + """
2.125 +
2.126 + try:
2.127 + return (iterator.next(), iterator)
2.128 + except StopIteration:
2.129 + return None
2.130 +
2.131 + def next(self):
2.132 +
2.133 + """
2.134 + Return the next value in an ordered sequence, choosing it from one of
2.135 + the available iterators.
2.136 + """
2.137 +
2.138 + if not self.current:
2.139 + raise StopIteration
2.140 +
2.141 + # Obtain the current value and remove the (value, iterator) pair,
2.142 + # pending insertion of a new pair for the iterator.
2.143 +
2.144 + current, iterator = self.current[0]
2.145 + del self.current[0]
2.146 +
2.147 + # Get the next value, if any and insert the value and iterator into the
2.148 + # ordered collection.
2.149 +
2.150 + t = self.get_next(iterator)
2.151 + if t:
2.152 + insort_left(self.current, t)
2.153 +
2.154 + return current
2.155 +
2.156 def get_overlapping(first, second):
2.157
2.158 """