1.1 --- a/imiptools/period.py Tue Feb 09 15:57:23 2016 +0100
1.2 +++ b/imiptools/period.py Wed Mar 02 21:17:11 2016 +0100
1.3 @@ -454,280 +454,328 @@
1.4 def make_corrected(self, start, end):
1.5 return self.__class__(start, end, self.tzid, self.origin, self.get_start_attr(), self.get_end_attr())
1.6
1.7 -# Time and period management.
1.8 +class FreeBusyCollection:
1.9 +
1.10 + "An abstraction for a collection of free/busy periods."
1.11 +
1.12 + def __init__(self, periods=None):
1.13 +
1.14 + """
1.15 + Initialise the collection with the given list of 'periods', or start an
1.16 + empty collection if no list is given.
1.17 + """
1.18 +
1.19 + self.periods = periods or []
1.20 +
1.21 + # List emulation methods.
1.22
1.23 -def can_schedule(freebusy, periods, uid, recurrenceid):
1.24 + def __list__(self):
1.25 + return self.periods
1.26 +
1.27 + def __iter__(self):
1.28 + return iter(self.periods)
1.29 +
1.30 + def __len__(self):
1.31 + return len(self.periods)
1.32 +
1.33 + def __getitem__(self, i):
1.34 + return self.periods[i]
1.35 +
1.36 + def __iadd__(self, other):
1.37 + for period in other:
1.38 + self.insert_period(period)
1.39 + return self
1.40 +
1.41 + # Operations.
1.42
1.43 - """
1.44 - Return whether the 'freebusy' list can accommodate the given 'periods'
1.45 - employing the specified 'uid' and 'recurrenceid'.
1.46 - """
1.47 + def can_schedule(self, periods, uid, recurrenceid):
1.48 +
1.49 + """
1.50 + Return whether the collection can accommodate the given 'periods'
1.51 + employing the specified 'uid' and 'recurrenceid'.
1.52 + """
1.53 +
1.54 + for conflict in self.have_conflict(periods, True):
1.55 + if conflict.uid != uid or conflict.recurrenceid != recurrenceid:
1.56 + return False
1.57 +
1.58 + return True
1.59 +
1.60 + def have_conflict(self, periods, get_conflicts=False):
1.61
1.62 - for conflict in have_conflict(freebusy, periods, True):
1.63 - if conflict.uid != uid or conflict.recurrenceid != recurrenceid:
1.64 + """
1.65 + Return whether any period in the collection overlaps with the given
1.66 + 'periods', returning a collection of such overlapping periods if
1.67 + 'get_conflicts' is set to a true value.
1.68 + """
1.69 +
1.70 + conflicts = set()
1.71 + for p in periods:
1.72 + overlapping = self.period_overlaps(p, get_conflicts)
1.73 + if overlapping:
1.74 + if get_conflicts:
1.75 + conflicts.update(overlapping)
1.76 + else:
1.77 + return True
1.78 +
1.79 + if get_conflicts:
1.80 + return conflicts
1.81 + else:
1.82 return False
1.83
1.84 - return True
1.85 + def insert_period(self, period):
1.86
1.87 -def have_conflict(freebusy, periods, get_conflicts=False):
1.88 + "Insert the given 'period' into the collection."
1.89
1.90 - """
1.91 - Return whether any period in 'freebusy' overlaps with the given 'periods',
1.92 - returning a collection of such overlapping periods if 'get_conflicts' is
1.93 - set to a true value.
1.94 - """
1.95 + i = bisect_left(self.periods, period)
1.96 + if i == len(self.periods):
1.97 + self.periods.append(period)
1.98 + elif self.periods[i] != period:
1.99 + self.periods.insert(i, period)
1.100 +
1.101 + def remove_periods(self, periods):
1.102
1.103 - conflicts = set()
1.104 - for p in periods:
1.105 - overlapping = period_overlaps(freebusy, p, get_conflicts)
1.106 - if overlapping:
1.107 - if get_conflicts:
1.108 - conflicts.update(overlapping)
1.109 - else:
1.110 - return True
1.111 + "Remove the given 'periods' from the collection."
1.112 +
1.113 + for period in periods:
1.114 + i = bisect_left(self.periods, period)
1.115 + if i < len(self.periods) and self.periods[i] == period:
1.116 + del self.periods[i]
1.117
1.118 - if get_conflicts:
1.119 - return conflicts
1.120 - else:
1.121 - return False
1.122 + def remove_event_periods(self, uid, recurrenceid=None):
1.123
1.124 -def insert_period(freebusy, period):
1.125 -
1.126 - "Insert into 'freebusy' the given 'period'."
1.127 + """
1.128 + Remove from the collection all periods associated with 'uid' and
1.129 + 'recurrenceid' (which if omitted causes the "parent" object's periods to
1.130 + be referenced).
1.131
1.132 - i = bisect_left(freebusy, period)
1.133 - if i == len(freebusy):
1.134 - freebusy.append(period)
1.135 - elif freebusy[i] != period:
1.136 - freebusy.insert(i, period)
1.137 -
1.138 -def remove_periods(freebusy, periods):
1.139 + Return the removed periods.
1.140 + """
1.141
1.142 - "Remove from 'freebusy' the given 'periods'."
1.143 -
1.144 - for period in periods:
1.145 - i = bisect_left(freebusy, period)
1.146 - if i < len(freebusy) and freebusy[i] == period:
1.147 - del freebusy[i]
1.148 -
1.149 -def remove_event_periods(freebusy, uid, recurrenceid=None):
1.150 + removed = []
1.151 + i = 0
1.152 + while i < len(self.periods):
1.153 + fb = self.periods[i]
1.154 + if fb.uid == uid and fb.recurrenceid == recurrenceid:
1.155 + removed.append(self.periods[i])
1.156 + del self.periods[i]
1.157 + else:
1.158 + i += 1
1.159
1.160 - """
1.161 - Remove from 'freebusy' all periods associated with 'uid' and 'recurrenceid'
1.162 - (which if omitted causes the "parent" object's periods to be referenced).
1.163 + return removed
1.164
1.165 - Return the removed periods.
1.166 - """
1.167 + def remove_additional_periods(self, uid, recurrenceids=None):
1.168
1.169 - removed = []
1.170 - i = 0
1.171 - while i < len(freebusy):
1.172 - fb = freebusy[i]
1.173 - if fb.uid == uid and fb.recurrenceid == recurrenceid:
1.174 - removed.append(freebusy[i])
1.175 - del freebusy[i]
1.176 - else:
1.177 - i += 1
1.178 + """
1.179 + Remove from the collection all periods associated with 'uid' having a
1.180 + recurrence identifier indicating an additional or modified period.
1.181
1.182 - return removed
1.183 + If 'recurrenceids' is specified, remove all periods associated with
1.184 + 'uid' that do not have a recurrence identifier in the given list.
1.185 +
1.186 + Return the removed periods.
1.187 + """
1.188
1.189 -def remove_additional_periods(freebusy, uid, recurrenceids=None):
1.190 -
1.191 - """
1.192 - Remove from 'freebusy' all periods associated with 'uid' having a
1.193 - recurrence identifier indicating an additional or modified period.
1.194 + removed = []
1.195 + i = 0
1.196 + while i < len(self.periods):
1.197 + fb = self.periods[i]
1.198 + if fb.uid == uid and fb.recurrenceid and (
1.199 + recurrenceids is None or
1.200 + recurrenceids is not None and fb.recurrenceid not in recurrenceids
1.201 + ):
1.202 + removed.append(self.periods[i])
1.203 + del self.periods[i]
1.204 + else:
1.205 + i += 1
1.206
1.207 - If 'recurrenceids' is specified, remove all periods associated with 'uid'
1.208 - that do not have a recurrence identifier in the given list.
1.209 + return removed
1.210
1.211 - Return the removed periods.
1.212 - """
1.213 + def remove_affected_period(self, uid, start):
1.214
1.215 - removed = []
1.216 - i = 0
1.217 - while i < len(freebusy):
1.218 - fb = freebusy[i]
1.219 - if fb.uid == uid and fb.recurrenceid and (
1.220 - recurrenceids is None or
1.221 - recurrenceids is not None and fb.recurrenceid not in recurrenceids
1.222 - ):
1.223 - removed.append(freebusy[i])
1.224 - del freebusy[i]
1.225 - else:
1.226 - i += 1
1.227 + """
1.228 + Remove from the collection the period associated with 'uid' that
1.229 + provides an occurrence starting at the given 'start' (provided by a
1.230 + recurrence identifier, converted to a datetime). A recurrence identifier
1.231 + is used to provide an alternative time period whilst also acting as a
1.232 + reference to the originally-defined occurrence.
1.233 +
1.234 + Return any removed period in a list.
1.235 + """
1.236
1.237 - return removed
1.238 + removed = []
1.239 +
1.240 + search = Period(start, start)
1.241 + found = bisect_left(self.periods, search)
1.242
1.243 -def remove_affected_period(freebusy, uid, start):
1.244 + while found < len(self.periods):
1.245 + fb = self.periods[found]
1.246 +
1.247 + # Stop looking if the start no longer matches the recurrence identifier.
1.248
1.249 - """
1.250 - Remove from 'freebusy' a period associated with 'uid' that provides an
1.251 - occurrence starting at the given 'start' (provided by a recurrence
1.252 - identifier, converted to a datetime). A recurrence identifier is used to
1.253 - provide an alternative time period whilst also acting as a reference to the
1.254 - originally-defined occurrence.
1.255 + if fb.get_start_point() != search.get_start_point():
1.256 + break
1.257 +
1.258 + # If the period belongs to the parent object, remove it and return.
1.259
1.260 - Return any removed period in a list.
1.261 - """
1.262 -
1.263 - removed = []
1.264 + if not fb.recurrenceid and uid == fb.uid:
1.265 + removed.append(self.periods[found])
1.266 + del self.periods[found]
1.267 + break
1.268
1.269 - search = Period(start, start)
1.270 - found = bisect_left(freebusy, search)
1.271 + # Otherwise, keep looking for a matching period.
1.272 +
1.273 + found += 1
1.274
1.275 - while found < len(freebusy):
1.276 - fb = freebusy[found]
1.277 + return removed
1.278 +
1.279 + def periods_from(self, period):
1.280
1.281 - # Stop looking if the start no longer matches the recurrence identifier.
1.282 + "Return the entries in the collection at or after 'period'."
1.283
1.284 - if fb.get_start_point() != search.get_start_point():
1.285 - break
1.286 + first = bisect_left(self.periods, period)
1.287 + return self.periods[first:]
1.288
1.289 - # If the period belongs to the parent object, remove it and return.
1.290 + def periods_until(self, period):
1.291 +
1.292 + "Return the entries in the collection before 'period'."
1.293
1.294 - if not fb.recurrenceid and uid == fb.uid:
1.295 - removed.append(freebusy[found])
1.296 - del freebusy[found]
1.297 - break
1.298 + last = bisect_right(self.periods, Period(period.get_end(), period.get_end(), period.get_tzid()))
1.299 + return self.periods[:last]
1.300 +
1.301 + def get_overlapping(self, period):
1.302
1.303 - # Otherwise, keep looking for a matching period.
1.304 -
1.305 - found += 1
1.306 -
1.307 - return removed
1.308 -
1.309 -def periods_from(freebusy, period):
1.310 + """
1.311 + Return the entries in the collection providing periods overlapping with
1.312 + 'period'.
1.313 + """
1.314
1.315 - "Return the entries in 'freebusy' at or after 'period'."
1.316 + # Find the range of periods potentially overlapping the period in the
1.317 + # free/busy collection.
1.318
1.319 - first = bisect_left(freebusy, period)
1.320 - return freebusy[first:]
1.321 + startpoints = self.periods_until(period)
1.322
1.323 -def periods_until(freebusy, period):
1.324 + # Find the range of periods potentially overlapping the period in a version
1.325 + # of the free/busy collection sorted according to end datetimes.
1.326
1.327 - "Return the entries in 'freebusy' before 'period'."
1.328 + endpoints = [(Period(fb.get_end_point(), fb.get_end_point()), fb) for fb in startpoints]
1.329 + endpoints.sort()
1.330 + first = bisect_left(endpoints, (Period(period.get_start_point(), period.get_start_point()),))
1.331 + endpoints = endpoints[first:]
1.332
1.333 - last = bisect_right(freebusy, Period(period.get_end(), period.get_end(), period.get_tzid()))
1.334 - return freebusy[:last]
1.335 + overlapping = set()
1.336
1.337 -def get_overlapping(freebusy, period):
1.338 + for p, fb in endpoints:
1.339 + if fb.overlaps(period):
1.340 + overlapping.add(fb)
1.341
1.342 - """
1.343 - Return the entries in 'freebusy' providing periods overlapping with
1.344 - 'period'.
1.345 - """
1.346 + overlapping = list(overlapping)
1.347 + overlapping.sort()
1.348 + return overlapping
1.349
1.350 - # Find the range of periods potentially overlapping the period in the
1.351 - # free/busy collection.
1.352 + def period_overlaps(self, period, get_periods=False):
1.353
1.354 - startpoints = periods_until(freebusy, period)
1.355 -
1.356 - # Find the range of periods potentially overlapping the period in a version
1.357 - # of the free/busy collection sorted according to end datetimes.
1.358 + """
1.359 + Return whether any period in the collection overlaps with the given
1.360 + 'period', returning a collection of overlapping periods if 'get_periods'
1.361 + is set to a true value.
1.362 + """
1.363
1.364 - endpoints = [(Period(fb.get_end_point(), fb.get_end_point()), fb) for fb in startpoints]
1.365 - endpoints.sort()
1.366 - first = bisect_left(endpoints, (Period(period.get_start_point(), period.get_start_point()),))
1.367 - endpoints = endpoints[first:]
1.368 -
1.369 - overlapping = set()
1.370 + overlapping = self.get_overlapping(period)
1.371
1.372 - for p, fb in endpoints:
1.373 - if fb.overlaps(period):
1.374 - overlapping.add(fb)
1.375 + if get_periods:
1.376 + return overlapping
1.377 + else:
1.378 + return len(overlapping) != 0
1.379
1.380 - overlapping = list(overlapping)
1.381 - overlapping.sort()
1.382 - return overlapping
1.383 + def remove_overlapping(self, period):
1.384
1.385 -def period_overlaps(freebusy, period, get_periods=False):
1.386 + "Remove all periods overlapping with 'period' from the collection."
1.387 +
1.388 + overlapping = self.get_overlapping(period)
1.389
1.390 - """
1.391 - Return whether any period in 'freebusy' overlaps with the given 'period',
1.392 - returning a collection of overlapping periods if 'get_periods' is set to a
1.393 - true value.
1.394 - """
1.395 + if overlapping:
1.396 + for fb in overlapping:
1.397 + self.periods.remove(fb)
1.398
1.399 - overlapping = get_overlapping(freebusy, period)
1.400 + def replace_overlapping(self, period, replacements):
1.401
1.402 - if get_periods:
1.403 - return overlapping
1.404 - else:
1.405 - return len(overlapping) != 0
1.406 + """
1.407 + Replace existing periods in the collection within the given 'period',
1.408 + using the given 'replacements'.
1.409 + """
1.410
1.411 -def remove_overlapping(freebusy, period):
1.412 + self.remove_overlapping(period)
1.413 + for replacement in replacements:
1.414 + self.insert_period(replacement)
1.415
1.416 - "Remove from 'freebusy' all periods overlapping with 'period'."
1.417 + def coalesce_freebusy(self):
1.418
1.419 - overlapping = get_overlapping(freebusy, period)
1.420 + "Coalesce the periods in the collection, returning a new collection."
1.421
1.422 - if overlapping:
1.423 - for fb in overlapping:
1.424 - freebusy.remove(fb)
1.425 + if not self.periods:
1.426 + return FreeBusyCollection(self.periods)
1.427
1.428 -def replace_overlapping(freebusy, period, replacements):
1.429 + fb = []
1.430 + start = self.periods[0].get_start_point()
1.431 + end = self.periods[0].get_end_point()
1.432
1.433 - """
1.434 - Replace existing periods in 'freebusy' within the given 'period', using the
1.435 - given 'replacements'.
1.436 - """
1.437 -
1.438 - remove_overlapping(freebusy, period)
1.439 - for replacement in replacements:
1.440 - insert_period(freebusy, replacement)
1.441 -
1.442 -def coalesce_freebusy(freebusy):
1.443 + for period in self.periods[1:]:
1.444 + if period.get_start_point() > end:
1.445 + fb.append(FreeBusyPeriod(start, end))
1.446 + start = period.get_start_point()
1.447 + end = period.get_end_point()
1.448 + else:
1.449 + end = max(end, period.get_end_point())
1.450
1.451 - "Coalesce the periods in 'freebusy'."
1.452 + fb.append(FreeBusyPeriod(start, end))
1.453 + return FreeBusyCollection(fb)
1.454
1.455 - if not freebusy:
1.456 - return freebusy
1.457 + def invert_freebusy(self):
1.458 +
1.459 + "Return the free periods from the collection as a new collection."
1.460
1.461 - fb = []
1.462 - start = freebusy[0].get_start_point()
1.463 - end = freebusy[0].get_end_point()
1.464 + if not self.periods:
1.465 + return FreeBusyCollection([FreeBusyPeriod(None, None)])
1.466 +
1.467 + # Coalesce periods that overlap or are adjacent.
1.468
1.469 - for period in freebusy[1:]:
1.470 - if period.get_start_point() > end:
1.471 - fb.append(FreeBusyPeriod(start, end))
1.472 - start = period.get_start_point()
1.473 - end = period.get_end_point()
1.474 - else:
1.475 - end = max(end, period.get_end_point())
1.476 + fb = self.coalesce_freebusy()
1.477 + free = []
1.478 +
1.479 + # Add a start-of-time period if appropriate.
1.480
1.481 - fb.append(FreeBusyPeriod(start, end))
1.482 - return fb
1.483 + first = fb[0].get_start_point()
1.484 + if first:
1.485 + free.append(FreeBusyPeriod(None, first))
1.486
1.487 -def invert_freebusy(freebusy):
1.488 -
1.489 - "Return the free periods from 'freebusy'."
1.490 + start = fb[0].get_end_point()
1.491
1.492 - if not freebusy:
1.493 - return [FreeBusyPeriod(None, None)]
1.494 -
1.495 - # Coalesce periods that overlap or are adjacent.
1.496 + for period in fb[1:]:
1.497 + free.append(FreeBusyPeriod(start, period.get_start_point()))
1.498 + start = period.get_end_point()
1.499
1.500 - fb = coalesce_freebusy(freebusy)
1.501 - free = []
1.502 + # Add an end-of-time period if appropriate.
1.503
1.504 - # Add a start-of-time period if appropriate.
1.505 + if start:
1.506 + free.append(FreeBusyPeriod(start, None))
1.507
1.508 - first = fb[0].get_start_point()
1.509 - if first:
1.510 - free.append(FreeBusyPeriod(None, first))
1.511 + return FreeBusyCollection(free)
1.512 +
1.513 + def update_freebusy(self, periods, transp, uid, recurrenceid, summary, organiser, expires=None):
1.514
1.515 - start = fb[0].get_end_point()
1.516 + """
1.517 + Update the free/busy details with the given 'periods', 'transp' setting,
1.518 + 'uid' plus 'recurrenceid' and 'summary' and 'organiser' details.
1.519
1.520 - for period in fb[1:]:
1.521 - free.append(FreeBusyPeriod(start, period.get_start_point()))
1.522 - start = period.get_end_point()
1.523 + An optional 'expires' datetime string indicates the expiry time of any
1.524 + free/busy offer.
1.525 + """
1.526
1.527 - # Add an end-of-time period if appropriate.
1.528 + self.remove_event_periods(uid, recurrenceid)
1.529
1.530 - if start:
1.531 - free.append(FreeBusyPeriod(start, None))
1.532 -
1.533 - return free
1.534 + for p in periods:
1.535 + self.insert_period(FreeBusyPeriod(p.get_start_point(), p.get_end_point(), uid, transp, recurrenceid, summary, organiser, expires))
1.536
1.537 # Period layout.
1.538
1.539 @@ -1014,19 +1062,4 @@
1.540
1.541 return spans
1.542
1.543 -def update_freebusy(freebusy, periods, transp, uid, recurrenceid, summary, organiser, expires=None):
1.544 -
1.545 - """
1.546 - Update the free/busy details with the given 'periods', 'transp' setting,
1.547 - 'uid' plus 'recurrenceid' and 'summary' and 'organiser' details.
1.548 -
1.549 - An optional 'expires' datetime string indicates the expiry time of any
1.550 - free/busy offer.
1.551 - """
1.552 -
1.553 - remove_event_periods(freebusy, uid, recurrenceid)
1.554 -
1.555 - for p in periods:
1.556 - insert_period(freebusy, FreeBusyPeriod(p.get_start_point(), p.get_end_point(), uid, transp, recurrenceid, summary, organiser, expires))
1.557 -
1.558 # vim: tabstop=4 expandtab shiftwidth=4