1.1 --- a/imiptools/data.py Wed Oct 25 00:10:09 2017 +0200
1.2 +++ b/imiptools/data.py Wed Oct 25 00:24:08 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 @@ -1202,159 +1202,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.main_period = main_period
1.109 - self.attr = main_period.get_start_attr()
1.110 - self.duration = main_period.get_duration()
1.111 - self.tzid = tzid
1.112 - self.iterator = iterator
1.113 -
1.114 - def next(self):
1.115 - recurrence_start = self.iterator.next()
1.116 - period = make_rule_period(recurrence_start, self.duration, self.attr, self.tzid)
1.117 -
1.118 - # Use the main period where it occurs.
1.119 -
1.120 - return period == self.main_period and self.main_period or period
1.121 -
1.122 -class MergingIterator:
1.123 -
1.124 - "An iterator merging ordered collections."
1.125 -
1.126 - def __init__(self, iterators):
1.127 -
1.128 - "Initialise an iterator merging 'iterators'."
1.129 -
1.130 - self.current = []
1.131 -
1.132 - # Populate an ordered collection of (value, iterator) pairs by obtaining
1.133 - # the first value from each iterator.
1.134 -
1.135 - for iterator in iterators:
1.136 - t = self.get_next(iterator)
1.137 - if t:
1.138 - self.current.append(t)
1.139 -
1.140 - self.current.sort()
1.141 -
1.142 - def __iter__(self):
1.143 - return self
1.144 -
1.145 - def get_next(self, iterator):
1.146 -
1.147 - """
1.148 - Return a (value, iterator) pair for 'iterator' or None if the iterator
1.149 - has been exhausted.
1.150 - """
1.151 -
1.152 - try:
1.153 - return (iterator.next(), iterator)
1.154 - except StopIteration:
1.155 - return None
1.156 -
1.157 - def next(self):
1.158 -
1.159 - """
1.160 - Return the next value in an ordered sequence, choosing it from one of
1.161 - the available iterators.
1.162 - """
1.163 -
1.164 - if not self.current:
1.165 - raise StopIteration
1.166 -
1.167 - # Obtain the current value and remove the (value, iterator) pair,
1.168 - # pending insertion of a new pair for the iterator.
1.169 -
1.170 - current, iterator = self.current[0]
1.171 - del self.current[0]
1.172 -
1.173 - # Get the next value, if any and insert the value and iterator into the
1.174 - # ordered collection.
1.175 -
1.176 - t = self.get_next(iterator)
1.177 - if t:
1.178 - insort_left(self.current, t)
1.179 -
1.180 - return current
1.181 -
1.182 def get_periods(obj, start=None, end=None, inclusive=False):
1.183
1.184 """
1.185 @@ -1408,7 +1255,7 @@
1.186
1.187 # Exclude exception dates.
1.188
1.189 - exdates = set(obj.get_date_value_item_periods("EXDATE", tzid) or [])
1.190 + exdates = set(obj.get_date_value_item_periods("EXDATE") or [])
1.191
1.192 return filter(lambda p, excluded=exdates: p not in excluded, periods)
1.193
1.194 @@ -1466,6 +1313,13 @@
1.195
1.196 return to_timezone(start or datetime.now(), tzid) + timedelta(days)
1.197
1.198 +def rule_has_end(rrule):
1.199 +
1.200 + "Return whether 'rrule' defines an end."
1.201 +
1.202 + parameters = rrule and get_parameters(rrule)
1.203 + return parameters and parameters.has_key("UNTIL") or parameters.has_key("COUNT")
1.204 +
1.205 def update_attendees_with_delegates(stored_attendees, attendees):
1.206
1.207 """
2.1 --- a/imiptools/period.py Wed Oct 25 00:10:09 2017 +0200
2.2 +++ b/imiptools/period.py Wed Oct 25 00:24:08 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 @@ -399,6 +400,152 @@
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.main_period = main_period
2.88 + self.attr = main_period.get_start_attr()
2.89 + self.duration = main_period.get_duration()
2.90 + self.tzid = tzid
2.91 + self.iterator = iterator
2.92 +
2.93 + def next(self):
2.94 + recurrence_start = self.iterator.next()
2.95 + period = make_rule_period(recurrence_start, self.duration, self.attr, self.tzid)
2.96 +
2.97 + # Use the main period where it occurs.
2.98 +
2.99 + return period == self.main_period and self.main_period or period
2.100 +
2.101 +class MergingIterator:
2.102 +
2.103 + "An iterator merging ordered collections."
2.104 +
2.105 + def __init__(self, iterators):
2.106 +
2.107 + "Initialise an iterator merging 'iterators'."
2.108 +
2.109 + self.current = []
2.110 +
2.111 + # Populate an ordered collection of (value, iterator) pairs by obtaining
2.112 + # the first value from each iterator.
2.113 +
2.114 + for iterator in iterators:
2.115 + t = self.get_next(iterator)
2.116 + if t:
2.117 + self.current.append(t)
2.118 +
2.119 + self.current.sort()
2.120 +
2.121 + def __iter__(self):
2.122 + return self
2.123 +
2.124 + def get_next(self, iterator):
2.125 +
2.126 + """
2.127 + Return a (value, iterator) pair for 'iterator' or None if the iterator
2.128 + has been exhausted.
2.129 + """
2.130 +
2.131 + try:
2.132 + return (iterator.next(), iterator)
2.133 + except StopIteration:
2.134 + return None
2.135 +
2.136 + def next(self):
2.137 +
2.138 + """
2.139 + Return the next value in an ordered sequence, choosing it from one of
2.140 + the available iterators.
2.141 + """
2.142 +
2.143 + if not self.current:
2.144 + raise StopIteration
2.145 +
2.146 + # Obtain the current value and remove the (value, iterator) pair,
2.147 + # pending insertion of a new pair for the iterator.
2.148 +
2.149 + current, iterator = self.current[0]
2.150 + del self.current[0]
2.151 +
2.152 + # Get the next value, if any and insert the value and iterator into the
2.153 + # ordered collection.
2.154 +
2.155 + t = self.get_next(iterator)
2.156 + if t:
2.157 + insort_left(self.current, t)
2.158 +
2.159 + return current
2.160 +
2.161 def get_overlapping(first, second):
2.162
2.163 """